圖片裁剪/縮略性能探究

最近搞圖片縮略,總結(jié)了幾種不同方式的。具體如下

UIKit

UIGraphicsBeginImageContext & drawInRect


- (UIImage*)scaleWithUIKit:(CGSize)size {
    TICK
    CGFloat width = self.size.width;
    CGFloat height = self.size.height;
    if (width * height ==0) {
        return self;
    }

    float verticalRadio  = size.height * 1.0 / height;
    float horizontalRadio = size.width * 1.0 / width;

    floatradio = 1;
    if (verticalRadio <1 || horizontalRadio <1) {
        radio = MIN(verticalRadio, horizontalRadio);
    }

    width = width * radio;
    height = height * radio;

    // 創(chuàng)建一個bitmap的context 并把它設(shè)置成為當(dāng)前正在使用的context
    UIGraphicsBeginImageContext(CGSizeMake(width - 1, height - 1));
    // 繪制改變大小的圖片
    [self drawInRect:CGRectMake(0,0, width, height)];
    // 從當(dāng)前context中創(chuàng)建一個改變大小后的圖片
    UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    // 使當(dāng)前的context出堆棧
    UIGraphicsEndImageContext();
    TOCK
    // 返回新的改變大小后的圖片
    return scaledImage;
}

PS: 其中的TICK 和 TOCK 定義如下

#define TICK  CFAbsoluteTime before = CFAbsoluteTimeGetCurrent();
#define TOCK  NSLog(@"resize: %.2f ms", (CFAbsoluteTimeGetCurrent() - before) *1000);
// UIKit
NSString*path = [[NSBundlemainBundle]pathForResource:nameofType:exts];
UIImage*image = [UIImageimageWithContentsOfFile:path];
image = [imagescaleWithUIKit:UPLOAD_IMG_SIZE];

使用Time Profiler查看系統(tǒng)的調(diào)用過程懦鼠,結(jié)果如下:

image

可以看出scaleImage方法中,[UIImage imageWithContentOfFile:]方法耗時11.00ms偎蘸,scaleWithUIKit:方法耗時34.00ms聚蝶,而[UIImage imageWithContentOfFile:]方法具體做了什么,可以參考我的另一篇文章圖片ImageI/O解碼探究

我們看下scaleWithUIKit:方法具體做了什么:

PNG的scaleWithUIKit

可以看出來钝吮,調(diào)用了PNGPlugin庫中的_cg_png_read_row方法埋涧,進行了圖片解碼。只不過奇瘦,獲取圖片的過程在[UIImage imageWithContentOfFile:]方法中棘催,進行了解壓縮。

JPEG的scaleWithUIKit

可以看出來耳标,scaleWithUIKit方法中醇坝,調(diào)用了AppleJPEGPlugin庫中的FigPhotoJPEGDecodeJPEGIntoRGBSurface方法進行了解碼。

CoreGraphics

CGBitmapContextCreate & CGContextDrawImage & CGBitmapContextCreateImage


- (UIImage*)resizeCG:(CGSize)size {
    TICK
    if (!self) {
        return nil;
    }

    CGFloat width = self.size.width;
    CGFloat height = self.size.height;
    if (width * height == 0) {
        return self;
    }

    float verticalRadio  = size.height * 1.0 / height;
    float horizontalRadio = size.width * 1.0 / width;

    floatradio =1;
    if (verticalRadio <1 || horizontalRadio <1) {
        radio = MIN(verticalRadio, horizontalRadio);
    }

    width = width * radio;
    height = height * radio;

    CGImageRefimageRef = self.CGImage;
    size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
    size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
    CGColorSpaceRef colorSpaceRef = CGImageGetColorSpace(imageRef);
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);

    CGContextRefcontext =CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorSpaceRef, bitmapInfo);
    if (!context) return nil;

    CGContextDrawImage(context,CGRectMake(0,0, width, height), imageRef);// decode
    CGImageRef newImageRef = CGBitmapContextCreateImage(context);
    UIImage *newImage = [UIImageimageWithCGImage:newImageRef];

    CFRelease(context);
    CGImageRelease(newImageRef);
    TOCK
    return newImage;
}

調(diào)用過程如下:

PNG的resizeCG

其中的resizeCG耗時14.00ms次坡,其中也進行了解碼呼猪。

JPEG的resizeCG

可以看出,resizeCG方法耗時28.00ms贸毕,其中也進行了解碼郑叠。(耗時過程有可能有誤差)

