大神原文地址:http://blog.csdn.net/deft_mkjing/article/details/52900586
更新:SDWebImageDecoder是用来图片的解压缩(原理以及为什么)
SD内部已经帮我们把请求回来的数据或者缓存到本地的图片资源都进行了异步解压缩,因此不需要我们来做,简单了解下
图片压缩流程
- 假设我们使用
+imageWithContentsOfFile:
方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
- 然后将生成的
UIImage
赋值给 UIImageView
;
- 接着一个隐式的
CATransaction
捕获到了 UIImageView
图层树的变化;
- 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
- 分配内存缓冲区用于管理文件 IO 和解压缩操作;
- 将文件数据从磁盘读到内存中;
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
- 最后 Core Animation 使用未压缩的位图数据渲染
UIImageView
的图层。
在上面的步骤中,我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。
点击打开链接摘自大神的博客,需要详细的可以点进去看看
个人理解:
1.首先PNG和JPEG都是图片的压缩格式,PNG是无损压缩,支持alpha,JPEG有损,可选1-100压缩比例
2.例如你有一张PNG的图片 20B,那么你对应的图片二进制数据也是20B的,解压缩后的图片就可能是几百几千B了
3.具体计算公式就是像素宽*像素高*每个像素对应的字节
4.那么当你进行图片渲染的时候,必须得到解压缩后的原始像素数据,才能进行图形渲染,这就是解压缩的原因
SD在SDWebImageDecoder这个文件中进行了强制解压缩,我们赋值给imageView的时候已经是解压缩的文件了,因此不会卡主主线程,不然默认是在主线程进行解压缩,图片一多,卡爆了
平时项目中用的库挺多的,没有认认真真看过几个,给自己定了个计划今年把SDWebImage,AFNetWorking看完,有时间把YYKit的组件也看看,虽然才这么几个,我感觉看完这些能学习个百分之一的代码逻辑,我就知足了,最近花了一周的时间在网上看了SDWebImage的源码分析以及SD本身代码的逻辑分析,分别都加上了注解,毕竟这是迈出去的第一步,看完一遍之后呢,偶然在网上看到一个图,加上自己的理解,觉得这个图太合适了,诠释的很完整
上图 图片出处
这个图分四个部分,听完我的分析你就能基本上理解了
首先把SD的所有类的作用简单介绍下
3.8.1版本所有类的作用介绍一下
NSData+ImageContentType 通过Image data判断当前图片的格式
SDImageCache 缓存 定义了 Disk 和 memory二级缓存(NSCache)负责管理cache 单例
SDWebImageCompat 保证不同平台/版本/屏幕等兼容性的宏定义和内联 图片缩放
SDWebImageDecoder 图片解压缩,内部只有一个接口
SDWebImageDownloader 异步图片下载管理,管理下载队列,管理operation 管理网络请求 处理结果和异常 单例
存放网络请求回调的block 自己理解的数据结构大概是
// 结构{"url":[{"progress":"progressBlock"},{"complete":"completeBlock"}]}
SDWebImageDownloaderOperation 实现了异步下载图片的NSOperation,网络请求给予NSURLSession 代理下载
自定义的Operation任务对象,需要手动实现start cancel等方法
SDWebImageManager 核心管理类 主要对缓存管理 + 下载管理进行了封装 主要接口downloadImageWithURL单利
SDWebImageOperation operation协议 只定义了cancel operation这一接口 上面的downloaderOperation的代理
SDWebImagePrefetcher 低优先级情况下预先下载图片,对SDWebImageViewManager进行简单封装 很少用
MKAnnotationView+WebCache – 为MKAnnotationView异步加载图片
UIButton+WebCache 为UIButton异步加载图片
UIImage+GIF 将Image data转换成指定格式图片
UIImage+MultiFormat 将image data转换成指定格式图片
UIImageView+HighlightedWebCache 为UIImageView异步加载图片
UIImageView+WebCache 为UIImageView异步加载图片
UIView+WebCacheOperation 保存当前MKAnnotationView / UIButton / UIImageView异步下载图片的operations
看完基本就这样的表情,没关系,继续往下看,最后会给个完整的总结
看下核心类的基本操作图
自己花的简图,凑活看吧
OK,我们就开始根据调用的代码一步步按照逻辑走一遍
1.第一步(外部控件)
直接调用sd暴露在外面的方法
- __weak typeof(self)weakSelf = self;
- [self.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://cdn.duitang.com/uploads/item/201111/08/20111108113800_wYcvP.thumb.600_0.jpg"] placeholderImage:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
-
- if (image && cacheType == SDImageCacheTypeNone)
- {
- weakSelf.imageView.alpha = 0;
- [UIView animateWithDuration:1.0f animations:^{
-
- weakSelf.imageView.alpha = 1.f;
- }];
- }
- else
- {
- weakSelf.imageView.alpha = 1.0f;
- }
-
- }];
这个很简单,大家也经常用,用的时候打一下sd机会出来一串方法,这一串方法最终在内部都会转换成
- [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
2.第二步(UIImageView + WebCache)
-
-
-
-
- - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
-
-
- [self sd_cancelCurrentImageLoad];
-
- {......省略一段代码}
-
- if (url) {
- {......省略一段代码}
- __weak __typeof(self)wself = self;
-
-
-
- id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
- [wself removeActivityIndicator];
- if (!wself) return;
- dispatch_main_sync_safe(^{
- if (!wself) return;
-
- if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
- {
- completedBlock(image, error, cacheType, url);
- return;
- }
- else if (image) {
- wself.image = image;
- [wself setNeedsLayout];
- } else {
- if ((options & SDWebImageDelayPlaceholder)) {
- wself.image = placeholder;
- [wself setNeedsLayout];
- }
- }
-
- if (completedBlock && finished) {
- completedBlock(image, error, cacheType, url);
- }
- });
- }];
-
-
-
- [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
- } else {
- dispatch_main_async_safe(^{
- [self removeActivityIndicator];
- if (completedBlock) {
- NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
- completedBlock(nil, error, SDImageCacheTypeNone, url);
- }
- });
- }
- }
分析下:
这里首先会调用cancel的方法,为什么会这样呢??解释下会发生的bug
正常情况下屏幕展示的cell是这样的,当网络不稳定的情况下,第一个cell还在请求数据的时候,用户直接下滑了
就会出现下面这样的状态,这个是tableView的复用简单图解,当列表后面的cell准备加载的时候,会从复用池中找,有的话直接拿出来用,那么复用池子里面的cell还是第一个cell指向的imageView指针,如果没有停止之前的网络请求,那么直接拿出来再根据后面的数据绑定进行请求就会发生数据错位,这是非常可怕的。原因就是如果没有取消之前的请求,imageView的原理就是优先展示最新下载完之后的图片,就会立马显示出来,所以一定要先取消之前的
再来个例子:
一个imageView请求了两张图片,1.png 和 2.png,但我们只希望显示 2.png,所以需要取消 1.png的请求。原因有两点:
1.在异步请求中(先后顺序不定),有可能 1.png 会在 2.png 后面获取到,会覆盖掉2.png
2.减少网络请求,网络请求是一个很耗时的操作
然后这一步的重点就是会启用SDWebImageManager管理单例,调用他的方法进行网络请求
- - (id )downloadImageWithURL:(NSURL *)url
- options:(SDWebImageOptions)options
- progress:(SDWebImageDownloaderProgressBlock)progressBlock
- completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
3.第三步(SDWebImageManager)
这个管理类有两个得力的手下
一个是SDImageCache 专门管理缓存
A:NSCache负责内存缓存,用法和NSDictionary基本一样
B:磁盘缓存用NSFileManager写文件的方式完成
注:
1. NSCache具有自动删除的功能,以减少系统占用的内存,还能设置内存临界值
2. NSCache是线程安全的,不需要加线程锁;
3. 键对象不会像 NSMutableDictionary 中那样被复制。(键不需要实现 NSCopying 协议)。
-
-
- - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
- if (!doneBlock) {
- return nil;
- }
-
- if (!key) {
- doneBlock(nil, SDImageCacheTypeNone);
- return nil;
- }
-
-
-
- UIImage *image = [self imageFromMemoryCacheForKey:key];
- if (image) {
- doneBlock(image, SDImageCacheTypeMemory);
- return nil;
- }
-
-
-
- NSOperation *operation = [NSOperation new];
- dispatch_async(self.ioQueue, ^{
- if (operation.isCancelled) {
- return;
- }
-
-
- @autoreleasepool {
-
- UIImage *diskImage = [self diskImageForKey:key];
- if (diskImage && self.shouldCacheImagesInMemory) {
-
- NSUInteger cost = SDCacheCostForImage(diskImage);
-
- [self.memCache setObject:diskImage forKey:key cost:cost];
- }
-
- dispatch_async(dispatch_get_main_queue(), ^{
- doneBlock(diskImage, SDImageCacheTypeDisk);
- });
- }
- });
-
- return operation;
- }
小知识点:
这里开了异步串行队列去Disk中查找,保证不阻塞主线程,而且开了autoreleasepool以降低内存暴涨问题,能得到及时释放,如果能取到,首先缓存到内存中然后再回调
如果内存和磁盘中都取不到图片,就会让Manager的另一个手下SDWebImageDownloader去下载图片
A:这货也是一个单例,专门负责图片的下载图片的下载都是放在NSOperationQueue中完成的
-
- - (id )downloadImageWithURL:(NSURL *)url
- options:(SDWebImageOptions)options
- progress:(SDWebImageDownloaderProgressBlock)progressBlock
- completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
- {......}
-
- NSString *key = [self cacheKeyForURL:url];
-
-
-
-
-
-
- operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
- if (operation.isCancelled) {
- @synchronized (self.runningOperations) {
- [self.runningOperations removeObject:operation];
- }
-
- return;
- }
-
-
- if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
- if (image && options & SDWebImageRefreshCached) {
- dispatch_main_sync_safe(^{
-
- completedBlock(image, nil, cacheType, YES, url);
- });
- }
-
-
- {......}
-
-
- id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
-
- __strong __typeof(weakOperation) strongOperation = weakOperation;
-
- if (!strongOperation || strongOperation.isCancelled) {
-
-
-
- }
- else if (error) {
-
- dispatch_main_sync_safe(^{
- if (strongOperation && !strongOperation.isCancelled) {
- completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
- }
- });
-
- if ( error.code != NSURLErrorNotConnectedToInternet
- && error.code != NSURLErrorCancelled
- && error.code != NSURLErrorTimedOut
- && error.code != NSURLErrorInternationalRoamingOff
- && error.code != NSURLErrorDataNotAllowed
- && error.code != NSURLErrorCannotFindHost
- && error.code != NSURLErrorCannotConnectToHost) {
- @synchronized (self.failedURLs) {
- [self.failedURLs addObject:url];
- }
- }
- }
- else {
-
- if ((options & SDWebImageRetryFailed)) {
- @synchronized (self.failedURLs) {
- [self.failedURLs removeObject:url];
- }
- }
-
- BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
-
- if (options & SDWebImageRefreshCached && image && !downloadedImage) {
-
- }
- else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
-
-
-
-
-
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
-
- if (transformedImage && finished) {
- BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
-
- [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
- }
-
- dispatch_main_sync_safe(^{
- if (strongOperation && !strongOperation.isCancelled) {
- completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
- }
- });
- });
- }
- else {
-
- if (downloadedImage && finished) {
- [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
- }
-
- dispatch_main_sync_safe(^{
- if (strongOperation && !strongOperation.isCancelled) {
- completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
- }
- });
- }
- }
-
-
- if (finished) {
- @synchronized (self.runningOperations) {
- if (strongOperation) {
- [self.runningOperations removeObject:strongOperation];
- }
- }
- }
- }];
- operation.cancelBlock = ^{
- [subOperation cancel];
-
- @synchronized (self.runningOperations) {
- __strong __typeof(weakOperation) strongOperation = weakOperation;
- if (strongOperation) {
- [self.runningOperations removeObject:strongOperation];
- }
- }
- };
- }
- else if (image) {
-
- dispatch_main_sync_safe(^{
- __strong __typeof(weakOperation) strongOperation = weakOperation;
- if (strongOperation && !strongOperation.isCancelled) {
- completedBlock(image, nil, cacheType, YES, url);
- }
- });
- @synchronized (self.runningOperations) {
- [self.runningOperations removeObject:operation];
- }
- }
- else {
-
-
- dispatch_main_sync_safe(^{
- __strong __typeof(weakOperation) strongOperation = weakOperation;
- if (strongOperation && !weakOperation.isCancelled) {
- completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
- }
- });
-
- @synchronized (self.runningOperations) {
- [self.runningOperations removeObject:operation];
- }
- }
- }];
-
- return operation;
- }
4.第四步(SDWebImageDownloader)
- - (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
- __block SDWebImageDownloaderOperation *operation;
- __weak __typeof(self)wself = self;
-
-
-
-
- [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
- NSTimeInterval timeoutInterval = wself.downloadTimeout;
- if (timeoutInterval == 0.0) {
- timeoutInterval = 15.0;
- }
-
-
-
-
- NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
- request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
- request.HTTPShouldUsePipelining = YES;
-
- if (wself.headersFilter) {
- request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
- }
- else {
- request.allHTTPHeaderFields = wself.HTTPHeaders;
- }
-
-
-
- operation = [[wself.operationClass alloc] initWithRequest:request
- inSession:self.session
- options:options
- progress:^(NSInteger receivedSize, NSInteger expectedSize) {
- SDWebImageDownloader *sself = wself;
- if (!sself) return;
- __block NSArray *callbacksForURL;
-
-
-
- dispatch_sync(sself.barrierQueue, ^{
-
- callbacksForURL = [sself.URLCallbacks[url] copy];
- });
-
- for (NSDictionary *callbacks in callbacksForURL) {
- dispatch_async(dispatch_get_main_queue(), ^{
- SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
-
- if (callback) callback(receivedSize, expectedSize);
- });
- }
- }
-
- completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
- SDWebImageDownloader *sself = wself;
- if (!sself) return;
- __block NSArray *callbacksForURL;
-
-
-
- dispatch_barrier_sync(sself.barrierQueue, ^{
- callbacksForURL = [sself.URLCallbacks[url] copy];
- if (finished) {
- [sself.URLCallbacks removeObjectForKey:url];
- }
- });
-
- for (NSDictionary *callbacks in callbacksForURL) {
- SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
- if (callback) callback(image, data, error, finished);
- }
- }
- cancelled:^{
- SDWebImageDownloader *sself = wself;
-
-
- if (!sself) return;
- dispatch_barrier_async(sself.barrierQueue, ^{
- [sself.URLCallbacks removeObjectForKey:url];
- });
- }];
-
- operation.shouldDecompressImages = wself.shouldDecompressImages;
-
- if (wself.urlCredential) {
- operation.credential = wself.urlCredential;
- } else if (wself.username && wself.password) {
- operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
- }
-
- if (options & SDWebImageDownloaderHighPriority) {
- operation.queuePriority = NSOperationQueuePriorityHigh;
- } else if (options & SDWebImageDownloaderLowPriority) {
- operation.queuePriority = NSOperationQueuePriorityLow;
- }
-
-
-
-
-
-
- [wself.downloadQueue addOperation:operation];
-
- if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
-
-
-
- [wself.lastAddedOperation addDependency:operation];
- wself.lastAddedOperation = operation;
- }
- }];
-
- return operation;
- }
知识点:
1.通过调用addProgressCallback:completeBlock:forURL:createBlock:来确保同一url只会下载一次(看下面注释)
- - (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
-
- if (url == nil) {
- if (completedBlock != nil) {
- completedBlock(nil, nil, nil, NO);
- }
- return;
- }
-
-
-
-
- dispatch_barrier_sync(self.barrierQueue, ^{
- BOOL first = NO;
- if (!self.URLCallbacks[url]) {
-
-
-
- self.URLCallbacks[url] = [NSMutableArray new];
- first = YES;
- }
-
-
-
- NSMutableArray *callbacksForURL = self.URLCallbacks[url];
- NSMutableDictionary *callbacks = [NSMutableDictionary new];
- if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
- if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
- [callbacksForURL addObject:callbacks];
- self.URLCallbacks[url] = callbacksForURL;
-
-
- if (first) {
- createCallback();
- }
- });
- }
2.通过继承NSOperation的SDWebImageDownloaderOperation进来初始化下载任务(下一步再讲解内部),这里的回调就是上面方法里面数据结构存储起来的所有回调的遍历执行
progressBlock,completeBlock和CancelBlock都用到了GCD的barrier的方法,有时间慢慢再看看原理,先看基本介绍
// 所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理的
// _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
@property (SDDispatchQueueSetterSementics,nonatomic)dispatch_queue_t barrierQueue;
//func dispatch_barrier_async(_ queue: dispatch_queue_t, _ block: dispatch_block_t):
//这个方法重点是你传入的 queue,当你传入的 queue是通过 DISPATCH_QUEUE_CONCURRENT参数自己创建的 queue 时,这个方法会阻塞这个queue(注意是阻塞 queue,而不是阻塞当前线程),一直等到这个 queue中排在它前面的任务都执行完成后才会开始执行自己,自己执行完毕后,再会取消阻塞,使这个 queue中排在它后面的任务继续执行。
//如果你传入的是其他的 queue,那么它就和 dispatch_async一样了。
//func dispatch_barrier_sync(_ queue: dispatch_queue_t, _ block: dispatch_block_t):
//这个方法的使用和上一个一样,传入自定义的并发队列(DISPATCH_QUEUE_CONCURRENT),它和上一个方法一样的阻塞 queue,不同的是这个方法还会阻塞当前线程。
//如果你传入的是其他的 queue,那么它就和 dispatch_sync一样了。
3.把createBlock里面的网络请求任务加入NSOperationQueue队列中,该队列右两个属性
-
- typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
-
-
-
-
- SDWebImageDownloaderFIFOExecutionOrder,
-
-
-
-
-
- SDWebImageDownloaderLIFOExecutionOrder
- };
GCD不能很好的设置依赖关系,那么NSOperation就能很好的实现了,关键代码让上一次的任务依赖于最后进来的任务,就能实现LIFO
[wself.lastAddedOperationaddDependency:operation];
5.第五步(SDWebImageDownloaderOperation)
这个类是继承与NSOperation的,并且采用了SDWebImageOperation的代理(只有个cancel的方法),并且它只暴露了一个方法,initWithRequest:inSession:options:progress:completed:canceled这个初始化方法来配置
由于他是自定义的,那么就必须重写Start的方法,在该方法里面SD已经把NSURLConnection替换成了NSURLSession来进行网络请求的操作,简言之,只要实现NSURLSession的代理方法就能获取到下载数据
这里主要看下一个不断接受data的代理回调
- - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
-
-
-
-
-
-
-
- [self.imageData appendData:data];
-
- if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
-
-
-
-
-
- const NSInteger totalSize = self.imageData.length;
-
-
-
- CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
-
-
- if (width + height == 0) {
- CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
- if (properties) {
- NSInteger orientationValue = -1;
- CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
- if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
- val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
- if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
- val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
- if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
- CFRelease(properties);
-
-
-
-
-
-
-
- orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
- }
-
- }
-
- if (width + height > 0 && totalSize < self.expectedSize) {
-
-
- CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
-
- #ifdef TARGET_OS_IPHONE
-
-
- if (partialImageRef) {
- const size_t partialHeight = CGImageGetHeight(partialImageRef);
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
- CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
- CGColorSpaceRelease(colorSpace);
- if (bmContext) {
- CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
- CGImageRelease(partialImageRef);
- partialImageRef = CGBitmapContextCreateImage(bmContext);
- CGContextRelease(bmContext);
- }
- else {
- CGImageRelease(partialImageRef);
- partialImageRef = nil;
- }
- }
- #endif
-
-
- if (partialImageRef) {
- UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
- NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
- UIImage *scaledImage = [self scaledImageForKey:key image:image];
- if (self.shouldDecompressImages) {
- image = [UIImage decodedImageWithImage:scaledImage];
- }
- else {
- image = scaledImage;
- }
- CGImageRelease(partialImageRef);
-
-
- dispatch_main_sync_safe(^{
- if (self.completedBlock) {
- self.completedBlock(image, nil, nil, NO);
- }
- });
- }
- }
-
- CFRelease(imageSource);
- }
-
- if (self.progressBlock) {
- self.progressBlock(self.imageData.length, self.expectedSize);
- }
- }
CG框架下的图片处理还是有点看的懵逼,还有图片缩放以及解压操作,以我目前的知识还是很难理解,不过加了了宏观的中文注解,稍微先过下流程
- - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {}
最终,complete的代理回调把图片一步步回调出去,就如刚才一步步走进来一样
6.第六步(回调到SDWebImageManager存储图片,完成最终回调)
内存缓存没什么好讲的,直接调用NSCache的set方法
-
- - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
- if (!image || !key) {
- return;
- }
-
- if (self.shouldCacheImagesInMemory) {
- NSUInteger cost = SDCacheCostForImage(image);
-
- [self.memCache setObject:image forKey:key cost:cost];
- }
-
- if (toDisk) {
-
-
- dispatch_async(self.ioQueue, ^{
- NSData *data = imageData;
-
- if (image && (recalculate || !data)) {
- #if TARGET_OS_IPHONE
-
-
-
-
-
-
-
- int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
- BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
- alphaInfo == kCGImageAlphaNoneSkipFirst ||
- alphaInfo == kCGImageAlphaNoneSkipLast);
-
-
-
- BOOL imageIsPng = hasAlpha;
-
-
-
- if ([imageData length] >= [kPNGSignatureData length]) {
- imageIsPng = ImageDataHasPNGPreffix(imageData);
- }
-
- if (imageIsPng) {
- data = UIImagePNGRepresentation(image);
- }
- else {
- data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
- }
- #else
- data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
- #endif
- }
-
- [self storeImageDataToDisk:data forKey:key];
- });
- }
- }
我滴个妈妈呀,又是CG框架下的API,等我有空了再来和你一战!!!
我们来看看磁盘缓存
-
- - (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
-
- if (!imageData) {
- return;
- }
-
-
-
- if (![_fileManager fileExistsAtPath:_diskCachePath]) {
- [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
- }
-
-
-
-
-
- NSString *cachePathForKey = [self defaultCachePathForKey:key];
-
- NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
-
-
- [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
-
-
- if (self.shouldDisableiCloud) {
- [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
- }
- }
7.第七步(SDWebImageCache的清理缓存策略)
在初始化的时候注册了几个通知
-
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(clearMemory)
- name:UIApplicationDidReceiveMemoryWarningNotification
- object:nil];
-
-
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(cleanDisk)
- name:UIApplicationWillTerminateNotification
- object:nil];
-
-
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(backgroundCleanDisk)
- name:UIApplicationDidEnterBackgroundNotification
- object:nil];
1.当收到内存警告时,直接调用NSCache的removeAllObject的方法来清理MemeryCache
2.当程序退出时或进入后台,根据缓存策略来清理磁盘缓存
-
-
-
-
-
- - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
- dispatch_async(self.ioQueue, ^{
-
-
- NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
-
- NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
-
-
-
- NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
- includingPropertiesForKeys:resourceKeys
- options:NSDirectoryEnumerationSkipsHiddenFiles
- errorHandler:NULL];
-
-
- NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
- NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
- NSUInteger currentCacheSize = 0;
-
-
-
-
-
-
- NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
- for (NSURL *fileURL in fileEnumerator) {
-
- NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
-
-
-
- if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
- continue;
- }
-
-
-
- NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
-
- if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
- [urlsToDelete addObject:fileURL];
- continue;
- }
-
-
-
- NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
- currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
-
- [cacheFiles setObject:resourceValues forKey:fileURL];
- }
-
- for (NSURL *fileURL in urlsToDelete) {
- [_fileManager removeItemAtURL:fileURL error:nil];
- }
-
-
-
-
- if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
-
-
- const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
-
-
-
- NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
- usingComparator:^NSComparisonResult(id obj1, id obj2) {
- return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
- }];
-
-
-
- for (NSURL *fileURL in sortedFiles) {
-
-
- if ([_fileManager removeItemAtURL:fileURL error:nil]) {
- NSDictionary *resourceValues = cacheFiles[fileURL];
- NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
- currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
-
- if (currentCacheSize < desiredCacheSize) {
- break;
- }
- }
- }
- }
- if (completionBlock) {
- dispatch_async(dispatch_get_main_queue(), ^{
- completionBlock();
- });
- }
- });
- }
3.穿插一个clearDisk的方法,这个就不是什么策略了,直接是完全清掉磁盘缓存 clear 和 clean的区别吧
-
-
- - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
- {
-
- dispatch_async(self.ioQueue, ^{
-
- [_fileManager removeItemAtPath:self.diskCachePath error:nil];
- [_fileManager createDirectoryAtPath:self.diskCachePath
- withIntermediateDirectories:YES
- attributes:nil
- error:NULL];
-
- if (completion) {
- dispatch_async(dispatch_get_main_queue(), ^{
- completion();
- });
- }
- });
- }
那么到这里,基本的流程和主要技术点就走完了,我觉得已经注释的很详细了,还是整理下知识点给大家
知识点整理:
1.这里的Dispatch_barrier_async来确保线程安全操作,任务需要等待之前的任务执行完
2.强大的Block回调和Associated Objects — runtime
3.NSOperationQueue和NSOperation的,相对于GCD来说,他能取消操作,也能设置任务之间的依赖,相较于GCD来说更加的强大,AF也是基于这个玩的,这样才可以完成LIFO的队列需求
4.NSCache和NSFileManager的二级缓存操作
5.缓存策略:超过一星期的杀掉,还能设置最大缓存数,根据文件的最后编辑时间进行升序,然后一个个删除,低于临界的时候清理完成
6.异步操作图片的处理,缩放和解压操作,还有图片的类型处理,这东西有空再研究,很少接触
7.还有就是同一URL不会被下载的优化处理
8.最后还是觉得封装和任务的分配都非常的清晰,值得学习
超级清晰的个人总结,如有不对,请指出,才看了一遍,还得多看几遍才能消化一点知识
1.入口UIImageView调用方法sd_setImageWithURL: placeholderImage: options: progress:completed:,无论你调用哪个,最终都转换成该方法处理url
2.必须先删除该控件之前的下载任务,sd_cancelCurrentImageLoad原因是当你网络不快的情况下,例如你一个屏幕能展示三个cell,第一个cell由于网络问题不能立刻下完,那么用户就滑动了tbv,第一个cell进去复用池,第五个出来的cell从复用池子拿,由于之前的下载还在,本来是应该显示第五个图片,但是SD的默认做法是立马把下载好的图片给UIImageView,所以这时候会图片数据错乱,BUG
3.有placeHolder先展示,然后启用SDWebImageManager单例downloadImageWithURLoptions:progress:completed:来处理图片下载
4.先判断url的合法性,再创建SDwebImageCombinedOperation的cache任务对象,再查看url是否之前下载失败过,最后如果url为nil,则直接返回操作对象完成回调,如果都正常,那么就调用SDWebImageManager中的管理缓存类SDImageCache单例的方法queryDiskCacheForKey:done:查看是否有缓存
5.SDImageCache内存缓存用的是NSCache,Disk缓存用的是NSFileManager的文件写入操作,那么查看缓存的时候是先去内存查找,这里的key都是经过MD5之后的字串,找到直接回调,没找到继续去磁盘查找,开异步串行队列去找,避免卡死主线程,启用autoreleasepool避免内存暴涨,查到了缓存到内存,然后回调
6.如果都没找到,就调用SDWebImageManager中的管理下载类SDWebImageDownloader单例
downloadImageWithURL:options:progress:completed:completedBlock处理下载
7.下载前调用addProgressCallback:completedBlock:forURL:createCallback:来保证统一url只会生成一个网络下载对象,多余的都只会用URLCallbacks存储传入的进度Block或者CompleteBlock,因此下载结果返回的时候会进行遍历回调
8.下载用NSOperation和NSOperationQueue来进行,SD派生了一个SDWebImageDownloaderOperation负责图片的下载任务,调用
initWithRequest:inSession:options:progress:completed:cancelled:
9.把返回的SDWebImageDownloaderOperation对象add到NSOperationQueue,FIFO队列就正常,如果是LIFO队列,就需要设置依赖,这也是GCD和NSOperation的区别,也是NSOperation的优点,让上一次的任务依赖于本次任务[wself.lastAddedOperationaddDependency:operation]
10.下载任务开始是用NSURLSession了,不用NSURLConnetion了,由于SD是自定义的NSOperation
内部需要重写start方法,在该方法里面配置Session,当taskResume的时候,根据设置的代理就能取到不同的回调参数
didReceiveResponse能获取到响应的所有参数规格,例如总size
didReceiveData是一步步获取data,压缩解码回调progressBlock
didCompleteWithError全部完成回调,图片解码,回调completeBlock
11.图片的解码是在SDWebImageDecoder里面完成的,缩放操作是在SDWebImageCompat内完成的,代理方法里面本身就已经是异步了,而且解码操作加入了autoreleasepool减少内存峰值
这方面知识还是需要进一步去了解,不是图片压缩解码不是很懂
12.当在SDWebImageDownloaderOperation中NSURLSession完成下载之后或者中途回调到SDWebImageDownloader中,然后再回调到SDWebImageManager,在Manager中二级缓存image,然后继续回调出去到UIImage + WebCache中,最后把Image回调出去,在调用的控件中展示出来
13.SDImageCache初始化的时候注册了几个通知,当内存警告的时候,程序进入后台或者程序杀死的时候根据策略清理缓存
内存警告:自动清除NSCache内存缓存
进入后台和程序杀死:清理过期的文件(默认一周),然后有个缓存期望值,对比已有文件的大小,先根据文件最后编辑时间升序排,把大于期望值大小的文件全部杀掉
14.SDWebImagePrefetcher这货还提供了预先下载图片,还没使用过
我自己看了一遍,关于SDWebImage的面试题就全部迎刃而解了
点击打开链接 点击打开链接2
最后还要感谢南峰子的知识点讲解,大神果然厉害,向您学习
南峰子传送门