iOS 修改圖片尺寸的方法

目前在iOS上對(duì)于圖片的優(yōu)化點(diǎn)有很多,例如圖片解碼、圖片漸加載和圖片尺寸處理。這篇文章是說(shuō)明目前iOS 代碼中修改圖片尺寸的兩種方法,以及這兩種方法區(qū)別和注意點(diǎn)。

修改圖片尺寸的兩種方法

1. 畫布ImageContext(UIKit)

/** 利用畫布對(duì)圖片尺寸進(jìn)行修改
 @param data ---- 圖片Data
 @param maxPixelSize ---- 圖片最大寬/高尺寸 ,設(shè)置后圖片會(huì)根據(jù)最大寬/高 來(lái)等比例縮放圖片

 @return 目標(biāo)尺寸的圖片Image */
+ (UIImage*) getThumImgOfConextWithData:(NSData*)data withMaxPixelSize:(int)maxPixelSize
{
    UIImage *imgResult = nil;
    if(data == nil)         { return imgResult; }
    if(data.length <= 0)    { return imgResult; }
    if(maxPixelSize <= 0)   { return imgResult; }
    
    const int sizeTo = maxPixelSize; // 圖片最大的寬/高
    CGSize sizeResult;
    UIImage *img = [UIImage imageWithData:data];
    if(img.size.width > img.size.height){ // 根據(jù)最大的寬/高 值,等比例計(jì)算出最終目標(biāo)尺寸
        float value = img.size.width/ sizeTo;
        int height = img.size.height / value;
        sizeResult = CGSizeMake(sizeTo, height);
    } else {
        float value = img.size.height/ sizeTo;
        int width = img.size.width / value;
        sizeResult = CGSizeMake(width, sizeTo);
    }
    
    UIGraphicsBeginImageContextWithOptions(sizeResult, NO, 0);
    [img drawInRect:CGRectMake(0, 0, sizeResult.width, sizeResult.height)];
    img = nil;
    imgResult = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    return imgResult;
}

2. image I/O 創(chuàng)建省略圖

/** Image I/O 獲取指定尺寸的圖片,返回的結(jié)果Image 目標(biāo)尺寸大小 <= 圖片原始尺寸大小
 @param data ---- 圖片Data
 @param maxPixelSize ---- 圖片最大寬/高尺寸 ,設(shè)置后圖片會(huì)根據(jù)最大寬/高 來(lái)等比例縮放圖片

 @return 目標(biāo)尺寸的圖片Image  */
+ (UIImage*) getThumImgOfImgIOWithData:(NSData*)data withMaxPixelSize:(int)maxPixelSize
{
    UIImage *imgResult = nil;
    if(data == nil)         { return imgResult; }
    if(data.length <= 0)    { return imgResult; }
    if(maxPixelSize <= 0)   { return imgResult; }
    
    const float scale = [UIScreen mainScreen].scale;
    const int sizeTo = maxPixelSize * scale;
    CFDataRef dataRef = (__bridge CFDataRef)data;
    
    /* CGImageSource的鍵值說(shuō)明
     kCGImageSourceCreateThumbnailWithTransform - 設(shè)置縮略圖是否進(jìn)行Transfrom變換
     kCGImageSourceCreateThumbnailFromImageAlways - 設(shè)置是否創(chuàng)建縮略圖,無(wú)論原圖像有沒(méi)有包含縮略圖,默認(rèn)kCFBooleanFalse,影響 CGImageSourceCreateThumbnailAtIndex 方法
     kCGImageSourceCreateThumbnailFromImageIfAbsent - 設(shè)置是否創(chuàng)建縮略圖,如果原圖像有沒(méi)有包含縮略圖,則創(chuàng)建縮略圖,默認(rèn)kCFBooleanFalse,影響 CGImageSourceCreateThumbnailAtIndex 方法
     kCGImageSourceThumbnailMaxPixelSize - 設(shè)置縮略圖的最大寬/高尺寸 需要設(shè)置為CFNumber值,設(shè)置后圖片會(huì)根據(jù)最大寬/高 來(lái)等比例縮放圖片
     kCGImageSourceShouldCache - 設(shè)置是否以解碼的方式讀取圖片數(shù)據(jù) 默認(rèn)為kCFBooleanTrue,如果設(shè)置為true,在讀取數(shù)據(jù)時(shí)就進(jìn)行解碼 如果為false 則在渲染時(shí)才進(jìn)行解碼 */
    CFDictionaryRef dicOptionsRef = (__bridge CFDictionaryRef) @{
                                                                 (id)kCGImageSourceCreateThumbnailFromImageIfAbsent : @(YES),
                                                                 (id)kCGImageSourceThumbnailMaxPixelSize : @(sizeTo),
                                                                 (id)kCGImageSourceShouldCache : @(YES),
                                                                 };
    CGImageSourceRef src = CGImageSourceCreateWithData(dataRef, nil);
    CGImageRef thumImg = CGImageSourceCreateThumbnailAtIndex(src, 0, dicOptionsRef); //注意:如果設(shè)置 kCGImageSourceCreateThumbnailFromImageIfAbsent為 NO,那么 CGImageSourceCreateThumbnailAtIndex 會(huì)返回nil
    
    CFRelease(src); // 注意釋放對(duì)象,否則會(huì)產(chǎn)生內(nèi)存泄露
    
    imgResult = [UIImage imageWithCGImage:thumImg scale:scale orientation:UIImageOrientationUp];
    
    if(thumImg != nil){
        CFRelease(thumImg); // 注意釋放對(duì)象,否則會(huì)產(chǎn)生內(nèi)存泄露
    }
    
    return imgResult;
}