PS: 在iOS11.0~iOS11.4版本中,由于蘋果手機快捷鍵屏幕截屏生成的圖片中的bitmap信息產(chǎn)生了變化明棍,導(dǎo)致上面的Core Graphics方法使用過程中報錯CGBitmapContextCreate: unsupported parameter combination乡革,因此需要作出更改,具體報錯分析請移步至CGBitmapContextCreate: unsupported parameter combination問題調(diào)查及解決

ImageIO

CGImageSourceCreateThumbnailAtIndex


- (UIImage*)resizeWithData:(NSData*)data scaleSize:(CGSize)size {

    if (!data) {
        returnnil;
    }

    // Create the image source
    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    if (!imageSourceRef) {
        return nil;
    }

    CGFloat maxPixelSize = MAX(size.width, size.height);
    // Create thumbnail options
    CFDictionaryRef options = (__bridge CFDictionaryRef) @{                                                       (__bridgeid)kCGImageSourceShouldCacheImmediately: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceShouldCache: (__bridgeid)kCFBooleanFalse,

                                                           (__bridgeid)kCGImageSourceCreateThumbnailFromImageAlways: (__bridgeid)kCFBooleanTrue,

                                                           (__bridgeid)kCGImageSourceThumbnailMaxPixelSize: [NSNumbernumberWithFloat:maxPixelSize]

                                                           };

    // Generate the thumbnail
    CGImageRefimageRef = CGImageSourceCreateThumbnailAtIndex(imageSourceRef, 0, options);

    UIImage*thumbnailImage = [UIImageimageWithCGImage:imageRef];

    CFRelease(imageSourceRef);
    CGImageRelease(imageRef);

    return thumbnailImage;
}

改方法是傳入的NSData對象摊腋,是未解壓的數(shù)據(jù)沸版,ImageI/O接受未解壓的數(shù)據(jù),進行decompress兴蒸,然后再解碼進行圖片的縮略视粮。針對ImageI/O的解碼,請移步至圖片ImageI/O解碼探究

CoreImage

  • (CIContext*)contextWithOptions: & createCGImage: fromRect:
- (UIImage *)resizeCI:(CGSize)size {

    if (!self) {
        return nil;
    }

    CGFloat width = self.size.width;
    CGFloat height = self.size.height;
    if (width * height ==0) {
        return self;
    }

    floatverticalRadio  = size.height * 1.0 / height;
    floathorizontalRadio = size.width * 1.0 / width;

    floatradio =1;
    if (verticalRadio < 1 || horizontalRadio < 1) {
        radio = MIN(verticalRadio, horizontalRadio);
    }

    CIImage *image = [CIImage imageWithCGImage:self.CGImage];
    CIFilter *filter = [CIFilter filterWithName:@"CILanczosScaleTransform"];
    [filter setValue:image forKey:kCIInputImageKey];
    [filter setValue:[NSNumber numberWithFloat:radio] forKey:kCIInputScaleKey];

    [filter setValue:[NSNumber numberWithFloat:1.0] forKey:kCIInputAspectRatioKey];

    CIImage *outputImage = [filtervalueForKey:kCIOutputImageKey];

    if (!outputImage) {
        returnnil;
    }

    CIContext *context = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer: @NO}];
    CGImageRefoutputImageRef = [contextcreateCGImage:outputImagefromRect:outputImage.extent];
    UIImage*newImage = [UIImageimageWithCGImage:outputImageRef];
    CGImageRelease(outputImageRef);

    return newImage;
}
PNG的resizeCI

可以看到改方法中調(diào)用了CoreImage中的CIMetalRenderToTextures橙凳,這句話是將緩沖區(qū)的數(shù)據(jù)渲染到紋理蕾殴,整個過程是將圖片渲染到畫布的一環(huán),其中進行了解碼操作岛啸。

JPEG的resizeCI

同樣看到了JPEG格式的圖片钓觉,也進行了紋理渲染。

vImage


