图片压缩上传从32s提速到2s

2020-09-14  本文已影响0人  三月木头

引导语:

问题展示:

  1. 首先线程存在问题,对图片处理非常耗时,但是居然是在主线程处理这些图片的。导致界面在上传期间卡死状态。
  2. 图片处理耗时,刚开始怀疑是加水印的问题,后来打印下处理时间,发现处理水印并没有消耗多久时间。再往下分析发现,压缩图片是消耗大量CPU并且耗时的主因,压缩一张图片掉了84次图片处理,导致耗损大量时间。
  3. 由于线上代码处理图片时候占用的主线程,所以App卡死状态不能点击多个大图,之前没有发现这个问题。我开子线程处理图片后发现点击几个大图片后,App居然会Crash甚至黑屏关机,查看了下内存300M->500M->800M-->1.3G,内存大的让人有些吃惊,到后面手机都会自动关机。。。。

解题思路:

  1. 首先搞两个线程,把耗时的图片加水、压缩图片放到子线程。
  2. 由于这两个子线程有先后执行顺序,所以可以通过GCD的group对其进行管理

实际操作一遍,觉得很爽,速度提上来不少,线程也不会再卡死。压缩图片速度提升到了3、4S左右吧,打包给QA团队去测试了。
然鹅第二天,就有人反馈,这个相当于以前虽然提速了不少,但是有时候不会显示图片。
代码走起来,发现压缩图片的算法有问题,由于图片数据比较大导致如果不降低分辨率,哪怕缩小到最小的1/250的数据时候,都比我目标压缩数据大很多。代码流程是如果这种情况下会执行降低分辨率后然后再一次压缩,问题是分辨率无法降低下去导致了比较长时间的循环而耗费了时间。了解清楚问题根源咱们就继续优化。

下面通过实战调用代码讲解一下

  1. 首先我们看代码看到最外层多个if else的代码,提取一些公共层,将if else 内移。
  2. 由于上传图片时候,主程卡死,所以问题出现在主程上面,咱们开始一点点找。
  3. 最开始怀疑addWatermarketWithOriginImage:image (后面会放出所有内部方法实现)这个方法耗时,也就是增加水印后重新绘图。看完这个方法内部代码后觉得没啥问题,打印了一下时间证实确实也没有耗时,所以放过这个方法。
  4. 开始往下研究,研究研究压缩图片的方法compressOriginalImage: toMaxDataSizeKBytes:先打印一下发现,执行这个方法前后耗时用了32s...主程卡死。所以咱们能确认问题代码了。而且耗时太久。那咱们接下来要做的就是从线程和算法层面考虑优化一下。
  5. 线程方面优化: 线程的话,由于我们需要拿到这个压缩后图的数据,然后才能上传服务器,这两步是有线程依赖关系的,也就是压缩数据的A线程先执行完,然后才能执行上传图片的B线程,等B线程上传完数据后,我们再回主线程处理下UI展示即可。关于线程代码我使用的GCD的group管理线程(当然你也可以使用栅栏,不另说了)下面会贴出优化后相关代码。
  6. 算法方面优化:先说一下原压缩方法中的问题,只对图片进行质量压缩,每次压缩0.01,最多可以压缩90次。也就是我们说的最终我们可以将原图压缩到0.01的质量,问题是即使压缩到0.01状态,图片数据还是很大,而且循环这么多次占用很长时间。了解了算法的问题,我们接下来就需要优化,首先想到的是如何避免这八九十次的循环问题,所以想到了二叉树查找来避免循环遍历的问题。其次还有一个问题就是,如果使用原图质量时候,即使降低到0.01图片质量时候,压缩出来数据还是跟目标数据200KB有很大出入,所以我们想到的办法是降低图片分辨率,然后再执行一次图片质量压缩。
  7. 第一次优化算法解析:首先进行分辨率调整(原第一次有问题,是由于tempWidth == tempHeight时没有进行分辨率调整),然后进行质量调整。将图片质量分为1/250最小单位,然后通过二叉树进行压缩。如果压缩到最小单位后,压缩数据还没达到目前200KB大小,那就进行分辨率调整知道满足为止。
  8. 第二次压缩图片优化解析:由于第一次分辨率算法中tempWidth == tempHeight 没有进行调整,导致大图片数据最多压缩到原图1/250最小单位为止,所以需要对分辨率算法进一步调整。最简单方法是将判断符>跟 < 改为 <= 和 >= 但是总觉得怪怪的,所以写了另外一套降低分辨率算法的问题,首先将分辨率等比例缩放至以款为基准的1024Kb的大小,然后再进行质量压缩。至此压缩完美降低到2s
  9. 差点忘记关于讲解对于关于内存的优化。内存从几十几百MB能飙升到1个多G。主要原因就是button设置背景图片的时候,加载的未压缩图片,未压缩图片几Mb十几Mb甚至几十Mb,优化后面直接加载自己已经压缩的图片,显示几百Kb,使用完成后即时置空nil即可。至此有关KYC上传图片优化结束。

