iOS 修改图片尺寸的方法

目前在iOS上对于图片的优化点有很多,例如图片解码、图片渐加载和图片尺寸处理。这篇文章是说明目前iOS 代码中修改图片尺寸的两种方法,以及这两种方法区别和注意点。

修改图片尺寸的两种方法

1. 画布ImageContext(UIKit)

/** 利用画布对图片尺寸进行修改
 @param data ---- 图片Data
 @param maxPixelSize ---- 图片最大宽/高尺寸 ,设置后图片会根据最大宽/高 来等比例缩放图片

 @return 目标尺寸的图片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){ // 根据最大的宽/高 值,等比例计算出最终目标尺寸
        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 创建省略图

/** Image I/O 获取指定尺寸的图片,返回的结果Image 目标尺寸大小 <= 图片原始尺寸大小
 @param data ---- 图片Data
 @param maxPixelSize ---- 图片最大宽/高尺寸 ,设置后图片会根据最大宽/高 来等比例缩放图片

 @return 目标尺寸的图片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的键值说明
     kCGImageSourceCreateThumbnailWithTransform - 设置缩略图是否进行Transfrom变换
     kCGImageSourceCreateThumbnailFromImageAlways - 设置是否创建缩略图,无论原图像有没有包含缩略图,默认kCFBooleanFalse,影响 CGImageSourceCreateThumbnailAtIndex 方法
     kCGImageSourceCreateThumbnailFromImageIfAbsent - 设置是否创建缩略图,如果原图像有没有包含缩略图,则创建缩略图,默认kCFBooleanFalse,影响 CGImageSourceCreateThumbnailAtIndex 方法
     kCGImageSourceThumbnailMaxPixelSize - 设置缩略图的最大宽/高尺寸 需要设置为CFNumber值,设置后图片会根据最大宽/高 来等比例缩放图片
     kCGImageSourceShouldCache - 设置是否以解码的方式读取图片数据 默认为kCFBooleanTrue,如果设置为true,在读取数据时就进行解码 如果为false 则在渲染时才进行解码 */
    CFDictionaryRef dicOptionsRef = (__bridge CFDictionaryRef) @{
                                                                 (id)kCGImageSourceCreateThumbnailFromImageIfAbsent : @(YES),
                                                                 (id)kCGImageSourceThumbnailMaxPixelSize : @(sizeTo),
                                                                 (id)kCGImageSourceShouldCache : @(YES),
                                                                 };
    CGImageSourceRef src = CGImageSourceCreateWithData(dataRef, nil);
    CGImageRef thumImg = CGImageSourceCreateThumbnailAtIndex(src, 0, dicOptionsRef); //注意:如果设置 kCGImageSourceCreateThumbnailFromImageIfAbsent为 NO,那么 CGImageSourceCreateThumbnailAtIndex 会返回nil
    
    CFRelease(src); // 注意释放对象,否则会产生内存泄露
    
    imgResult = [UIImage imageWithCGImage:thumImg scale:scale orientation:UIImageOrientationUp];
    
    if(thumImg != nil){
        CFRelease(thumImg); // 注意释放对象,否则会产生内存泄露
    }
    
    return imgResult;
}

需要注意的是, 使用Image I/O 时,设置kCGImageSourceThumbnailMaxPixelSize 的最大高/宽值时,如果设置值超过了图片文件原本的高/宽值,那么CGImageSourceCreateThumbnailAtIndex获取的图片尺寸将是原始图片文件的尺寸。比如,设置 kCGImageSourceThumbnailMaxPixelSize 为600,而如果图片文件尺寸为580*212,那么最终获取到的图片尺寸是580 * 212。


小注释:UIKit处理很大的图片时,容易出现内存崩溃(超过App可使用内存的上限),原因是[UIImage drawInRect:]在绘制时,会先解码图片,再生成原始分辨率大小的bitmap,这会占用很大的内存,并且还有位数对齐等耗时操作。目前我知道的较好方法是使用ImageIO接口,避免在改变图片大小的过程中产生临时的bitmap。


两种方法的效率区别

一般我们要决定使用哪种方法的时候,首先都是看哪种方法的效率比较高,那么我们现在比较这两种方法的效率。

测试代码:

 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){ // 循环两百次
            @autoreleasepool{ // 这里注意,需要加上autoreleasepool,具体原因等下说明
                int index = i%5; // 我在项目放了五张图片
                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", timeEnd - timeBegin);// 处理耗费时间
    });

模拟器上测试,输出结果:

/** ImageContext */
2018-03-07 15:58:38.836944+0800 Demo[39119:3623621] 耗费时间:6.395285

/** Image I/O */
2018-03-07 15:59:35.482825+0800 JDDemo[39144:3626712] 耗费时间:6.306523

从时间看,两种方法的效率其实是差不多的,看样子用哪种方法都可以的。


但是,需要注意一点!!!
ImageContext有一个很严重的问题
那就是占用内存!

