SDWebImage原理相关

一、SDWebImage的核心是:SDWebImageManger。SDWebImage的工作就是由它调度SDImageCache(一个处理缓存的类)和一个SDWebImageDownloader(负责下载网络图片)来完成的。
二、SDWebImage提供了如下三个category来进行缓存。
1.UIImageView + WebCache imageView的图片
2.UIButton + WebCache 给按钮设置图片
3.MKAnnotationView + WebCache 地图大头针

1.首先将placeholderImage进行展示,SDWebImageManager根据URL开始处理图片

2.SDImageCache从缓存中查找图片图片,如果有SDImageCacheDelegate回调image:didFindImage:forkey:useInfo:给SDWebImageManager ,SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片

3.缓存中没有,生成NSInvocationOperation添加到队列中开始在硬盘中查找,
根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
如果找到会将图片添加到内存缓存中(如果空闲缓存不够,会先清理)然后SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

4.如果硬盘中没有则共享或生成下载器SDWebImageDownLoader开始下载图片,图片下载由NSURLConnection来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

5.图片解码处理在一个NSOperationQueue完成,不会拖慢主线程UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

6.在主线程notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给SDWebImageDownloader。
imageDownloader:didFinishWithImage: 回调给SDWebImageManager告知图片下载完成

7.通知所有的downloadDelegates下载完成,回调给需要的地方展示图片。将图片保存到SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程。

8.SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片,
当应用进入后台时,会涉及到『Long-Running Task』正常程序在进入后台后、虽然可以继续执行任务。但是在时间很短内就会被挂起待机。
Long-Running可以让系统为app再多分配一些时间来处理一些耗时任务。

流程图如下


10405915-153e48f83e03898c.png

四、缓存要点
1.SDWebImage 实现了一个叫做 AutoPurgeCache 的类 继承自 NSCache ,相比于普通的 NSCache, 它提供了一个在内存紧张时候释放缓存的能力。

自动删除机制:当系统内存紧张时,NSCache 会自动删除一些缓存对象
线程安全:从不同线程中对同一个 NSCache 对象进行增删改查时,不需要加锁
不同于 NSMutableDictionary、NSCache存储对象时不会对 key 进行 copy 操作
@end

@implementation AutoPurgeCache

- (nonnull instancetype)init {
    self = [super init];
    if (self) {
#if SD_UIKIT
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
    }
    return self;
}

- (void)dealloc {
#if SD_UIKIT
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}

@end

2.通过内部的一个枚举可以看出磁盘不是强制写入的


typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { /**
     *  禁用磁盘缓存
     */ SDWebImageCacheMemoryOnly = 1 << 2,
}

3.缓存的时机有两种情况:一个是在下载完成之后,自动保存,或者开发者通过代理处理完图片并返回后缓存。二是当缓存中没有、但是从硬盘中查询到了图片。

4.磁盘的缓存时长默认是一周,清除时间是当程序退出到后台、或者被杀死的时候,在后台执行耗时任务是要申请时间,不要一进入后台短时间就被挂起

// 默认缓存时长
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

// 清理时间
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(deleteOldFiles)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundDeleteOldFiles)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];
/*   磁盘清理的原则
首先、通过时间进行清理。(最后修改时间>一周)
然后、根据占据内存大小进行清理。(如果占据内存大于上限、则按时间排序、删除到上限的1/2。)
由于在源码中没有找到给maxCacheSize设置最大显示的代码,所以猜测默认没有设置上限

*/
// 清理磁盘的方法
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

5.如何保证内存和磁盘的读写安全?
a:NScache是线程安全的,在多线程操作中,不需要对Cache加锁。
读取缓存的时候是在主线程进行。所有不需要要担心线程安全
b:磁盘的读取虽然创建了一个NSOperation对象、但据我所见这个对象只是用来标记该操作是否被取消、以及取消之后不再读取磁盘文件的作用。
真正的磁盘缓存是在另一个IO专属线程中的一个串行队列下进行的。
如果你搜索self.ioQueue还能发现、不只是读取磁盘内容。
包括删除、写入等所有磁盘内容都是在这个IO线程进行、以保证线程安全。
但计算大小、获取文件总数等操作。则是在主线程进行。

- (void)checkIfQueueIsIOQueue {
    const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
    const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
    if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
        NSLog(@"This method should be called from the ioQueue");
    }
}

6.磁盘的路径:默认路径
缓存在磁盘沙盒目录下Library/Caches
二级目录为~/Library/Caches/default/com.hackemist.SDWebImageCache.default
当然也可自定义文件名

  1. 下载最大并发数、超时时长?
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadTimeout = 15.0;