1.原32s时候的原业务代码:

if (picker.view.tag == 100) {
        [picker dismissViewControllerAnimated:YES completion:^{
           /** 正面*/
            UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
            [self.certificatesOne.leftButton setImage:image forState:UIControlStateNormal];
            
            /**  提交图片 获取地址 */
             [BCHud showLoading];
            image = [BCManagementTool addWatermarketWithOriginImage:image WaterText:@"BitCoke"];
            NSData *imageData = [NSData dataWithData:UIImageJPEGRepresentation([BCManagementTool compressOriginalImage:image toMaxDataSizeKBytes:200], 0.1)];
            BCURLModel * urlModel = [BCURLManager bc_fileUpload:Nil_String isPublic:NO];
            [BCRequestManager multiPartRequest:urlModel.url parameters:urlModel.param  fileName:imageData successRequest:^(BCRequestModel *requestModel) {
                BCLog(@"--%@",requestModel.data);
                [BCHud dismissLoading];
                self.model.url1 = requestModel.data;
                [self changeState];
            }
            dataError:^(BCRequestModel *requestModel) {
                 [BCHud dismissLoading];
                [BCProgressHUD showMessage:BC_ChangString([requestModel.code stringValue])];
            } failRequest:^(NSError *error) {
                 [BCHud dismissLoading];
                [BCProgressHUD showMessage:BC_ChangString(@"500")];
            }];       
        }];
    }else if (picker.view.tag == 200){}

目前业务逻辑代码:

[picker dismissViewControllerAnimated:YES completion:^{
            [BCHud showLoading];
           __block UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
           __block UIImage *imageWater = nil;
           __block NSData *imageWaterData = nil;
           __block UIImage *compressionImageWater = nil;
          
           dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
           dispatch_group_t group = dispatch_group_create();
           dispatch_group_async(group, queue, ^{
               imageWater = [BCManagementTool addWatermarketWithOriginImage:image WaterText:@"BitCoke"];
               imageWaterData = [BCManagementTool resetSizeOfImageData:imageWater maxSize:200];
               compressionImageWater = [UIImage imageWithData:imageWaterData];
             //BCLog(@"Thread A %@", [NSThread currentThread]);
           });
           
           //等待A执行完之后执行B
           dispatch_group_notify(group, queue, ^{
              // BCLog(@"Thread B %@", [NSThread currentThread]);
               BCURLModel * urlModel = [BCURLManager bc_fileUpload:Nil_String isPublic:NO];
               [BCRequestManager multiPartRequest:urlModel.url parameters:urlModel.param  fileName:imageWaterData successRequest:^(BCRequestModel *requestModel) {
                   //BCLog(@"--%@",requestModel.data);
                   if (picker.view.tag == 100) {
                       [self.certificatesOne.leftButton setImage:compressionImageWater forState:UIControlStateNormal];
                       self.model.url1 = requestModel.data;
                   } else if (picker.view.tag == 200) {
                       [self.certificatesTwo.leftButton setImage:compressionImageWater forState:UIControlStateNormal];
                       self.model.url3 = requestModel.data;
                   } else if (picker.view.tag == 300) {
                       [self.certificatesThree.leftButton setImage:compressionImageWater forState:UIControlStateNormal];
                       self.model.url2 = requestModel.data;
                   }
                   [BCHud dismissLoading];
                   [self changeState];
                   image = nil;
                   imageWater  = nil;
                   imageWaterData = nil;
                   compressionImageWater = nil;

               }
               dataError:^(BCRequestModel *requestModel) {
                   [BCHud dismissLoading];
                   [BCProgressHUD showMessage:BC_ChangString([requestModel.code stringValue])];
               } failRequest:^(NSError *error) {
                   [BCHud dismissLoading];
                   [BCProgressHUD showMessage:BC_ChangString(@"500")];
               }];
           });

           dispatch_async(queue, ^{
              // BCLog(@"Wait Thread Test In %@", [NSThread currentThread]);
               dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
           });
    }];

