iOS优秀的图片压缩处理方案

大家好,好久没有更新博客了。一个早9晚5点半的硬是上成了996。悲剧的加班狗!

背景:

        最近遇到一个图片压缩的问题,项目需求压缩图片500k以内上传服务器,还要求图片要清晰一点。还有证明是图片500k已经确实很清晰了,那就没办法,做呗~~!(不喜欢听bb的可以直接去下面撸代码)

思路

        本来以为很简单的问题,自己随意写了一个UIImageJPEGRepresentation的方法进行一个循环压缩不就搞定了?,后来事实证明这个玩意儿很坑,有太多东西不是你想当然的。

1.为什么不提UIImagePNGRepresentation(<#UIImage * _Nonnull image#>)?

   回复:据说这个读取图片的大小会比较大,因为是png格式,读取的内容会有多图层的的问题导致读取的会显示比较大,而且比较耗时间。网上有人做过测试:同样是读取摄像头拍摄的同样景色的照片,UIImagePNGRepresentation() 返回的数据量大小为199K,而 UIImageJPEGRepresentation(UIImage* image, 1.0) 返回的数据量大小只为 140KB,比前者少了50多KB。如果对图片的清晰度要求不高,还可以通过设置 UIImageJPEGRepresentation 函数的第二个参数,大幅度降低图片数据量。
如果还有什么问题可以继续百度,这里不进行过多赘述。

2.首先第一个参数是我们都知道的图片image,但是第二个参数scale,一个0~1的浮点型比率,你以为0就是没有,压缩到0b大小,1.0就是原图大小?答案是?:错,首先你的图片的大小是根据(图片的宽*图片的高*每一个色彩的深度,这个和手机的系统有关,一般是4)。你的图片只会按照你的手机像素的分辨率[UIScreen mainScreen].scale来读取值。其次,第二个参数苹果官方并没有明确说明这个参数的具体意义。对于大图片来说,即使你的scale选的很小,比如:0.0000000(n个0)001,但是得到的结果还是很大,这里做了一个实验:一个10M左右的图片,处理后大小为2M多。有点像是“压不动”的感觉。当然如果是小图片的话那就是没问题,能满足你的希望的压缩到的大小。

        既然是循环压,那么就要说一下算法,考虑到递归,二分法,后来发现网上也是有的,二分法处理。更快一点压缩图片到指定的大小。先看一段代码:

//二分最大10次,区间范围精度最大可达0.00097657;最大6次,精度可达0.015625
        for (int i = 0; i < 10; ++i) {
            compression = (max + min) / 2;
            imageData = UIImageJPEGRepresentation(image, compression);
            //容错区间范围0.9~1.0
            if (imageData.length < fImageBytes * 0.9) {
                min = compression;
            } else if (imageData.length > fImageBytes) {
                max = compression;
            } else {
                break;
            }
        }

上面就是使用二分法进行处理,比for循环依次递减“高效”很多,而且也合理很多。

 

但是你也会问,压缩“压不动”怎么办?

这样压缩到“极致”(一般我们不用进行太多的for循环,个人觉得参数到0.05已经可以了如果还是比你想要的大很多那就不要用UIImageJPEGRepresentation了),劳民伤财,劳的是cpu的高速运转,伤的是手机老化加快。哈哈,皮一下!

然后我们其实可以换一个方式,进行尺寸压缩:

提到尺寸压缩,你会不会很失望,看你的文章,原来也是使用UIGraphicsBeginImageContextWithOptions然后drawInRect绘制一个图片,大小。代码类似如下:

/* 根据 dWidth dHeight 返回一个新的image**/
- (UIImage *)drawWithWithImage:(UIImage *)imageCope Size:(CGSize)size {
//这里设置为0,意为自动设置清晰度,图片可以是别的传过来的图片信息
    UIGraphicsBeginImageContextWithOptions(size, NO,0);
    [imageCope drawInRect:CGRectMake(0, 0, size.width, size.height)];
    imageCope = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return imageCope;
}

        首先我需要说一下这个绘制很耗内存性能的,[UIImage drawInRect:]在绘制时,先解码图片,再生成原始分辨率大小的bitmap,这是很耗内存的,并且还有位数对齐等耗时操作。如果在一个方法中循环压缩比例进行代码的比例压缩,那么这种使用UIKit类进行图片绘制的话是需要先把图片读入内存然后在进行绘制,那么势必会给内存中占用大量的临时内存bitmap,而这个如果再加上循环,那么内存占有将是不可估量的。

你可能会说,我加一个自动释放池@autoreleasepool,不就好了?

        错:首先这个自动释放池@autoreleasepool不要放在循环的外面,包着这个循环,原因就不过多说明,可以自行百度。然后放在for循环内部包着这个绘制的方法,你的内存并不是画完就得到了释放,内存占有的情况可以得到缓解,但是还是不能解决内存突然暴增的问题。尤其是大图片的压缩尤其明显。

         然后你会想换一个方式,这里我也亲测试了,使用Image I/O相关的处理方式,使用相关的生成缩略图的形式压缩图片文件。直接上代码如下:

记得导入相关的头文件
#import 
#import 


static size_t getAssetBytesCallback(void *info, void *buffer, off_t position, size_t count{
    ALAssetRepresentation *rep = (__bridge id)info;

    NSError *error = nil;

    size_t countRead = [rep getBytes:(uint8_t *)buffer fromOffset:position length:count error:&error];

    if (countRead == 0 && error) {
        // We have no way of passing this info back to the caller, so we log it, at least.

        NSLog(@"thumbnailForAsset:maxPixelSize: got an error reading an asset: %@", error);
    }
    return countRead;
}

static void releaseAssetCallback(void *info) {

    // The info here is an ALAssetRepresentation which we CFRetain in thumbnailForAsset:maxPixelSize:.

    // This release balances that retain.
    CFRelease(info);
}


- (UIImage *)thumbnailForAsset:(ALAsset *)asset maxPixelSize:(NSUInteger)size {
    NSParameterAssert(asset != nil);
    NSParameterAssert(size > 0);

    ALAssetRepresentation *rep = [asset defaultRepresentation];
    CGDataProviderDirectCallbacks callbacks = {
        .version = 0,
        .getBytePointer = NULL,
        .releaseBytePointer = NULL,
        .getBytesAtPosition = getAssetBytesCallback,
        .releaseInfo = releaseAssetCallback,
    };
    CGDataProviderRef provider = CGDataProviderCreateDirect((void *)CFBridgingRetain(rep), [rep size], &callbacks);
    CGImageSourceRef source = CGImageSourceCreateWithDataProvider(provider, NULL);
    
    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(source, 0, (__bridge CFDictionaryRef) @{
                                                                                                      (NSString *)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
                                                                                                      (NSString *)kCGImageSourceThumbnailMaxPixelSize : @(size),
                                                                                                      (NSString *)kCGImageSourceCreateThumbnailWithTransform : @YES,
                                                                                                      });
    CFRelease(source);
    CFRelease(provider);
    
    if (!imageRef) {
        return nil;
    }
    
    UIImage *toReturn = [UIImage imageWithCGImage:imageRef];
    
    CFRelease(imageRef);
    
    return toReturn;
}

这个是网上的,说的不清楚,(某两个人以及阿里云文档)只管代码补上,很反感。而且还有人搞了两个voidvoid,都是什么,,,,,自己搞了一下,这个东西,可以结合从数据库获取的info返回使用。这里代码如下:

#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
    NSLog(@"info:\n%@", info);
    UIImage *image = info[UIImagePickerControllerOriginalImage];
    NSData *imgData = UIImageJPEGRepresentation(image, 1.0);
    NSLog(@"length1: %lu", (unsigned long)imgData.length);
    
    NSURL *imageURL = info[UIImagePickerControllerReferenceURL];
    ALAssetsLibrary *assetsLibrary = [[ALAssetsLibrary alloc] init];
    [assetsLibrary assetForURL:imageURL resultBlock:^(ALAsset *asset) {
        image = [self thumbnailForAsset:asset maxPixelSize:600];
        imgData = UIImageJPEGRepresentation(image, 1.0);
        NSLog(@"length2: %lu", (unsigned long)imgData.length);
        NSArray * paths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *filePath = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"new/ceshi.jpg"];// 保存文件的名称
        
        BOOL result = [imgData writeToFile: filePath atomically:YES]; // 保存成功会返回YES
        
        NSLog(@"文件保存成功?%d",result);
    } failureBlock:nil];
    
    [picker dismissViewControllerAnimated:YES completion:^{
    }];
}