8、缓存图片命名

//1.写入缓存时、直接用图片url作为key
//写入缓存 NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost]

// 2.写入磁盘用url的MD5编码作为key。可以防止文件名过长,以及避免重名

五、图片格式和解码
1.为什么下载和从磁盘中读取的图片要解码?

一般下载或者从磁盘获取的图片是PNG或者JPG,这是经过编码压缩后的图片数据,不是位图,要把它们渲染到屏幕前就需要进行解码转成位图数据,而这个解码操作比较耗时。
你也可以这么理解,图片在远端存储一定都是编码后存储的,这样体积小,一个图像可以看做是一个图像文件,里面包含了文件头,文件体和文件尾,图像的数据就包含在文件体中,而我们的解码就是运用算法将文件体中的图像数据转化为位图数据,方便渲染和展示。
iOS默认是在主线程解码,所以SDWebImage将这个过程放到子线程了。
同时因为位图体积很大,所以磁盘缓存不会直接缓存位图数据,而是编码压缩后的PNG或JPG数据。

2.怎么判断图片格式?

将数据data转为十六进制数据,取第一个字节数据进行判断。

3.如何播放gif图片

3.1 把用户传入的gif图片转成NSData
3.2 根据该Data创建一个图片数据源(NSData->CFImageSourceRef)
3.3 计算该数据源中一共有多少帧,把每一帧数据取出来放到图片数组中
3.4 根据得到的数组+计算的动画时间
3.5 [UIImage animatedImageWithImages:images duration:duration];

六、SDWebImage在多线程下载图片时防止错乱的策略
由于cell的重用机制,在我们加载出一个cell的时候imageView数据源开启一个下载任务并返回一个image,当cell重用时,其数据源又会开启一个下载任务下载新的image,但关联的对象是同一个imageView,这个时候直接setImage时会发生错乱。

SDWebImage的处理是:
imageView对象会关联一个下载列表(列表是给AnimationImages用的,这个时候会下载多张图片),当tableview滑动,imageView重设数据源(url)时,会cancel掉下载列表中所有的任务,然后开启一个新的下载任务。这样子就保证了只有当前可见的cell对象的imageView对象关联的下载任务能够回调,不会发生image错乱。
同时,SDWebImage管理了一个全局下载队列(在DownloadManager中),并发量设置为6.也就是说如果可见cell的数目是大于6的,就会有部分下载队列处于等待状态。而且,在添加下载任务到全局的下载队列中去的时候,SDWebImage默认是采取LIFO(last in,first out)策略的,具体是在添加下载任务的时候,将上次添加的下载任务添加依赖为新添加的下载任务。

[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperationaddDependency:operation];
wself.lastAddedOperation = operation;
}

七、下载图片失败后的处理
使用

- (void)sd_setImageWithURL:(NSURL *)url
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder 

这两个方法下载图片,如果下载第一次 失败了,再使用同样的url 调用该方法,也不会进行第二次尝试, 因为 SD会记录 失败的URL ,对它直接进行错误处理,

@synchronized (self.failedURLs) {
    isFailedUrl = [self.failedURLs containsObject:url];
}

好在 SD 有提供接口 灵活 处理这样的情况,避免这样的情况 是使用

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,                    // 值为2的0次方
    SDWebImageLowPriority = 1 << 1,                    // 值为2的1次方
    SDWebImageCacheMemoryOnly = 1 << 2,                // 值为2的2次方
    SDWebImageProgressiveDownload = 1 << 3,            // 值为2的3次方
    SDWebImageRefreshCached = 1 << 4,                  // 值为2的4次方
    SDWebImageContinueInBackground = 1 << 5,           // 值为2的5次方
    SDWebImageHandleCookies = 1 << 6,                  // 值为2的6次方
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,    // 值为2的7次方
    SDWebImageHighPriority = 1 << 8,
    SDWebImageDelayPlaceholder = 1 << 9,
    SDWebImageTransformAnimatedImage = 1 << 10,
    SDWebImageAvoidAutoSetImage = 1 << 11,
    SDWebImageScaleDownLargeImages = 1 << 12
};
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options 
// 由于SD提供的这个枚举是位移枚举,可以同时传入多个,比如:
[icommageView sd_setImageWithURL:url placeholderImage:image options:SDWebImageRefreshCached | SDWebImageRetryFailed progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
     }];

注:本文是通过个人研读SD源码和参考其他博文总结的SD实现流程和使用过程中有可能遇到的问题,如有大佬发现有误之处,欢迎指正

你可能感兴趣的:(SDWebImage原理相关)