原32s压缩图片算法:

+ (UIImage *)compressOriginalImage:(UIImage *)image toMaxDataSizeKBytes:(CGFloat)size{
    NSData * data = UIImageJPEGRepresentation(image, 1.0);
    CGFloat dataKBytes = data.length/1000.0;
    CGFloat maxQuality = 0.9f;
    CGFloat lastData = dataKBytes;
    while (dataKBytes > size && maxQuality > 0.01f) {
        maxQuality = maxQuality - 0.01f;
        data = UIImageJPEGRepresentation(image, maxQuality);
        dataKBytes = data.length / 1000.0;
        if (lastData == dataKBytes) {
            break;
        }else{
            lastData = dataKBytes;
        }
    }
      UIImage *compressedImage = [UIImage imageWithData:data];
    
         BCLog(@"当前大小:%fkb",(float)[data length]/1024.0f);
    return compressedImage;
}

第一次优化后图片压缩算法(注意分辨率压缩有bug)

#pragma mark - 图片压缩
+ (NSData *)resetSizeOfImageData:(UIImage *)sourceImage maxSize:(NSInteger)maxSizeKB {
    //先判断当前质量是否满足要求,不满足再进行压缩
    __block NSData *finallImageData = UIImageJPEGRepresentation(sourceImage,1.0);
    NSUInteger sizeOrigin   = finallImageData.length;
    NSUInteger sizeOriginKB = sizeOrigin / 1000;
    
    if (sizeOriginKB <= maxSizeKB) {
        return finallImageData;
    }
    
    //获取原图片宽高比
    CGFloat sourceImageAspectRatio = sourceImage.size.width/sourceImage.size.height;
    //先调整分辨率
    CGSize defaultSize = CGSizeMake(1024, 1024/sourceImageAspectRatio);
    UIImage *newImage = [self newSizeImage:defaultSize image:sourceImage];
    
    finallImageData = UIImageJPEGRepresentation(newImage,1.0);
    
    //保存压缩系数
    NSMutableArray *compressionQualityArr = [NSMutableArray array];
    CGFloat avg   = 1.0/250;
    CGFloat value = avg;
    for (int i = 250; i >= 1; i--) {
        value = i*avg;
        [compressionQualityArr addObject:@(value)];
    }
    
    /*
     调整大小
     说明:压缩系数数组compressionQualityArr是从大到小存储。
     */
    //思路:使用二分法搜索
    finallImageData = [self halfFuntion:compressionQualityArr image:newImage sourceData:finallImageData maxSize:maxSizeKB];
    //如果还是未能压缩到指定大小,则进行降分辨率
    while (finallImageData.length == 0) {
        //每次降100分辨率
        CGFloat reduceWidth = 100.0;
        CGFloat reduceHeight = 100.0/sourceImageAspectRatio;
        if (defaultSize.width-reduceWidth <= 0 || defaultSize.height-reduceHeight <= 0) {
            break;
        }
        defaultSize = CGSizeMake(defaultSize.width-reduceWidth, defaultSize.height-reduceHeight);
        UIImage *image = [self newSizeImage:defaultSize
                                      image:[UIImage imageWithData:UIImageJPEGRepresentation(newImage,[[compressionQualityArr lastObject] floatValue])]];
        finallImageData = [self halfFuntion:compressionQualityArr image:image sourceData:UIImageJPEGRepresentation(image,1.0) maxSize:maxSizeKB];
    }
    return finallImageData;
}

