美文网首页
图片压缩上传从32s提速到2s

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

作者: 三月木头 | 来源:发表于2020-09-14 15:33 被阅读0次

    引导语:

    • 偶然在buggly看到一个KYC上传失败图片,但没接到其它人员反馈。自己试了下产品KYC图片上传等了30多秒后提示上传失败。问了问QA说确实上传不太好用,但是多试几次,用小点图片还是能上传的.......
    • 亲自动手查查问题。

    问题展示:

    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;
    }
    

    相关文章

      网友评论

          本文标题:图片压缩上传从32s提速到2s

          本文链接:https://www.haomeiwen.com/subject/xyubektx.html