iOS 中常见的加载图片方式有两种:一个是用 imageNamed,二是用imageWithContentsOfFile。imageNamed 的优点是当加载时会缓存图片。imageNamed 的文档中这么说:这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。相反的,imageWithContentsOfFile 仅加载图片。
其实 imageNamed 内部实现大致猜测如下:
- (UIImage *)imageNamed:(NSString *)name {
// 从缓存中获取
UIImage *result = [imageCache objectForKey:name];
if ( result == nil ) {
// 根据屏幕缩放比例获取 @1x,@2x,@3x 图片
NSString *path = [NSString stringWithFormat:@"%@@%@x", name, @([UIScreen mainScreen].scale)];
// 获取图片路径
NSString *filePath = [[NSBundle mainBundle] pathForResource:path ofType:@"png"];
// 加载图片,底层可能使用ImageIO
result = [UIImage imageWithContentsOfFile:filePath];
// 添加至缓存队列
[imageCache addObject:result forKey:name];
}
return result;
}
其中的 imageCache 为系统缓存,无法手动清除,只有在 APP 收到
didReceiveMemoryWarning 时候系统才会回收这部分缓存。
因此,imageNamed 常用来加载系统图片资源,其他场景都用 imageWithContentsOfFile,配合自己实现的缓存机制来加载图片资源。常用的第三方库有 SDWebImage,YYImage 等。但使用 SDWebImage 加载网络图片的时候经常会遇到加载不出图片的情况,比如广告轮播图等。通常这类问题都是因为图片太大或分辨率太高,解决方案通常也是换张压缩过的图片。那还有没有其他解决方法呢,答案肯定有。
首先了解一下 iOS 中图片加载机制,系统从磁盘加载一张图片,并使用 UIImageView 来显示,需要经过以下步骤:
1、从磁盘拷贝图片数据到内核缓冲区(内核决定的,无法干扰,也不需要处理)
2、从内核缓冲区复制数据到用户空间
3、把图像数据赋值给 UIImageView,如果图片数据是没有解码的 png / jepg 格式,会先解码为位图数据
4、进行图像渲染
其中 imageNamed 加载图片之后会立刻进行解码,并由系统缓存图片解码后的数据(即刻占用内存);imageWithContentsOfFile 加载图片后,不会进行解码(直至渲染时才会占用内存)。然后再来了解一下图片占内存容量计算公式:
内存大小 = 图片高度(像素) * 图片宽度(像素) * 一个像素所占内存空间
常见的几种图片格式下:
1、ARGB_4444:每个像素占四位,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位
2、ARGB_8888:每个像素占四位,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位
3、RGB_565:每个像素占四位,即R=5,G=6,B=5,没有透明度,那么一个像素点占5+6+5=16位
4、ALPHA_8:每个像素占四位,只有透明度,没有颜色。
一般情况下我们都是使用的ARGB_8888( iOS 和安卓都是用这个),由此可知它是最占内存的,因为一个像素占32位,8位=1字节,所以一个像素占4字节的内存。对于 iOS 的 Retain 屏幕每个视图点实际上是用的2-3个像素点显示,假如一张分辨率10241024的图片,iPhone 6在渲染的时候实际内存占用 10241024323 = 96MB,这是一个相当大的内存消耗,因此 APP 在渲染图片前最好都进行压缩至缩率图。而 SDWebImage,YYImage 等加载大图片时候都会因内存暴增 Crash,解决方案可以在渲染图片之前对图片进行压缩处理。网上大部分的 UIImage 压缩方案都是类似如下代码:
但对于 UIGraphics 实际已经开始占用内存(未等压缩图片就已经 Crash 了)。正确的方式应该使用 ImageIO 来压缩图片,如下
然而实际应用中还是会遇到需要加载分辨率超大图片的场景,比如地图类 APP,手游中地图场景等,这时候该怎么处理呢?答案按需处理,局部加载,每次只渲染手机一屏幕的图片。在《iOS 核心动画高级技巧》中有到 CATiledLayer 将大图分解成小片然后将它们单独按需载入,定义如下:
在 Apple 官方的例子里面也提供了使用 CATiledLayer 来渲染超大图的例子。https://developer.apple.com/library/content/samplecode/PhotoScroller/PhotoScroller.zip 其中原理:
1、将大分辨率的图片按手机不同缩放比下分割成许多局部小图片
2、使用 CATiledLayer 来按需渲染局部图片
3、在渲染过程中根据坐标位置计算所需哪一张小图片
地图类 APP 的基础原理也大致如此吧。