iOS中的图片加载
加载方式
imageWithContentsOfFile:+图片路径([[NSBundle mainBundle] pathForResource:@”icon” ofType:@”png”]),不会缓存到内存,适合用于加载较大的不常用的图片降低内存消耗。
imageNamed:+图片名,会缓存到内存且无法释放。
imageWithData:+二进制data,不缓存,适合网络下载图像生成UIImage。
然后将生成的UIImage赋值给UIImageView,CA捕捉到图层树的变化在下一个run loop中提交,然后对图片进行copy操作:解压缩成位图、读到内存中、CA渲染到UIImageView图层。
解压缩
位图其实是一个像素集合,所以图片的文件大小和其解压后所占的内存没有任何关系,只跟图片的像素有关(图片大小:像素宽*像素高*4字节)。但解压缩需要消耗大量CPU时间而且默认在主线程进行。所以出现了,在子线程提前强制解压缩的方案(因为默认的解压缩步骤是在显示到屏幕上的时候才执行的),核心函数:CGBitmapContextCreate。
SDWebImageDecoder中的decodedImageWithImage函数提供了解决方案,原理如下:CGBitmapContextCreate创建一个位图上下文→CGContextDrawImage绘制原始位图到上下文→CGBitmapContextCreateImage创建解压后的新位图。
注:CGContextDrawImage过程中把alpha通道移除了,所以不支持透明度的图片。
图片压缩
对于超大的图片,sd还提供了decodedAndScaledDownImageWithImage压缩方法,避免内存爆炸。原理:将图像矩阵按照规则分割成小型子矩阵进行压缩,然后插值拼接。
SDWebImage
UIImageView+WebCache.h → SDWebImageManager → SDImageCache → SDWebImageDownloader
源码解析
加载:通常我们使用UIImageView显示图片,所以SDWebImage提供了它的扩展方法,一般使用
- (void)sd_setImageWithURL:(nullable NSURL *)url //路径
placeholderImage:(nullableUIImage *)placeholder //缺省图
options:(SDWebImageOptions)options //加载规则
progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock //进度
completed:(nullableSDExternalCompletionBlock)completedBlock; //完成的block
或其变种方法进行图片加载(底层依赖UIView+WebCache的扩展方法,更底层是SDWebImageManager的loadImageWithURL方法)。
注:底层流程,SDImageCache的queryCacheOperationForKey方法判断缓存,imageDownloader的downloadImageWithURL方法开启下载任务(方法中利用SDWebImageDownloaderOperation对象配置各种属性以及实现提前子线程解压缩的流程)。
SDWebImageManager:
下载图片:核心方法→loadImageWithURL
注:其中还封装了缓存类和下载器的单例;shouldDownloadImageForURL代理设置是否在无缓存时下载;transformDownloadedImage设置是否对图片进行transform操作。
SDImageCache:
请求获取缓存:核心方法→queryCacheOperationForKey(一般用url做key),实现流程→imageFromMemoryCacheForKey判断是否在缓存中,self.memCache;diskImageForKey从磁盘中获取,含解码decodedImageWithImage。
生成缓存:核心方法→storeImage: forKey: completion:(可设置是否缓存到硬盘等),实现流程→self.memCache setObject: forKey: cost: 实现内存缓存,storeImageDataToDisk: forKey: 实现磁盘缓存(文件名使用了key即URL的MD5转换)。
缓存清理:核心方法→clearMemory清理内存,clearDiskOnCompletion异步清理磁盘;每次app退出接收到UIApplicationWillTerminateNotification通知时执行deleteOldFilesWithCompletionBlock方法清理过期缓存,若清理完成后磁盘占用大于设定值self.config.maxCacheSize,则按照时间排序(NSURLContentModificationDateKey)后删除。
注:可设置maxMemoryCost最大空间花销或者maxMemoryCountLimit最大数量进行缓存约束;SDImageCacheConfig中控制了maxCacheAge过期时间和maxCacheSize最大缓存空间。
SDWebImageDecoder:
提前解码图片:核心方法→decodedImageWithImage,image.CGImage获取CGImageRef位图,CGColorSpaceRef色彩空间,宽高等,根据前文中的解压缩原理返回一个新的UIImage对象。
压缩图片:核心方法:decodedAndScaledDownImageWithImage,根据属性判断是否需要压缩,然后获取图形上下文和位图,计算像素并分块,然后循环插值。
底层依赖于Quartz 2D的图像处理库。
图片缓存模块
核心功能:根据键值从缓存中获取图片;缓存图片到内存或硬盘;缓存管理机制(过期、清理、限制大小)。
缓存淘汰算法之FIFO
原理:按照时间顺序先进先出,队列。
Second-Chance(FIFO变种1)
原理:给对象加标志位,如果引用了则设置标志位为1。淘汰时判断如果标志位为1,则置为0后加入队列尾部,淘汰一下个为0的对象。
复杂度:需要记录标志位和数据移动,命中率和代价比FIFO高。
Clock(Second-Chance改进版)
通过指针实现环形队列避免数据移动,降低代价。
注:FIFO及其变种算法,命中率过低,实际很少使用。
缓存淘汰算法之LRU
原理:最近最少使用。判断最近被使用的时间,最远的优先淘汰。新数据插入链表头部,缓存被命中时移动到链表头部,链表满时丢弃尾部。
命中率:存在热点数据时效率很好但偶发性、周期性的批量操作会导致命中率急剧下降,缓存污染情况严重。
代价:遍历链表找命中的数据块,然后移动到头部。
LRU-K(解决LRU-1的缓存污染)
原理:需要两个队列,首次访问的数据加入到历史队列中,当数据访问次数达到K次时,放入正式的缓存队列中,并删除历史队列中的索引。淘汰数据时,先按照FIFO或者LRU淘汰历史队列中的数据,再按照时间排序淘汰缓存队列中的数据。
命中率:降低了缓存污染问题,命中率提高。但适应性差,K值大时需要大量访问才能将数据移除历史队列,加入缓存。
代价:由于要维护两个队列,所以内存消耗大;由于要基于时间排序,所以CPU消耗高;由于是优先级队列,算法复杂度也较高。
Two queues(2Q,FIFO+LRU)
原理:第一次访问加入FIFO队列中,第二次访问将数据从FIFO移入LRU,两队列各自按照规则淘汰数据。
命中率:高于LRU,代价是FIFO和LRU之和。
缓存淘汰算法之LFU
原理:每个数据块都有一个引用计数,按照引用计数排序,相同按时间。新数据插入到队列尾部,被访问时引用计数+1,队列重新排序。淘汰数据时直接淘汰尾部。
命中率:一般情况下优于LRF,但需要时间适应新的访问模式。
代价:需要记录所有访问数据,内存消耗较高;需要排序,性能消耗较高。
变种:LFU*,只淘汰引用计数为1的数据,无法适应访问模式。
LFU-Aging,考虑访问时间,通过最大访问数来控制访问时间,比如达到100了,就变成50,能更快适应新的访问模式。
Window-LFU,不记录所有历史访问,只记录一段时间内的访问,节约了内存和性能。