需要注意的是, 使用Image I/O 時(shí),設(shè)置kCGImageSourceThumbnailMaxPixelSize 的最大高/寬值時(shí),如果設(shè)置值超過(guò)了圖片文件原本的高/寬值,那么CGImageSourceCreateThumbnailAtIndex獲取的圖片尺寸將是原始圖片文件的尺寸。比如,設(shè)置 kCGImageSourceThumbnailMaxPixelSize 為600,而如果圖片文件尺寸為580*212,那么最終獲取到的圖片尺寸是580 * 212。


小注釋:UIKit處理很大的圖片時(shí),容易出現(xiàn)內(nèi)存崩潰(超過(guò)App可使用內(nèi)存的上限),原因是[UIImage drawInRect:]在繪制時(shí),會(huì)先解碼圖片,再生成原始分辨率大小的bitmap,這會(huì)占用很大的內(nèi)存,并且還有位數(shù)對(duì)齊等耗時(shí)操作。目前我知道的較好方法是使用ImageIO接口,避免在改變圖片大小的過(guò)程中產(chǎn)生臨時(shí)的bitmap。


兩種方法的效率區(qū)別

一般我們要決定使用哪種方法的時(shí)候,首先都是看哪種方法的效率比較高,那么我們現(xiàn)在比較這兩種方法的效率。

測(cè)試代碼:

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSMutableArray<UIImage*> *muAry = [NSMutableArray new];
        NSTimeInterval timeBegin = [[NSDate date] timeIntervalSince1970];
        for(int i=0; i<200; i+=1){ // 循環(huán)兩百次
            @autoreleasepool{ // 這里注意,需要加上autoreleasepool,具體原因等下說(shuō)明
                int index = i%5; // 我在項(xiàng)目放了五張圖片
                NSString *strName = [NSString stringWithFormat:@"temp%i", index];
                NSString *strFilePath = [[NSBundle mainBundle] pathForResource:strName ofType:@"jpg"];
                NSData *data = [NSData dataWithContentsOfFile:strFilePath];
                UIImage *img = [self.class getThumImgOfConextWithData:data withMaxPixelSize:500]; // ImageContext 方法
                // UIImage *img = [self.class getThumImgOfImgIOWithData:data withMaxPixelSize:500]; // Image I/O方法
                [muAry addObject:img];
                data = nil;
                strFilePath = nil;
            }
        }
        NSTimeInterval timeEnd = [[NSDate date] timeIntervalSince1970];
        NSLog(@"耗費(fèi)時(shí)間:%f", timeEnd - timeBegin);// 處理耗費(fèi)時(shí)間
    });

模擬器上測(cè)試,輸出結(jié)果:

/** ImageContext */
2018-03-07 15:58:38.836944+0800 Demo[39119:3623621] 耗費(fèi)時(shí)間:6.395285

/** Image I/O */
2018-03-07 15:59:35.482825+0800 JDDemo[39144:3626712] 耗費(fèi)時(shí)間:6.306523

從時(shí)間看,兩種方法的效率其實(shí)是差不多的,看樣子用哪種方法都可以的。


但是,需要注意一點(diǎn)?。?!
ImageContext有一個(gè)很嚴(yán)重的問(wèn)題
那就是占用內(nèi)存!

首先,你可以注意到上面的測(cè)試代碼,我在for循環(huán)里面添加了@autoreleasepool,你可以把他去掉再運(yùn)行試試。


屏幕快照 2018-03-07 16.05.38.png