#pragma mark 调整图片分辨率/尺寸(等比例缩放)
+ (UIImage *)newSizeImage:(CGSize)size image:(UIImage *)sourceImage {
    CGSize newSize = CGSizeMake(sourceImage.size.width, sourceImage.size.height);
    
    CGFloat tempHeight = newSize.height / size.height;
    CGFloat tempWidth = newSize.width / size.width;
    
、、注意此处有bugtempWidth >= tempHeight  跟  tempWidth <= tempHeight 否则无法压缩。下面有对此分辨率算法优化
    if (tempWidth > 1.0 && tempWidth > tempHeight) {
        newSize = CGSizeMake(sourceImage.size.width / tempWidth, sourceImage.size.height / tempWidth);
    } else if (tempHeight > 1.0 && tempWidth < tempHeight) {
        newSize = CGSizeMake(sourceImage.size.width / tempHeight, sourceImage.size.height / tempHeight);
    }
    
    UIGraphicsBeginImageContext(newSize);
    [sourceImage drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}

#pragma mark 二分法
+ (NSData *)halfFuntion:(NSArray *)arr image:(UIImage *)image sourceData:(NSData *)finallImageData maxSize:(NSInteger)maxSize {
    NSData *tempData = [NSData data];
    NSUInteger start = 0;
    NSUInteger end = arr.count - 1;
    NSUInteger index = 0;
    
    NSUInteger difference = NSIntegerMax;
    while(start <= end) {
        index = start + (end - start)/2;
        
        finallImageData = UIImageJPEGRepresentation(image,[arr[index] floatValue]);
        
        NSUInteger sizeOrigin = finallImageData.length;
        NSUInteger sizeOriginKB = sizeOrigin / 1024;
        NSLog(@"当前降到的质量:%ld", (unsigned long)sizeOriginKB);
        NSLog(@"\nstart:%zd\nend:%zd\nindex:%zd\n压缩系数:%lf", start, end, (unsigned long)index, [arr[index] floatValue]);
        
        if (sizeOriginKB > maxSize) {
            start = index + 1;
        } else if (sizeOriginKB < maxSize) {
            if (maxSize-sizeOriginKB < difference) {
                difference = maxSize-sizeOriginKB;
                tempData = finallImageData;
            }
            if (index<=0) {
                break;
            }
            end = index - 1;
        } else {
            break;
        }
    }
    return tempData;
}

+ (UIImage *)newSizeImage:(CGSize)size image:(UIImage *)sourceImage

+ (UIImage *)newSizeImage:(CGSize)targetSize image:(UIImage *)sourceImage {
    CGFloat souceImageW = sourceImage.size.width;
    CGFloat souceImageH = sourceImage.size.height;
    if (souceImageH == 0 || souceImageW == 0) {
        return sourceImage;
    }
    BOOL isBiggerH = souceImageH > souceImageW;
    
    CGFloat targetW = isBiggerH ? MIN(targetSize.width, targetSize.height) : MAX(targetSize.width, targetSize.height);
    CGFloat targetH = isBiggerH ? MAX(targetSize.width, targetSize.height) : MIN(targetSize.width, targetSize.height);
    
    CGFloat coefficientW = targetW * 1.0 / souceImageW;
    CGFloat coefficientH = targetH * 1.0 / souceImageH ;
    CGFloat finalCoefficient = MIN(coefficientW, coefficientH);
    if (finalCoefficient > 1) {
        return sourceImage;   // 不需要缩小
    }else{
        CGPoint thumbnailPoint =CGPointMake(0.0,0.0);//这个是图片剪切的起点位置
        UIGraphicsBeginImageContext(CGSizeMake(MIN(finalCoefficient * souceImageW, targetW), MIN(finalCoefficient * souceImageH, targetH)));//开始剪切
        CGRect thumbnailRect =CGRectZero;//剪切起点(0,0)
        thumbnailRect.origin= thumbnailPoint;
        thumbnailRect.size.width= souceImageW * finalCoefficient;
        thumbnailRect.size.height= souceImageH * finalCoefficient;
        [sourceImage drawInRect:thumbnailRect];
        UIImage*newImage =UIGraphicsGetImageFromCurrentImageContext();//截图拿到图片
        return newImage;
    }
}

第一次优化


291599648805_.pic.jpg

第二次优化


381599650125_.pic_hd.jpg

第三次优化


411599821351_.pic_hd.jpg

图片添加水印算法:

+ (UIImage *)addWatermarketWithOriginImage:(UIImage *)originImage WaterText:(NSString *)waterText{
    //原始image的宽高
    CGFloat viewWidth = originImage.size.width;
    CGFloat viewHeight = originImage.size.height;
    
    UIGraphicsBeginImageContextWithOptions(originImage.size, NO, 0);
    // 绘制图片
    [originImage drawInRect:CGRectMake(0, 0, viewWidth, viewHeight)];
    // 添加水印
    if (waterText.length > 0) {
        CGFloat horizontalSpace = 60;// 水平间隔
        CGFloat vertivalSpace = 60; // 竖直间隔
        NSDictionary *attributedDic =@{NSFontAttributeName:[UIFont boldSystemFontOfSize:30],NSForegroundColorAttributeName:Default_Gray_Color,NSBackgroundColorAttributeName:[UIColor clearColor]};
        NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:waterText attributes:attributedDic];
        //绘制文字的宽高
        CGFloat strWidth = attrStr.size.width;
        CGFloat strHeight = attrStr.size.height;
        // 开始旋转上下文矩阵,绘制水印文字
        CGContextRef context = UIGraphicsGetCurrentContext();
        //将绘制原点(0,0)调整到源image的中心
        CGContextConcatCTM(context, CGAffineTransformMakeTranslation(viewWidth/2, viewHeight/2));
        //以绘制原点为中心旋转  (M_PI_2 / 3 ) <45>角度
        CGContextConcatCTM(context, CGAffineTransformMakeRotation(M_PI_2 / 3));
    //将绘制原点恢复初始值,保证当前context中心和源image的中心处在一个点(当前context已经旋转,所以绘制出的任何layer都是倾斜的)
        CGContextConcatCTM(context, CGAffineTransformMakeTranslation(-viewWidth/2, -viewHeight/2));
        
        // 对角线
        CGFloat sqrtLength = sqrt(viewWidth*viewWidth + viewHeight*viewHeight);
        //计算需要绘制的列数和行数
        int horCount = sqrtLength / (strWidth + horizontalSpace) + 1;
        int verCount = sqrtLength / (strHeight + vertivalSpace) + 1;
        
        //此处计算出需要绘制水印文字的起始点,由于水印区域要大于图片区域所以起点在原有基础上移
        CGFloat orignX = -(sqrtLength-viewWidth)/2;
        CGFloat orignY = -(sqrtLength-viewHeight)/2;
        //在每列绘制时X坐标叠加
        CGFloat tempOrignX = orignX;
        //在每行绘制时Y坐标叠加
        CGFloat tempOrignY = orignY;
        for (int i = 0; i < horCount * verCount; i++) {
            [waterText drawInRect:CGRectMake(tempOrignX, tempOrignY, strWidth, strHeight) withAttributes:attributedDic];
            if (i % horCount == 0 && i != 0) {
                tempOrignX = orignX;
                tempOrignY += (strHeight + vertivalSpace);
            }else{
                tempOrignX += (strWidth + horizontalSpace);
            }
        }
    }
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}
上一篇下一篇

猜你喜欢

热点阅读