如果要是问我怎么打开相册?这里也配给你们:

 //初始化UIImagePickerController类
    UIImagePickerController * picker = [[UIImagePickerController alloc] init];
    //判断数据来源为相册
    picker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
    //设置代理
    picker.delegate = self;
    //打开相册
    [self presentViewController:picker animated:YES completion:nil];

记得声明一下代理。

使用ImageIO接口,避免在改变图片大小的过程中产生临时的bitmap,就能够在很大程度上减少内存的占有从而避免由此导致的app闪退问题。

在这之前我也直接有过另外一种方式压缩图片(直接靠图片尺寸压缩绘制图片):

+ (void)compressedImageFiles:(UIImage *)image imageKB:(CGFloat)fImageKBytes imageBlock:(ReturnCompressImage)block {
    
    __block UIImage *imageCope = image;
    CGFloat fImageBytes = fImageKBytes * 1024;//需要压缩的字节Byte
    __block NSData *uploadImageData = nil;
    
//        uploadImageData = UIImagePNGRepresentation(imageCope);
    uploadImageData = UIImageJPEGRepresentation(imageCope, 1.0);
//    NSLog(@"图片压前缩成 %fKB",uploadImageData.length/1024.0);
//    CGFloat value1 = uploadImageData.length/1024.0;

    CGSize size = imageCope.size;
    CGFloat imageWidth = size.width;
    CGFloat imageHeight = size.height;
    
    if (uploadImageData.length > fImageBytes && fImageBytes >0) {
        
        dispatch_async(dispatch_queue_create("CompressedImage", DISPATCH_QUEUE_SERIAL), ^{
            
            /* 宽高的比例 **/
            CGFloat ratioOfWH = imageWidth/imageHeight;
            /* 压缩率 **/
            CGFloat compressionRatio = fImageBytes/uploadImageData.length;
            /* 宽度或者高度的压缩率 **/
            CGFloat widthOrHeightCompressionRatio = sqrt(compressionRatio);
            
            CGFloat dWidth   = imageWidth *widthOrHeightCompressionRatio;
            CGFloat dHeight  = imageHeight*widthOrHeightCompressionRatio;
            if (ratioOfWH >0) { /* 宽 > 高,说明宽度的压缩相对来说更大些 **/
                dHeight = dWidth/ratioOfWH;
            }else {
                dWidth  = dHeight*ratioOfWH;
            }
            
            imageCope = [self drawWithWithImage:imageCope width:dWidth height:dHeight];
            //            uploadImageData = UIImagePNGRepresentation(imageCope);
            uploadImageData = UIImageJPEGRepresentation(imageCope, 1.0);
            
//            NSLog(@"当前的图片已经压缩成 %fKB",uploadImageData.length/1024.0);
            //微调
            NSInteger compressCount = 0;
            /* 控制在 1M 以内**/
            while (fabs(uploadImageData.length - fImageBytes) > 1024) {
                /* 再次压缩的比例**/
                CGFloat nextCompressionRatio = 0.9;
                
                if (uploadImageData.length > fImageBytes) {
                    dWidth = dWidth*nextCompressionRatio;
                    dHeight= dHeight*nextCompressionRatio;
                }else {
                    dWidth = dWidth/nextCompressionRatio;
                    dHeight= dHeight/nextCompressionRatio;
                }
                
                imageCope = [self drawWithWithImage:imageCope width:dWidth height:dHeight];
                //                uploadImageData = UIImagePNGRepresentation(imageCope);
                uploadImageData = UIImageJPEGRepresentation(imageCope, 1.0);
                
                /*防止进入死循环**/
                compressCount ++;
                if (compressCount == 10) {
                    break;
                }
            }
            
//            NSLog(@"图片已经压缩成 %fKB",uploadImageData.length/1024.0);
//            CGFloat value2 = uploadImageData.length/1024.0;

            imageCope = [[UIImage alloc] initWithData:uploadImageData];
            
            dispatch_sync(dispatch_get_main_queue(), ^{
                if (block) {
                    block(imageCope);
                }
            });
        });
    } else{
        if (block) {
            block(imageCope);
        }
    }
}