- (UIImage*)resizeVI:(CGSize)size {
    if (!self) {
        return nil;
    }

    CGFloat width = self.size.width;
    CGFloat height = self.size.height;

    floatverticalRadio  = size.height * 1.0 / self.size.height;
    floathorizontalRadio = size.width * 1.0 / self.size.width;

    floatradio =1;
    if (verticalRadio < 1 || horizontalRadio < 1) {
        radio = MIN(verticalRadio, horizontalRadio);
    }

    width = width * radio;
    height = height * radio;

    CGImageRef imageRef = self.CGImage;
    uint32_t bitsPerComponent = (uint32_t)CGImageGetBitsPerComponent(imageRef);
    uint32_t bitsPerPixel = (uint32_t)CGImageGetBitsPerPixel(imageRef);
    CGColorSpaceRef colorSpaceRef = CGImageGetColorSpace(imageRef);
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
    vImage_CGImageFormat cgfromat = {
        .bitsPerComponent= bitsPerComponent,
        .bitsPerPixel = bitsPerPixel,
        .colorSpace = colorSpaceRef,
        .bitmapInfo = bitmapInfo,
        .version = 0,
        .decode = nil,
        .renderingIntent = kCGRenderingIntentDefault,
    };

    vImage_Buffer sourceBuffer = {};
    // 首先坚踩,創(chuàng)建一個buffer荡灾,可以用vImage提供的CGImage的便攜構(gòu)造方法,里面需要傳入原始數(shù)據(jù)所需要的format,這里就是ARGB8888
    vImage_Error a_ret = vImageBuffer_InitWithCGImage(&sourceBuffer, &cgfromat, NULL, imageRef, kvImageNoFlags);

    // 所有vImage的方法一般都有一個result批幌,判斷是否成功
    if (a_ret != kvImageNoError) return NULL;

    // create a destination buffer
    vImage_Buffer destBuffer = {};

    CGFloatscale =self.scale;
    uint32_t bytesPerPixel = bitsPerPixel / 8;
    uint32_t destBytesPerRow = bytesPerPixel * width;

    destBuffer.width = width;
    destBuffer.height = height;
    destBuffer.rowBytes = destBytesPerRow;
    destBuffer.data = malloc(destBuffer.rowBytes* destBuffer.height);

    vImage_Error ret = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, NULL, kvImageHighQualityResampling);
    if (ret != kvImageNoError) return NULL;

    CGImageRef outputImage = vImageCreateCGImageFromBuffer(&destBuffer, &cgfromat,NULL,NULL,kvImageNoFlags, &ret);
    if (ret != kvImageNoError) return NULL;

    UIImage *image = [[UIImage alloc] initWithCGImage:outputImage scale:scale orientation:self.imageOrientation];

    free(sourceBuffer.data);
    free(destBuffer.data);
    return image;
}

vImage對于大多人來說比較陌生础锐,看下vImage的過程:

PNG的resizeVI

同樣是經(jīng)過了解碼過程。而且注意荧缘,vImage底層解碼也是使用的ImageI/O的方法皆警。

JPEG的resizeVI

由此可見,vImage底層的解碼實現(xiàn)也是通過ImageI/O框架的方法截粗。

當(dāng)然耀怜,一些buffer的操作,以及vImageScale等操作是使用的vImage桐愉。其中涉及一些buffer的切換等操作。

以上五種方法掰派,分別進行將512x384从诲、1024x768、2048x1536三種尺寸的PNG和JPEG圖片縮略至256x192靡羡,統(tǒng)計了他們各自的耗時系洛。

因為ImageI/O接受的入?yún)⑹荖SData對象,會經(jīng)過解壓縮并解碼縮略略步,為了實驗的公平性描扯,其他四種入?yún)閁Iimage對象的方法,會將入?yún)Iimage的生成過程時間也算進去趟薄。這樣一來绽诚,統(tǒng)計了五種從path獲取到的圖片進行縮略的大致時間,統(tǒng)計如下:

PNG耗時統(tǒng)計
JPEG耗時統(tǒng)計

根據(jù)統(tǒng)計結(jié)果可以看出杭煎,CoreImage框架的方法性能相對較差恩够,其中的CoreGraphics和ImageI/O相對比較突出些,由于實驗材料不夠充分羡铲,更大尺寸的圖片沒有測試蜂桶,但是UIKit會隨著尺寸的增大,耗時會有較大的增加也切。同時扑媚,JPEG格式的圖片相對于PNG格式的圖片整體性能更好。

蘋果官方在Performance Best Practices section of the Core Image Programming Guide部分中特別推薦使用Core Graphics或Image I / O功能預(yù)先裁剪或縮小圖像雷恃。

那么基本確定了最好是使用CoreGraphics或者ImageI/O這兩種方案疆股。但是,影響性能并不只是耗時褂萧,同時內(nèi)存的分配也是考量的重要方面押桃。下面,針對CoreGraphics和ImageI/O,我們看一下兩者分配內(nèi)存的區(qū)別唱凯。

