一、iOS 加载超大尺寸图片 Crash 的调研及解决方案1.1、问题描述
前段时间遇到一个工单,客户反馈,只要进入订单列表界面 1~2 秒,客户端就会 Crash,订单列表界面示意如下:
1.2、问题分析
由于是客户投诉的 Bug,没有 Debug 信息,先猜测各种情况,数组越界/后台传 nil 值/内存泄露/ KVO 赋未定义值等等;然而经过仔细分析模拟逐个排除了上述可能,仍查找不到 Crash 原因,百思不得其解。
排除了代码的问题,只有可能是数据问题了,猜测是异常的图片/数据解析出现的问题,于是抽取用户订单数据分析,发现有 2 张尺寸非常大的 JPEG 图片,尺寸达到了 15000*8000 的像素,瞬间想明白了怎么回事,像素总量达到了一亿两千万,猜测是图片解压缩到内存后占用内存过大,导致系统内存紧张,因此系统杀死了 App 进程。
1.3、问题验证
验证是否因大尺寸图片引起的错误。验证过程如下:
写一个类似上面订单列表的 Demo,点击 Cell 逐个加载大图图片,测试用的手机为 iPhone 7P,图片尺寸为(15000px*15000px),点击加载第二张图片就发生了 Crash,一般情况下,APP 占用系统内存 60% 左右就会被杀死进程。
Tips: 不同手机由于内存和屏幕不一样,内存超限 App 发生 Crash 的条件不一样,其中 iPhone 6P 是最容易 Crash 的,因为它有 5.5 寸的屏幕,却只有 1G 内存,加载 Assets.xcassets 图片时会加载 3x 图片,同一张网络图片,UIImageView 布局一般会按照比例放大,大屏手机图片会放大,解码后占用内存也就更大。
1.4、解决方案
约定大于配置,上传图片也要遵守一定的约定。 基于 SDWebImage/YYImage 等第三方库加载超大图引起的崩溃,可通过修改源码解决,但不建议这样做;修改源码可能会引起其他 Bug,而且大图毕竟是少数,没必要对所有图片都进行判断,个别大图单独处理即可。按照一定约定,通过管理平台限定上传图片尺寸大小,增加 APP 流畅度的同时,还能减少用户流量损失,此为最佳方案。
缩放图片尺寸。 如果是展示整张图片,不需要展示图片细节,受限于屏幕分辨率,太大尺寸的图片是没有意义的;如果需要做类似于图片浏览器,可对图片进行放大缩小操作的需求,大图预览的时候可加载缩略图,展示的时候切片处理。
1.5、iOS 图片解码
我们常见的图片格式例如 PNG/JPG/GIF 等格式都属于图像压缩格式,解压为位图后占用的内存会非常大。
假设 iOS 系统从磁盘加载一张图片,首先将文件数据从磁盘读到内存中,此时在内存中仍旧是压缩格式,只有在需要的时机,才会把图片解码为无压缩的位图格式,最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。
将压缩的图片数据解码成未压缩的位图形式,这是一个耗时的 CPU 操作,SDWebimage/YYImage 等第三方框架一般都会提前异步强制解码图片,保证了 UI 界面的流畅性。
1.6、图片解码占用内存计算
图片解码后会占用多少内存呢?其实这个很好计算,苹果手机采用 24 位真彩色显示图像,也就是 24bit(3 字节,RGB 红绿蓝三原色分别占用 8bit,每个颜色有 256 种状态),如果是不包含 Alpha 通道(透明度)的 RGB 图片,那每个像素占用的就是 3 字节,15000px*15000px*3Byte = 644MB,如果是包含透明度的 RGBA 图片,则为 15000px*15000px*4Byte = 858MB,如图2所示,加载一张长和宽 15000px 的图片,内存暴增 858MB。
1.7、图片缩放最优选择
最常用的图片缩放方法是使用 CGContext 的 UIGraphicsGetImageFromCurrentImageContext 方法对图片进行裁剪缩放,能够满足大部分需求。但如果是处理多张大图,这时候就需要优化缩放速度了,可通过 Image I/O 框架对图片进行缩放,在工程中添加 Image I/O Framework,然后在需要使用的地方 #import <ImageIO/ImageIO.h> 即可,示例代码如下:
//maxPixelSize MUST BE a valid value.+ (UIImage *)thumbImageFromLargeFile:(NSString *)filePath withConfirmedMaxPixelSize:(CGFloat)maxPixelSize{ // Create the image source (from path) CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef) [NSURL fileURLWithPath:filePath], NULL); // Create thumbnail options CFDictionaryRef options = (__bridge CFDictionaryRef) @璐村惂鐢ㄦ埛_053SAty馃惥 (id) kCGImageSourceCreateThumbnailWithTransform : @璐村惂鐢ㄦ埛_05NCARy馃惥 (id) kCGImageSourceCreateThumbnailFromImageAlways : @YES, (id) kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize) }; // Generate the thumbnail CGImageRef thumbnail = CGImageSourceCreateThumbnailAtIndex(src, 0, options); CFRelease(src); UIImage *image = [[UIImage alloc] initWithCGImage:thumbnail]; CFRelease(thumbnail); return image;} 复制代码
前段时间遇到一个工单,客户反馈,只要进入订单列表界面 1~2 秒,客户端就会 Crash,订单列表界面示意如下:
1.2、问题分析
由于是客户投诉的 Bug,没有 Debug 信息,先猜测各种情况,数组越界/后台传 nil 值/内存泄露/ KVO 赋未定义值等等;然而经过仔细分析模拟逐个排除了上述可能,仍查找不到 Crash 原因,百思不得其解。
排除了代码的问题,只有可能是数据问题了,猜测是异常的图片/数据解析出现的问题,于是抽取用户订单数据分析,发现有 2 张尺寸非常大的 JPEG 图片,尺寸达到了 15000*8000 的像素,瞬间想明白了怎么回事,像素总量达到了一亿两千万,猜测是图片解压缩到内存后占用内存过大,导致系统内存紧张,因此系统杀死了 App 进程。
1.3、问题验证
验证是否因大尺寸图片引起的错误。验证过程如下:
写一个类似上面订单列表的 Demo,点击 Cell 逐个加载大图图片,测试用的手机为 iPhone 7P,图片尺寸为(15000px*15000px),点击加载第二张图片就发生了 Crash,一般情况下,APP 占用系统内存 60% 左右就会被杀死进程。
Tips: 不同手机由于内存和屏幕不一样,内存超限 App 发生 Crash 的条件不一样,其中 iPhone 6P 是最容易 Crash 的,因为它有 5.5 寸的屏幕,却只有 1G 内存,加载 Assets.xcassets 图片时会加载 3x 图片,同一张网络图片,UIImageView 布局一般会按照比例放大,大屏手机图片会放大,解码后占用内存也就更大。
1.4、解决方案
约定大于配置,上传图片也要遵守一定的约定。 基于 SDWebImage/YYImage 等第三方库加载超大图引起的崩溃,可通过修改源码解决,但不建议这样做;修改源码可能会引起其他 Bug,而且大图毕竟是少数,没必要对所有图片都进行判断,个别大图单独处理即可。按照一定约定,通过管理平台限定上传图片尺寸大小,增加 APP 流畅度的同时,还能减少用户流量损失,此为最佳方案。
缩放图片尺寸。 如果是展示整张图片,不需要展示图片细节,受限于屏幕分辨率,太大尺寸的图片是没有意义的;如果需要做类似于图片浏览器,可对图片进行放大缩小操作的需求,大图预览的时候可加载缩略图,展示的时候切片处理。
1.5、iOS 图片解码
我们常见的图片格式例如 PNG/JPG/GIF 等格式都属于图像压缩格式,解压为位图后占用的内存会非常大。
假设 iOS 系统从磁盘加载一张图片,首先将文件数据从磁盘读到内存中,此时在内存中仍旧是压缩格式,只有在需要的时机,才会把图片解码为无压缩的位图格式,最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。
将压缩的图片数据解码成未压缩的位图形式,这是一个耗时的 CPU 操作,SDWebimage/YYImage 等第三方框架一般都会提前异步强制解码图片,保证了 UI 界面的流畅性。
1.6、图片解码占用内存计算
图片解码后会占用多少内存呢?其实这个很好计算,苹果手机采用 24 位真彩色显示图像,也就是 24bit(3 字节,RGB 红绿蓝三原色分别占用 8bit,每个颜色有 256 种状态),如果是不包含 Alpha 通道(透明度)的 RGB 图片,那每个像素占用的就是 3 字节,15000px*15000px*3Byte = 644MB,如果是包含透明度的 RGBA 图片,则为 15000px*15000px*4Byte = 858MB,如图2所示,加载一张长和宽 15000px 的图片,内存暴增 858MB。
1.7、图片缩放最优选择
最常用的图片缩放方法是使用 CGContext 的 UIGraphicsGetImageFromCurrentImageContext 方法对图片进行裁剪缩放,能够满足大部分需求。但如果是处理多张大图,这时候就需要优化缩放速度了,可通过 Image I/O 框架对图片进行缩放,在工程中添加 Image I/O Framework,然后在需要使用的地方 #import <ImageIO/ImageIO.h> 即可,示例代码如下:
//maxPixelSize MUST BE a valid value.+ (UIImage *)thumbImageFromLargeFile:(NSString *)filePath withConfirmedMaxPixelSize:(CGFloat)maxPixelSize{ // Create the image source (from path) CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef) [NSURL fileURLWithPath:filePath], NULL); // Create thumbnail options CFDictionaryRef options = (__bridge CFDictionaryRef) @璐村惂鐢ㄦ埛_053SAty馃惥 (id) kCGImageSourceCreateThumbnailWithTransform : @璐村惂鐢ㄦ埛_05NCARy馃惥 (id) kCGImageSourceCreateThumbnailFromImageAlways : @YES, (id) kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize) }; // Generate the thumbnail CGImageRef thumbnail = CGImageSourceCreateThumbnailAtIndex(src, 0, options); CFRelease(src); UIImage *image = [[UIImage alloc] initWithCGImage:thumbnail]; CFRelease(thumbnail); return image;} 复制代码