/* 根据 dWidth dHeight 返回一个新的image**/
+ (UIImage *)drawWithWithImage:(UIImage *)imageCope width:(CGFloat)dWidth height:(CGFloat)dHeight{
    
    UIGraphicsBeginImageContext(CGSizeMake(dWidth, dHeight));
    [imageCope drawInRect:CGRectMake(0, 0, dWidth, dHeight)];
    imageCope = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return imageCope;
    
}

这种方式极耗手机的cpu,而且绘制也是使用uikit进行绘制,内存占用也是比较严重的。

总结:

综合上面的所有情况现在我的最终处理方案如下:

1.首先使用UIImageJPEGRepresentation进行尽可能的压缩,这里我使用二分法(考虑到手机性能问题,这里二分法设置10次(能精确到0.00097657)以内即可)处理压缩的比率参数,

2.首先根据我设置的二分法的最小可能压缩一下原图片信息,比对一下最小的二分法能处理的最大限度得到的最小图片信息能否满足条件(在你设定的目标大小以内)。以减少不必要的循环,保护cpu处理。

3.然后对处理后的图片信息,保留最大压缩比(即上面的最小二分法的scale结果),然后再进行和最终目标的大小比值,求根,然后对图像的宽和高等比压缩处理。然后再次根据最小二分法的scale以UIImageJPEGRepresentation读取结果再和你的目标大小比对,然后以此循环。直到大小小于目标大小。