運(yùn)行占用內(nèi)存Memory可以隨時(shí)讓你的App say goodbye ! ??!
為什么會(huì)出現(xiàn)這種情況呢,接下來(lái)我用Time Profiler分析一下。


屏幕快照 2018-03-07 16.13.44.png

從調(diào)用的方法可以看到,ImageContext方法的drawInRect底層也是使用image I/O 對(duì)圖片進(jìn)行處理。Image I/O函數(shù)會(huì)創(chuàng)建一個(gè)圖片數(shù)據(jù)對(duì)象保存,但是關(guān)閉ImageContext我們只有一個(gè)方法:UIGraphicsEndImageContext。那么我們來(lái)看看這個(gè)方法干了什么。

屏幕快照 2018-03-07 16.19.22.png

可以看到,這個(gè)方法僅僅是把Context對(duì)象從棧頂釋放,卻沒(méi)有釋放我們的圖片內(nèi)存數(shù)據(jù),怪不得內(nèi)存那么高?。?!

那么為什么添加了@autoreleasepool就可以解決了呢,我推測(cè)是底層代碼對(duì)圖片數(shù)據(jù)對(duì)象 添加了 autorelease 標(biāo)識(shí),那么他就會(huì)添加到最近的 autoreleasepool 中。(如果你不手動(dòng)添加一層autoreleasepool,那么就會(huì)添加到dispatch_async自動(dòng)添加的autoreleasepool,這個(gè)需要等子線程運(yùn)行結(jié)束才會(huì)被釋放,關(guān)于autoreleasepool可以看我的這篇文章:http://www.itdecent.cn/p/61d8131c6bf3
以圖為證:(沒(méi)有手動(dòng)添加@autoreleasepool的情況)

屏幕快照 2018-03-07 16.27.15.png

這就搞明白了為什么運(yùn)行時(shí)內(nèi)存那么高啦,因?yàn)樗袌D片的數(shù)據(jù)對(duì)象要等到子線程運(yùn)行結(jié)束后才會(huì)釋放!
那么我們添加@autoreleasepool在for內(nèi),然后運(yùn)行看看 autoreleasepool 做了什么處理


屏幕快照 2018-03-07 16.31.53.png

放上drawInRect的細(xì)節(jié)圖對(duì)比更清晰


屏幕快照 2018-03-07 16.36.25.png

好啦,大概明白為什么要加一層@autoreleasepool了吧,不過(guò)再深究是不是再imageIO_Malloc導(dǎo)致的占用內(nèi)存,我就搞不明白啦,畢竟水平有限,我也看著很頭疼…

那么為什么用Image I/O沒(méi)有這個(gè)問(wèn)題呢
因?yàn)椋覀円呀?jīng)手動(dòng)調(diào)用了CFRelease

CFRelease(src);
CFRelease(thumbnail);

最后說(shuō)明一下,這篇是我自己找方法監(jiān)測(cè)的,可能存在有錯(cuò)誤的地方,如果大神們發(fā)現(xiàn)了,請(qǐng)告訴我一聲唄,不勝感激?。。?/p>


2018.10.09 后續(xù)

最近在看資料CoreImage的時(shí)候,看到了CoreImage也有一種方法可以進(jìn)行圖片尺寸,那就是利用CIFilter濾鏡。

3. CoreImage

/** CoreImage 獲取指定尺寸的圖片,返回的結(jié)果Image 目標(biāo)尺寸大小 <= 圖片原始尺寸大小
 @param data ---- 圖片Data
 @param maxPixelSize ---- 圖片最大寬/高尺寸 ,設(shè)置后圖片會(huì)根據(jù)最大寬/高 來(lái)等比例縮放圖片
 
 @return 目標(biāo)尺寸的圖片Image  */
