为什么我不建议你直接使用UIImage传值?--从一个诡异的相册九图连读崩溃bug谈起

#if 1// 优化连续读取多张高像素图时,内存是瞬间飙升的问题

        

        [[TZImageManagermanager] getOriginalPhotoDataWithAsset:assetcompletion:^(NSData *data,NSDictionary *info, BOOL isDegraded) {

            

            UIImage * photo = [UIImageimageWithData:data];

            UIImage *newImage = [UIImagescaleImage:photo toSize:ImageScaleStandardSize];

            NSLog(@"%@",info[PHImageResultIsDegradedKey]);

            //            [scaledImageArray addObject:newImage];

            

            // 该block块是在异步执行的,故等图片都获取完成之后再做其他操作

            //            if (scaledImageArray.count == photos.count) {

            

            [HFUtilshidePrograssAll];

            [selfsendImages:@[newImage]orignal:YES];

            NSLog(@"photos.count%lu",(unsignedlong)photos.count);

            

            //            }

            

        }];

        

#else

    

        [[TZImageManager manager] getOriginalPhotoWithAsset:asset completion:^(UIImage *photo, NSDictionary *info) {

            UIImage *newImage = [UIImage scaleImage:photo toSize:ImageScaleStandardSize];

            NSLog(@"%@",info[PHImageResultIsDegradedKey]);

//            [scaledImageArray addObject:newImage];

            

            // 该block块是在异步执行的,故等图片都获取完成之后再做其他操作

//            if (scaledImageArray.count == photos.count) {

            

                [HFUtils hidePrograssAll];

                [self sendImages:@[newImage] orignal:YES];

                NSLog(@"photos.count%lu",(unsignedlong)photos.count);

                

//            }

        }];

        

#endif


分析如下:

Bug仅在操作多张高像素图片时才会触发,所谓高像素就是图片本身并不算大,但是图片宽高非常大的图片.这次触发这个问题的是一组 5701 * 3171 的图片.画风大家可以点击链接查看原图自行感受下 --https://github.com/ios122/why_not_uiimage/blob/master/bug_img.jpg?raw=true

BOSS刚好是一个摄影爱好者

在大多数情况下,是很少有用户触发这个问题的,但是BOSS是一个摄影爱好者,手机里有许多高像素图,一天他想往自己公司的App上传分享几张图片时,他竟然没法把一次性地从相册选取九张图,每次选中后,点击"确定",都会理解Crash.是的,就是那九张图,其他图片是没问题的,8张图,也是OK,他还强调了下是用的最新版本的App.

关于 BUG 的预处理

首先,我的第一反应是肯定是他的手机太烫了吧,重启下,就好了.,肯定是这样.发布作品的逻辑,好几个版本都没动过.模拟器,手机,我自己试了下,都是OK.也没有其他用户反馈过,fabric也看不到任何log.,手机太烫了.我稍后,再联系他,肯定就OK.

稍后,再直接联系BOSS,竟然还是会Crash,他甚至给我录屏演示了一下,真的每次都会crash.而且我还无法复现.而且BOSS手机iPhone6 plus,自身内存不足的原因非常非常小.

形势,瞬间变得很紧张,这个问题的优先级瞬间被提到了最高!再次尝试了各种可能的情况.图片大小?它是91.5M的图,我就用93M的图,也是OK的呀!选取时,顺序有问题?我试着按照录屏中演示的顺序去选取图片,也是OK.一股深深地无力感!竟然连复现都无法复现不了!

最后的最后,说是会拿手机给我测试.不过,最后BOSS的手机,还是没有拿到,只是拿到了开篇那张画风诡异的图片.没错,就是它,连续选取9,Crash.

至少,我现在能复现问题了.下面的,需要的就只是时间,耐心还有大开的脑洞了.

Bug 分析思路的简要描述

我不觉得,分析Bug真的有什么思路可言.Bug产生的原因,是有许多可能性的,可能行验证的顺序,方式和深度很大程度上取决于coder本身已有的经验,天赋,甚至还有些许的运气!我能描述的,可能仅仅是我处理这个问题的一个相对的完整脑洞过程.部分分析过程间,明显不是有逻辑性的.越是诡异的问题,越是不能循规蹈矩,要时刻尝试去问自己最可能地问题是什么,而不是沿着一条路,一条道走到黑.

1.排除通用逻辑问题

Coder有些许高傲,有时候是有利于自己更冷静地处理问题的.稍微不自信点的童鞋,可能就会怀疑:我代码是不是有什么特殊的临界判断没有加?不行,我得去看看.一行一行,看代码,从天黑到天亮,从期待到绝望...其实,稍微有一些对比实验常识的人,都很容易猜到:两种情况,唯一的变量是图片素材本身,最可能 的原因肯定是图片本身的问题.一种高大上的说法,这某种程度上,也暗合了所谓的"贪心算法".每次,都只从最可能的原因入手,管他谁是谁,我的代码就算有问题,那触发这个问题的可能性,也是远小于图片素材本身的.---多么朴素的真理呀!

2.确定是相册选取图片内存过高

这个问题,在真机上,并不好确定,因为连续读取9张高像素图时,内存是瞬间飙升的,你几乎没有机会去观察内存占用,给人一种因为某种逻辑判断而导致的Crash的错觉.如果换做模拟器,会很容易看到,这个内存占用,是飙升到G单位的.当然,我也没那么睿智,我是单个N个断点,最终确认了Crash的代码的准确位置.一个for循环,每次step 1,这下很明显地看到内存,几乎是 100M/张的速度在飙升,而图片本身的大小只有 1.5M/.此处我想说的是,打断点也是有技巧的,最后没有办法的办法也是讲究办法的.可是试着注释掉可能的引起的代码,然后逐步放开注释,这要观察,会比直接打断点快些.--意会!