这样得到的图片几乎就能够在你设定的大小以内的附近,而且图片的信息肉眼几乎看不出来多大的区别。亲自试了3M,4M,6M,10M的大图片没有发现内存消耗有太大的波动。而且压缩出来的图片清晰度很高。

这里上代码如下:

- (void)compressedImageFiles:(UIImage *)image
                         imageKB:(CGFloat)fImageKBytes imageBlock:(void(^)(NSData *imageData))block{
    //二分法压缩图片
    CGFloat compression = 1;
    NSData *imageData = UIImageJPEGRepresentation(image, compression);
    NSUInteger fImageBytes = fImageKBytes * 1000;//需要压缩的字节Byte,iOS系统内部的进制1000
    if (imageData.length <= fImageBytes){
        block(imageData);
        return;
    }
    CGFloat max = 1;
    CGFloat min = 0;
    //指数二分处理,s首先计算最小值
    compression = pow(2, -6);
    imageData = UIImageJPEGRepresentation(image, compression);
    if (imageData.length < fImageBytes) {
        //二分最大10次,区间范围精度最大可达0.00097657;最大6次,精度可达0.015625
        for (int i = 0; i < 6; ++i) {
            compression = (max + min) / 2;
            imageData = UIImageJPEGRepresentation(image, compression);
            //容错区间范围0.9~1.0
            if (imageData.length < fImageBytes * 0.9) {
                min = compression;
            } else if (imageData.length > fImageBytes) {
                max = compression;
            } else {
                break;
            }
        }
        
        block(imageData);
        return;
    }

    // 对于图片太大上面的压缩比即使很小压缩出来的图片也是很大,不满足使用。
    //然后再一步绘制压缩处理
    UIImage *resultImage = [UIImage imageWithData:imageData];
    while (imageData.length > fImageBytes) {
        @autoreleasepool {
            CGFloat ratio = (CGFloat)fImageBytes / imageData.length;
            //使用NSUInteger不然由于精度问题,某些图片会有白边
            NSLog(@">>>>>>>>>>>>>>>>>%f>>>>>>>>>>>>%f>>>>>>>>>>>%f",resultImage.size.width,sqrtf(ratio),resultImage.size.height);
            CGSize size = CGSizeMake((NSUInteger)(resultImage.size.width * sqrtf(ratio)),
                                     (NSUInteger)(resultImage.size.height * sqrtf(ratio)));
//            resultImage = [self drawWithWithImage:resultImage Size:size];
//            resultImage = [self scaledImageWithData:imageData withSize:size scale:resultImage.scale orientation:UIImageOrientationUp];
            resultImage = [self thumbnailForData:imageData maxPixelSize:MAX(size.width, size.height)];
            imageData = UIImageJPEGRepresentation(resultImage, compression);
        }
    }

//   整理后的图片尽量不要用UIImageJPEGRepresentation方法转换,后面参数1.0并不表示的是原质量转换。
    block(imageData);
    
}
- (UIImage *)thumbnailForData:(NSData *)data maxPixelSize:(NSUInteger)size {
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
    CGImageSourceRef source = CGImageSourceCreateWithDataProvider(provider, NULL);
    
    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(source, 0, (__bridge CFDictionaryRef) @{
                                                                                                      (NSString *)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
                                                                                                      (NSString *)kCGImageSourceThumbnailMaxPixelSize : @(size),
                                                                                                      (NSString *)kCGImageSourceCreateThumbnailWithTransform : @YES,
                                                                                                      });
    CFRelease(source);
    CFRelease(provider);
    
    if (!imageRef) {
        return nil;
    }
    
    UIImage *toReturn = [UIImage imageWithCGImage:imageRef];
    
    CFRelease(imageRef);
    
    return toReturn;
}

