目前在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)0疟小!卸耘!
ImageContext有一個(gè)很嚴(yán)重的問(wèn)題
那就是占用內(nèi)存退敦!
首先,你可以注意到上面的測(cè)試代碼蚣抗,我在for循環(huán)里面添加了@autoreleasepool侈百,你可以把他去掉再運(yùn)行試試。
運(yùn)行占用內(nèi)存Memory可以隨時(shí)讓你的App say goodbye 翰铡! 6塾颉!
為什么會(huì)出現(xiàn)這種情況呢锭魔,接下來(lái)我用Time Profiler分析一下例证。
從調(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è)方法干了什么党涕。
可以看到烦感,這個(gè)方法僅僅是把Context對(duì)象從棧頂釋放巡社,卻沒(méi)有釋放我們的圖片內(nèi)存數(shù)據(jù)膛堤,怪不得內(nèi)存那么高!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.reibang.com/p/61d8131c6bf3)
以圖為證:(沒(méi)有手動(dòng)添加@autoreleasepool的情況)
這就搞明白了為什么運(yùn)行時(shí)內(nèi)存那么高啦,因?yàn)樗袌D片的數(shù)據(jù)對(duì)象要等到子線程運(yùn)行結(jié)束后才會(huì)釋放!
那么我們添加@autoreleasepool在for內(nèi)蚜锨,然后運(yùn)行看看 autoreleasepool 做了什么處理
放上drawInRect的細(xì)節(jié)圖對(duì)比更清晰
好啦档插,大概明白為什么要加一層@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)告訴我一聲唄,不勝感激>登病V嵩邸!
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)存大性谐)。
/** 獲取圖片在內(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;
}