首先,你可以注意到上面的测试代码,我在for循环里面添加了@autoreleasepool,你可以把他去掉再运行试试。


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

运行占用内存Memory可以随时让你的App say goodbye ! !!
为什么会出现这种情况呢,接下来我用Time Profiler分析一下。


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

从调用的方法可以看到,ImageContext方法的drawInRect底层也是使用image I/O 对图片进行处理。Image I/O函数会创建一个图片数据对象保存,但是关闭ImageContext我们只有一个方法:UIGraphicsEndImageContext。那么我们来看看这个方法干了什么。

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

可以看到,这个方法仅仅是把Context对象从栈顶释放,却没有释放我们的图片内存数据,怪不得内存那么高!!!

那么为什么添加了@autoreleasepool就可以解决了呢,我推测是底层代码对图片数据对象 添加了 autorelease 标识,那么他就会添加到最近的 autoreleasepool 中。(如果你不手动添加一层autoreleasepool,那么就会添加到dispatch_async自动添加的autoreleasepool,这个需要等子线程运行结束才会被释放,关于autoreleasepool可以看我的这篇文章:https://www.jianshu.com/p/61d8131c6bf3
以图为证:(没有手动添加@autoreleasepool的情况)

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

这就搞明白了为什么运行时内存那么高啦,因为所有图片的数据对象要等到子线程运行结束后才会释放!
那么我们添加@autoreleasepool在for内,然后运行看看 autoreleasepool 做了什么处理


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

放上drawInRect的细节图对比更清晰


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

好啦,大概明白为什么要加一层@autoreleasepool了吧,不过再深究是不是再imageIO_Malloc导致的占用内存,我就搞不明白啦,毕竟水平有限,我也看着很头疼…

那么为什么用Image I/O没有这个问题呢
因为,我们已经手动调用了CFRelease

CFRelease(src);
CFRelease(thumbnail);

最后说明一下,这篇是我自己找方法监测的,可能存在有错误的地方,如果大神们发现了,请告诉我一声呗,不胜感激!!!


2018.10.09 后续

最近在看资料CoreImage的时候,看到了CoreImage也有一种方法可以进行图片尺寸,那就是利用CIFilter滤镜。

3. CoreImage

/** CoreImage 获取指定尺寸的图片,返回的结果Image 目标尺寸大小 <= 图片原始尺寸大小
 @param data ---- 图片Data
 @param maxPixelSize ---- 图片最大宽/高尺寸 ,设置后图片会根据最大宽/高 来等比例缩放图片
 
 @return 目标尺寸的图片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){ // 根据最大的宽/高 值,等比例计算出最终目标尺寸
        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]; // 设置图片的缩放比例
    CIImage *imgOuput = [filter valueForKey:kCIOutputImageKey];
    if(imgOuput != nil){ // 此时imgOuput属于CIImage,不能直接通过CPU渲染到屏幕上,需要一个中间对象进行转换
        
        // 方法1:CIContext
        NSDictionary *dicOptions = @{kCIContextUseSoftwareRenderer : @(YES)}; // kCIContextUseSoftwareRenderer 默认YES,设置YES是创建基于GPU的CIContext对象,效率要比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,但是这个方法不能指定CIContext的设置
//        imgResult = [UIImage imageWithCIImage:imgOuput scale:scale orientation:UIImageOrientationUp];
        
        /* ========================================================
         方法1和2的区别在于,方法1把图片渲染到屏幕的准备工作已经提前完成了,CPU可以直接把结果图片显示到图片上;
         而方法2则是把屏幕渲染工作推迟到了图片真正显示到屏幕的时候才进行,会卡住主线程的。
          ======================================================== */
    }
    
    return imgResult;
}

不过CIFilter的主要问题在于,虽然其处理图片渲染很强大,但是在进行图片尺寸缩放的操作时会比较耗时,明显比ImageI/O和UIKit慢,所以这个方法仅仅只是说明一下,在处理图片尺寸时优先选用ImageI/O。

最后这是我做方法对比时写的demo结果截图(把原图压缩到100时各个方法的图片内存大小)。


Simulator Screen Shot - iPhone 6s - 2018-10-09 at 14.22.11.png
/** 获取图片在内存中占用的空间大小 */
+ (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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,522评论 25 707
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,125评论 29 470
  • 中国特色小镇操盘手与投资人全球峰会在北京朗丽兹西山花园酒店举办,此次峰会很荣幸邀请到了国家发改委国际合作中心投融资...
    Hcxzh小镇阅读 428评论 0 3
  • 01 大学里最后一个暑假,打着学车的旗号在家里得过且过。 晚上八点黄金档,我在电视机前对每个台的节目进行扫射。微信...
    一只螃蟹367阅读 312评论 0 0
  • 昨晚知道专业课成绩,自己差的太多了。很失望吧!父母也很难受,今天一上午都在颓废,感觉自己一无所有了。现在想想这只是...
    赏心悦事阅读 207评论 0 0