+ (UIImage*) getThumImgOfCIWithData:(NSData*)data withMaxPixelSize:(int)maxPixelSize{
    
    UIImage *imgResult = nil;
    if(data == nil)         { return imgResult; }
    if(data.length <= 0)    { return imgResult; }
    if(maxPixelSize <= 0)   { return imgResult; }
    
    const float scale = [UIScreen mainScreen].scale;
    CIImage *imgInput = [CIImage imageWithData:data];
    if(imgInput == nil) { return imgResult; }
    const float maxSizeTo = scale * maxPixelSize;
    
    float scaleHandle = 0;
    CGSize sizeImg = imgInput.extent.size;
    
    if(sizeImg.width > sizeImg.height){ // 根據(jù)最大的寬/高 值,等比例計(jì)算出最終目標(biāo)尺寸
        scaleHandle = maxSizeTo / sizeImg.width;
    } else {
        scaleHandle = maxSizeTo / sizeImg.height;
    }
    if(scaleHandle > 1.0){
        scaleHandle = 1.0;
    }
    
    CIFilter *filter = [CIFilter filterWithName:@"CILanczosScaleTransform"];
    [filter setValue:imgInput forKey:kCIInputImageKey];
    [filter setValue:@(scaleHandle) forKey:kCIInputScaleKey]; // 設(shè)置圖片的縮放比例
    CIImage *imgOuput = [filter valueForKey:kCIOutputImageKey];
    if(imgOuput != nil){ // 此時(shí)imgOuput屬于CIImage,不能直接通過(guò)CPU渲染到屏幕上,需要一個(gè)中間對(duì)象進(jìn)行轉(zhuǎn)換
        
        // 方法1:CIContext
        NSDictionary *dicOptions = @{kCIContextUseSoftwareRenderer : @(YES)}; // kCIContextUseSoftwareRenderer 默認(rèn)YES,設(shè)置YES是創(chuàng)建基于GPU的CIContext對(duì)象,效率要比CPU高很多。
        CIContext *context = [CIContext contextWithOptions:dicOptions];
        CGImageRef imgRef = [context createCGImage:imgOuput fromRect:imgOuput.extent];
        imgResult = [UIImage imageWithCGImage:imgRef scale:scale orientation:UIImageOrientationUp];
        
        // 方法2: [UIImage imageWithCIImage:]生成UIImage,但是這個(gè)方法不能指定CIContext的設(shè)置
//        imgResult = [UIImage imageWithCIImage:imgOuput scale:scale orientation:UIImageOrientationUp];
        
        /* ========================================================
         方法1和2的區(qū)別在于,方法1把圖片渲染到屏幕的準(zhǔn)備工作已經(jīng)提前完成了,CPU可以直接把結(jié)果圖片顯示到圖片上;
         而方法2則是把屏幕渲染工作推遲到了圖片真正顯示到屏幕的時(shí)候才進(jìn)行,會(huì)卡住主線程的。
          ======================================================== */
    }
    
    return imgResult;
}

不過(guò)CIFilter的主要問(wèn)題在于,雖然其處理圖片渲染很強(qiáng)大,但是在進(jìn)行圖片尺寸縮放的操作時(shí)會(huì)比較耗時(shí),明顯比ImageI/O和UIKit慢,所以這個(gè)方法僅僅只是說(shuō)明一下,在處理圖片尺寸時(shí)優(yōu)先選用ImageI/O。

最后這是我做方法對(duì)比時(shí)寫的demo結(jié)果截圖(把原圖壓縮到100時(shí)各個(gè)方法的圖片內(nèi)存大小)。


Simulator Screen Shot - iPhone 6s - 2018-10-09 at 14.22.11.png
/** 獲取圖片在內(nèi)存中占用的空間大小 */
+ (UInt64) getMemorySizeWithImg:(UIImage*)img{
    UInt64 cgImageBytesPerRow = CGImageGetBytesPerRow(img.CGImage);
    UInt64 cgImageHeight = CGImageGetHeight(img.CGImage);
    UInt64 size  = cgImageHeight * cgImageBytesPerRow;
    NSLog(@"MemorySize:%lu Bytes",(unsigned long)size);
    return size;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,753評(píng)論 25 709
  • *面試心聲:其實(shí)這些題本人都沒(méi)怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,580評(píng)論 30 472
  • 中國(guó)特色小鎮(zhèn)操盤手與投資人全球峰會(huì)在北京朗麗茲西山花園酒店舉辦,此次峰會(huì)很榮幸邀請(qǐng)到了國(guó)家發(fā)改委國(guó)際合作中心投融資...
    Hcxzh小鎮(zhèn)閱讀 488評(píng)論 0 3
  • 01 大學(xué)里最后一個(gè)暑假,打著學(xué)車的旗號(hào)在家里得過(guò)且過(guò)。 晚上八點(diǎn)黃金檔,我在電視機(jī)前對(duì)每個(gè)臺(tái)的節(jié)目進(jìn)行掃射。微信...
    一只螃蟹367閱讀 378評(píng)論 0 0
  • 昨晚知道專業(yè)課成績(jī),自己差的太多了。很失望吧!父母也很難受,今天一上午都在頹廢,感覺(jué)自己一無(wú)所有了?,F(xiàn)在想想這只是...
    賞心悅事閱讀 229評(píng)論 0 0

友情鏈接更多精彩內(nèi)容