使用Instrments中的Allocations工具羡忘,查看內(nèi)存分配的情況。為了使實驗效果明顯磕昼,我們使用一張12000x12000尺寸的JPEG圖片卷雕,大小為20.9MB。我們的目標(biāo)是將這張圖片縮略到長寬不能超過256票从。

CoreGraphics

縮略之前:

CoreGraphics縮略之前的內(nèi)存情況

請注意勾選的 VM:CG image 一欄漫雕,此時該欄總共分配了16.00KiB的內(nèi)存空間,此時依然留存16.00KiB的空間峰鄙。(Total Bytes表示總共分配的內(nèi)存空間大小浸间,Persistent Bytes表示目前沒有被回收的、依然使用的內(nèi)存空間)

下面進行縮略吟榴,縮略之后:

CoreGraphics縮略之后的內(nèi)存情況

VM:CG image 一欄在縮略之后魁蒜,雖然最終的Persistent Bytes依然是16KiB,但是這個工程中Total Bytes為11.78MiB吩翻,也就是過程中臨時分配了11+MiB的空間兜看,雖然最后多余的內(nèi)存都被回收了,但是證明了在縮略過程中的瞬時消耗內(nèi)存多大11+MiB狭瞎。內(nèi)存的暴漲經(jīng)常會造成APP閃退细移。

之所以如此,是因為CGContextDrawImage時熊锭,先解碼圖片弧轧,再生成原始分辨率大小的bitmap,這個位圖大致相關(guān)于 圖片像素寬度 x 圖片像素高度 x 4球涛。

ImageI/O

那么ImageI/O如何呢劣针?我們來試驗下

縮略之前:

ImageI/O縮略之前的內(nèi)存情況

縮略之前跟CoreGraphics是一致的,在此不多贅述亿扁。

縮略之后:

ImageI/O縮略之后的內(nèi)存情況

VM:CG image 一欄在縮略之后捺典,最終的Persistent Bytes依然是16KiB,Total Bytes為僅僅為320.00KiB从祝,相對于使用CoreGraphics襟己,ImageI/O在縮略過程中,不會生成對應(yīng)的bitmap牍陌,大大降低了瞬時峰值擎浴,而且圖片越大這種效果越明顯。

結(jié)論:

1毒涧、推薦使用CoreGraphics或者ImageI/O進行縮略操作

2贮预、越大的圖片,更推薦使用ImageI/O,會大大降低瞬時內(nèi)存的峰值

參考文獻:

https://nshipster.com/image-resizing/

http://www.cocoachina.com/ios/20180305/22458.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仿吞,一起剝皮案震驚了整個濱河市滑频,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唤冈,老刑警劉巖峡迷,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異你虹,居然都是意外死亡绘搞,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門傅物,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夯辖,“玉大人,你說我怎么就攤上這事董饰÷ケⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵尖阔,是天一觀的道長。 經(jīng)常有香客問我榨咐,道長介却,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任块茁,我火速辦了婚禮齿坷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘数焊。我一直安慰自己永淌,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布佩耳。 她就那樣靜靜地躺著遂蛀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪干厚。 梳的紋絲不亂的頭發(fā)上李滴,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音蛮瞄,去河邊找鬼所坯。 笑死,一個胖子當(dāng)著我的面吹牛挂捅,可吹牛的內(nèi)容都是我干的芹助。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼状土!你這毒婦竟也來了无蜂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤声诸,失蹤者是張志新(化名)和其女友劉穎酱讶,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彼乌,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡泻肯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了慰照。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片灶挟。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖毒租,靈堂內(nèi)的尸體忽然破棺而出稚铣,到底是詐尸還是另有隱情,我是刑警寧澤墅垮,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布惕医,位于F島的核電站,受9級特大地震影響算色,放射性物質(zhì)發(fā)生泄漏抬伺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一灾梦、第九天 我趴在偏房一處隱蔽的房頂上張望峡钓。 院中可真熱鬧,春花似錦若河、人聲如沸能岩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拉鹃。三九已至,卻和暖如春鲫忍,著一層夾襖步出監(jiān)牢的瞬間毛俏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工饲窿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留煌寇,地道東北人。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓逾雄,卻偏偏與公主長得像阀溶,于是被迫代替她去往敵國和親腻脏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359