demo地址:https://github.com/KirstenDunst/CSXImageCompress

demo里面我是做了批量压缩处理,对于多个大图处理,内存也是没有什么太大的波动的。这里附上demo中的批量压缩的图片存储路径:iOS优秀的图片压缩处理方案_第1张图片

如有问题,欢迎指正!

这里也奉献一些大图(6M,10M)以供测试。:https://pan.baidu.com/s/13eexiBPy_lyJxBLXIddnZw

 

后续补充:

       之前的测试中有发现使用上面demo中的方法有遇到iphone手机内存不足的手机拍的照片没有问题,再进行compressedImageFiles压缩处理的时候,会得到  “糊掉的”  图片,之后经过处理,在图片进行二分法压缩前,进行了一次图片的重绘操作解决了这个问题。以上的demo工具中的另一个方法resetSizeOfImage:能够解决这个问题。

 

扩展:

       其实上面的demo中提到的Quartz2D或者UIKit的类中对图片的压缩,水印,剪切等操作,当看过CoreGraphics之后觉得图片原来也可以这么玩。它是iOS的核心图形库,包含Quartz2D绘图API接口,常用的是point,size,rect等这些图形,都定义在这个框架中,类名以CG开头的都属于CoreGraphics框架,它提供的都是C语言函数接口,是可以在iOS和mac OS 通用的。刚接触,这里的了解并不是很深入,但是是更接近底层的图像处理,操作处理上面也是有着很大的灵活性,也有可能会解答iphone内存不足遇到的压缩图片需要重绘问题。之后有时间我会再次整理一篇CoreGraphics的图片处理文章,敬请期待吧!

你可能感兴趣的:(ios开发)