UML类图
SDWebImage主要分为以下模块:
- 主管理器
- 图片缓存
- 图片下载
- 图片编解码
- 分类工具
- 预加载
下面以 [imageView sd_setImageWithURL:[NSURLURLWithString:urlString]]; 这行代码的执行流程来讲解几个模块。
调用该方法首先会进入分类工具模块。
分类工具
包括UIButton,MKAnnotationView(地图大头针),UIImageView,FLAnimatedImageView(gif)的分类工具使用最终都会调用UIView分类工具的sd_internalSetImageWithURL方法。
sd_internalSetImageWithURL方法中,validOperationKey为UIImageView,sd_cancelImageLoadOperationWithKey的作用是判断当前有没有正在进行的UIImageView异步下载任务, 若有则停止它们。
看到这里可能会有疑问,我们想要的是取消当前图片的下载任务,那么根据UIImageView字符串来取消不会取消掉其他所有图片的下载任务吗?下文分析有解答。
从operationDictionary中取出key为UIImageView字符串的任务,并取消它。可以看到,为了避免线程安全问题,在Dictionary读写操作上都加了@synchronized递归锁。
可以看到,operationDictionary并非全局变量,而是用懒加载的方式+关联对象挂载在当前的UIView对象上。也就是说一个UIView对象和一个operationDictionary是 1对1 关系。如果有三个UIImageView在下载图片,那么就有三个operationDictionary,因此前面的疑问也已经解答了。
疑问:
为什么对于一个UIView对象需要容器来存储图片?
因为有时候一个UIView关联的图片是多张的,这就有多个下载图片任务,比如gif本质是多张图片。
下面来看下sd_operationDictionary的数据结构。NSMapTable是个类似NSDictionary的哈希表结构,只是NSDictionary对Key只能保持copy,value保持retain。而NSMapTable可自定义赋值方式,可以看到sd_operationDictionary对Key保持了strong(retain),对value保持了weak。因为key是不可变NSString,所以copy或strong区别不大。在SDWebImage中,将图片请求所需的操作定了在SDWebImageOperation协议中,支持该协议的对象即支持网络图片操作。
疑问:sd_operationDictionary对id<SDWebImageOperation>保持弱引用,那么id<SDWebImageOperation>不会被释放掉吗?
作者提供的注释给出了答案,key is copy(笔误),value使用weak,因为操作(id<SDWebImageOperation>)已经被主管理器(SDWebImageManager)的 当前运行的操作列表(runningOperations)属性强引用持有了。
这些方法可能主线程外的被其他线程调用,所以我们加了锁来保持线程安全。
UIView+WebCacheOperation.m 文件中,提供了一系列sd_operationDictionary的管理方法,在读写方面都加入了@synchronized锁。
下面回到sd_internalSetImageWithURL:方法,首先将当前下载图片地址关联到当前的UIImageView上,以方便后续使用。
option和context是参数带入的配置项和上下文信息,我们这时UIImageView带进来的options和context是0和nil。
首先通过options位运算判断是否开启了占位图延迟显示。若未开启则判断是否带入了dispatch_group,这里的dispatch_group用于gif的多图下载,若带入了则触发dispatch_group_enter。dispatch_group_enter和dispatch_group_leave是配对使用的,与dispatch_group_async类似,都是将任务加入到group中。这里SDWebImage并没有配对的leave,说明leave需要开发者在实现代码中自己添加。
下面来到dispatch_main_async_safe宏。其作用是切换到主线程执行任务。
从下图可以看到,在SDWebImage早期阶段是使用[NSThread isMainThread]判断是否处于主线程,若是则直接在当前主线程执行block。
后来有人发帖MKMapView调用addOverlay方法,必须在在主队列执行,若只是在主线程而不在主队列可能crash。
Apple方面承认了这是一个bug,isMainThread和gcd的使用可能导致crash。
I contacted Apple DTS, they confirmed it is a bug in MapKit. But they also cautioned that the pattern of dispatch_sync and the isMainThread will likely run into more issues like this and the one reported by @laptobbe above.
They also explicitly stated that main queue and the main thread are not the same thing, but are have subtle differences, as shown in my demo app.(https://github.com/ReactiveCocoa/ReactiveCocoa/issues/2635#issuecomment-170215083)
因此,SDWebImage作者也进行了如下更新。
dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())的作用是判断当前队列和主队列的标签名称是否相同,若相同则说明当前处于主线程。
注:UIApplicationMain内已经处于主队列,后续的操作都是在主队列执行。
下面来看下切换到主线程调用的方法,该方法主要作用是设置图片,设置占位图,缓存图片,网络下载图片都会通过该方法。
该方法内部实现比较长,就不贴出图片,直接写过程。从UIImageView调用setImageBlock都为nil。
1. 如果是UIImageView,且配置了setImageBlock,则不处理,直接回调setImageBlock。
2. 如果是UIImageView,未配置setImageBlock,则将图片设置为占位图。
3. 如果是UIButton,则将图片设置为占位图。
4. 如果开启了过渡动画,则在过渡动画结束将图片设置为占位图。
可以看出,当前场景UIImageView第一次进来,方法内只执行了imageView.image = placeholder;。
判断是否有传入自定义manager,如果没有使用默认的manager,一般都使用后者。
初始化下载进度block,sd_imageProgress是懒加载动态关联到UIView的对象,NSProgress类型。
Manager的核心方法loadImageWithURL,通过url从缓存或者网络去找图片,然后将图片数据通过block返回。
这里可以看到SDWebImage中设计的 weak-strong dance,当block回调触发时,可能self(UIView)已经被销毁了,如果销毁则立即返回,不进行后续处理。若未销毁,则需要防止后续操作中途销毁,对self进行strong强引用。只有当strong指针sself离开作用域(退出block),sself指针才会释放对self的强引用。
block内的具体操作如下:
1. 关闭加载指示器。
2. 加载进度设置为完成。
3. 如果设置了SDWebImageAvoidAutoSetImage(关闭自动设置图片),比如需要对图片进行处理,加水印等,则不会触发设置图片。
在下一个runloop周期更新View布局。
4. 如果是多图下载(gif),则使用dispatch_group_notify监听group内任务完成后再触发完成回调。
可以看到这里设置缓存或网络返回的图片的方法与设置placeholder是相同的。
下面来看下Manager的核心方法loadImageWithURL内部,如何实现图片的下载和缓存。
首先对参数url做了容错处理。
将我们设置的网络图片策略转换成内部的缓存策略。
SDWebImage默认使用内存缓存。
/**
* By default, we do not query disk data when the image is cached in memory. This mask can force to query disk data at the same time.
*/
SDImageCacheQueryDataWhenInMemory =1<<0, // 默认在内存中查到就不查磁盘,开启则同时查询磁盘
/**
* By default, we query the memory cache synchronously, disk cache asynchronously. This mask can force to query disk cache synchronously.
*/
SDImageCacheQueryDiskSync =1<<1, // 默认,我们同步查询内存缓存,异步查询磁盘缓存。开启则强制同步查询磁盘缓存。
/**
* By default, images are decoded respecting their original size. On iOS, this flag will scale down the
* images to a size compatible with the constrained memory of devices.
*/
SDImageCacheScaleDownLargeImages =1<<2 // 是否需要缩小大图片。
SDWebImage默认使用图片地址作为缓存存储的key,也可以通过设置cacheKeyFilter来自定义key规则,比如图片可能有多个地址,图片名是md5,则可以在自定义的cacheKeyFilter中只返回图片名。
queryCacheOperationForKey方法内,会先从内存缓存中查找该key对应的图片。SD中图片缓存管理类为SDImageCache,其中SDMemoryCache内存缓存作为其属性,SDMemoryCache继承自NSCache。NSCache是线程安全的,且会自动根据内存使用情况自动删减缓存数据。SDMemoryCache内部还有个NSMapTable属性,默认使用weak来存储value。当config中的shouldUseWeakMemoryCache开启时,SDMemoryCache同时会在NSMapTable中存储一份。shouldUseWeakMemoryCache用于解决app进入后台内存被清除了,回到前台cell闪烁的问题。
The option to control weak memory cache for images. When enable, `SDImageCache`'s memory cache will use a weak maptable to store the image at the same time when it stored to memory, and get removed at the same time. * However when memory warning is triggered, since the weak maptable does not hold a strong reference to image instacnce, even when the memory cache itself is purged, some images which are held strongly by UIImageViews or other live instances can be recovered again, to avoid later re-query from disk cache or network. This may be helpful for the case, for example, when app enter background and memory is purged, cause cell flashing after re-enter foreground. * Defautls to YES. You can change this option dynamically.
下面回到queryCacheOperationForKey方法。
内存缓存返回图片且未设置强制读取磁盘缓存,则直接带上图片触发block回调。
定义了一个磁盘查询图片的block。
可以看到这里加了个@autoreleasepool,从文件读取数据(dataWithContentsOfURL),NSData转换为UIImage的过程中会产生大量autorelease临时变量,这样可以使得临时变量尽快释放。
如果缓存中读取到了图片(设置了强制读取磁盘),则磁盘图片diskImage=缓存图片(两者相同,避免性能浪费),通过doneBlock(diskImage, diskData, cacheType)回调,返回缓存图片(磁盘图片),磁盘数据,SDImageCacheTypeMemory)。
如果缓存中未读到图片,则将磁盘setup数据转换成UIImage,并同时设置缓存(shouldCacheImagesInMemory),最后doneBlock返回。
使用默认方式解码图片。
sd_imageFormat是SD为UIImage动态关联的属性,代表图片格式。
取出图片第一字节,判断图片格式。
typedefNS_ENUM(NSInteger,SDImageFormat) {
SDImageFormatUndefined = -1,
SDImageFormatJPEG =0,
SDImageFormatPNG,
SDImageFormatGIF,
SDImageFormatTIFF,
SDImageFormatWebP,
SDImageFormatHEIC,
SDImageFormatHEIF
};
根据配置选择同步或异步的方式查询磁盘缓存。
ioQueue是个串行队列,虽然加入ioQueue的可能处于不同线程,但串行执行方式保证了文件IO安全。