3.确定是PHImageManager的问题requestImageForAsset:方法引起的高内存占用

当你通过注释法,配合断点,很容易就可以引起内存高占用的代码.此处,我的App,是读取相册原图,用的是PHImageManager requestImageForAsset:targetSize:contentMode:options: resultHandler:方法.此处接下来的解决思路,有大坑呀!你可能会想,UIImage加载的问题吧?那就研究下UIImage渲染机制吧.然后1天过去了,等你学成归来,蓦然发现PHImageManager 是一个系统方法,它加载的图片机制,你无力干涉!我可能运气比较好些吧,研究UIImage的渲染机制,想想都头疼,抱着试一试的态度,google了下:PHImageManager requestImageForAsset memory high,然后第一条链接的第二个回答就是我要到答案:http://stackoverflow.com/questions/33274791/high-memory-usage-looping-through-phassets-and-calling-requestimageforasset

是的,我运气,似乎总是很好~

4.使用requestImageDataForAsset:替换的问题requestImageForAsset:

答案原文是:

I found that if iswitch from


- requestImageForAsset:targetSize:contentMode:options:resultHandler:


to


- requestImageDataForAsset:options:resultHandler:


i will receive the image with the same dimension {5376, 2688} but the sizein byte is much smaller. So the memory issue is solved.


hope this help !!


(note : [UIImageimageWithData:imageData] usethis to convert NSData to UIImage)

简单说,就是用 - requestImageDataForAsset:options:resultHandler:替换 requestImageForAsset:targetSize:contentMode:options:resultHandler:就可以了,前者是直接返回二进制数据,不渲染.

但是,这里有一个可能不是问题的问题,这个方法调用是位于一个名为第三方库 TZImagePickerController,我方便直接改吗?我是直接给改了.此处,将来必成大患,以后再用到,肯定还会有相同问题,还不如直接把原来的实现直接替换掉.当然,这也是成本最小的方法.这个库,本身,已经在App,深度定制和重写了,如果一些成熟的第三方库,这么做,最好先备份或备注下.

5.使用imageWithData:兼容原来的调用

为了和原来的Api接口调用兼容,imageWithData:NSData转换为 UIImage传出,同时扩展方法,使支持同时传出 UIImage和原始的 NSData对象.传出NSData对象的原因是,是因为高像素图片,会引起一些列的问题,故事到此远远没有结束,详见衍生问题部分.

6.变更前后的代码对比

还是来段代码感受下吧,一码剩千言:

/*原来的代码*/

[[PHImageManager defaultManager] requestImageForAsset:assettargetSize:PHImageManagerMaximumSizecontentMode:PHImageContentModeAspectFitoptions:option resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {

            BOOL downloadFinined = (![[info objectForKey:PHImageCancelledKey] boolValue] && ![infoobjectForKey:PHImageErrorKey]);

            if (downloadFinined && result) {

                result = [self fixOrientation:result];

                if (completion) completion(result,info);

            }

        }];

/*优化后代码*/

[[PHImageManager defaultManager] requestImageDataForAsset:asset options:option resultHandler:^(NSData * _Nullable imageData,NSString * _Nullable dataUTI,UIImageOrientation orientation,NSDictionary * _Nullable info) {

            UIImage * result = [UIImage imageWithData:imageData];


            BOOL downloadFinined = (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey]);

            if (downloadFinined && result) {

                result = [self fixOrientation:result];

                if (completion) completion(result,info,imageData);

            }

        }];

此类Bug的可能的通用解决思路

首先,我要说明下,我解决的思路和方式,很大程度上依赖也受限于我已有的经验,此处的解法,可能不是最优解,最多只能算是个通用解.说不定,将来等我再研究下渲染机制一类的技术,会有一个新的更简单的方法.欢迎大神补充!

未来遇到UIImage内存问题的童鞋,至少能从此处获取的一个至少验证可用的解决策略.

回到问题本身,用一句概括就是:永远不要直接传递UIImage对象.在需要传递UIImage的场景中,请使用图片名或者NSData二进制对代替.

衍生问题应用与解决

故事,真的还没有完结.从相册顺利读取这张诡异的高像素图后,我发现我没有办法将它上传,也无法在轮播图上,连续显示.简要概括如下.

无法直接以UIImage格式,连续把九张图保存到缓存目录

图片选取后,并不是立即上传的,为了能实现"重发"功能,需要在缓存目录保留副本.原来是将 UIImage 转换为 NSData写入.在此过程中,又一次引起了巨额的内存开销.解决方法,就是直接缓存原始获取的 NSData 的对象,而不要 NSData --> UIImage --> NSData.

无法直接以UIImage格式,连续在轮播图上显示九张图

此处对应的是一个本地大图预览功能,实现是在前一个页面把九张本地图的UIImage传递给轮播预览组件.此处的坑是:把一个存放在 数组中的UIImage对象传递给 UIImageView image属性,UIImageView加载到父视图时,会引起巨额的内存占用.原因初步猜测是 UIImage 对象显示到 UIImageView 会有一个特殊的耗费内存的操作,如果原始的 UIImage对象一直存在,这一块内存那就无法释放.这一步,困扰了我很久很久,好几个小时!我真没想到,一个UIImage对象,竟然会二次引起高内存占用.最终的解决方法,就是在前一个页面传递 NSData数组,在赋值处,再使用imageWithData:转换为 UIImage.这样,内存使用基本没什么起伏.

或许,我应该研究下一个UIImage对象,竟然会二次引起高内存占用的原因.欢迎大神完善!

参考链接

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值