zoukankan      html  css  js  c++  java
  • 【原】SDWebImage源码阅读(二)

    【原】SDWebImage源码阅读(二)

    本文转载请注明出处 —— polobymulberry-博客园

    1. 解决上一篇遗留的坑


    上一篇中对sd_setImageWithURL函数简单分析了一下,还留了一些坑。不过因为我们现在对这个函数有一个大概框架了,我们就按顺序一个个来解决。

    首先是这一句代码:

    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    就是给UIImageView当前这个对象添加一个NSString的关联对象url。相当于现在这个图片的url属性绑定到了UIImageView对象上。如果对这个函数有疑问,请移步我的这篇博客

    下面简单的部分我就不说了,直接跳到

    if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
    {
        completedBlock(image, error, cacheType, url);
        return;
    }

    首先是SDWebImageAvoidAutoSetImage,我们看看它的注释

    /**
     * By default, image is added to the imageView after download. But in some cases, we want to
     * have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
     * Use this flag if you want to manually set the image in the completion when success
     */

    翻译过来就是说,默认情况下是等image完全从网络端下载完后,就会直接将结果设置到UIImageView。但是有些人想在获取到图片后,对图片做一些处理,比如使用filter去渲染图片或者给图片加个cross-fade animation(淡出动画)显示出来。那你就设置这个选项。然后得手动去处理图片下载完成后的事情。

    上面说了要手动处理了,很自然你就会想到,这个手动处理就是compeletedBlock啊!当然,除了有这个枚举选项时需要手动处理,其实只要你自定义了compeletedBlock,都会调用你自定义处理的函数。你说我怎么知道的?你看下面的代码,如果你自定义了下载完成后的处理方式,并且也确实下载完成了(finished为YES),就执行自定义方式:

    if (completedBlock && finished) {
        completedBlock(image, error, cacheType, url);
    }

    最后还剩下一个情况,就是url不存在的情况(注意上面讲的是if(url){…},下面讲的是在else{…}):

    dispatch_main_async_safe(^{
        [self removeActivityIndicator];
        NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
        if (completedBlock) {
            completedBlock(nil, error, SDImageCacheTypeNone, url);
        }
    });

    首先自定义一个NSError的对象,表示url为空的错误。然后传给compeletedBlock。


    知识点:NSError构造方法errorWithDomain

    + (instancetype)errorWithDomain:(NSString *)domain code:(NSInteger)code userInfo:(nullable NSDictionary *)dict;

    具体细节可以移步我的这篇博客


    其实目前来说,我心中还有两个最大的疑惑,一个就是operation怎么执行的一个就是如何自定义compeletedBlock

    2. operation执行过程

    我们可以看到这里downloadImageWithURL其实是SDWebImageManager的方法

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

    我们进去该函数实现,快两百行的代码了。好吧,先歇着,我们看看注释(我直接贴出我翻译过后的注释)。

    /**
     * 如果图片不在缓存中,根据指定的URL下载图片,否则使用缓存中的图片。.
     *
     * @param url            图片的URL
     * @param options        该请求所要遵循的选项。(前面已经介绍了两个)
     * @param progressBlock  当图片正在下载时调用该block。
     * @param completedBlock 当操作完成后调用该block。
     *
     *   该参数是必须的。(指的是completedBlock)
     * 
     *   该block没有返回值并且用请求的UIImage作为第一个参数。
     *   如果请求出错,那么image参数为nil,而第二参数将包含一个NSError对象。
     *
     *   第三个参数是'SDImageCacheType'枚举,表明该图片重新获取方式是从本地缓存(硬盘)或者
     *   从内存缓存,还是从网络端重新获取image一遍。.
     *
     *   当options设为SDWebImageProgressiveDownload并且此时图片正在下载,finished将设为NO
     *   因此这个block会不停地调用直到图片下载完成,此时才会设置finished为YES.
     *
     * @return 返回一个遵循SDWebImageOperation协议的NSObject. 应该是一个SDWebImageDownloaderOperation的实例
     */

    上面的注释有几个不认识的概念。一个是SDImageCacheType,另一个是options中的SDWebImageProgressiveDownload,还有一个SDWebImageDownloaderOperation

    2.1 SDImageCacheType


    typedef NS_ENUM(NSInteger, SDImageCacheType) {
        /**
         * 该图片无法从SDWebImage的缓存中获取,必须从web端下载。
         */
        SDImageCacheTypeNone,
        /**
         * 图片从硬盘缓存(disk cache)中获取
         */
        SDImageCacheTypeDisk,
        /**
         * 图片从硬盘缓存(disk cache)中获取
         */
        SDImageCacheTypeMemory
    };

    具体缓存实现方式我放在SDWebImage源码阅读(五)了。

    2.2 SDWebImageProgressiveDownload


    如果在加载图片中设定了该选项,那么图片会随着下载的进度一点点地显示出来。缺省情况下,图片是下载完成后一次显示出来的。

    2.3 SDWebImageDownloaderOperation


    看到这个类,我内心是愉快的。之前我不是说这个opertion应该和NSOperation有些关系吗?这个类就是NSOperation的子类啊,并且遵循SDWebImageOperation协议。这下SDWebImageDownloaderOperation将NSOperation和SDWebImageOperation联系在了一起,我们可以看下它的声明:

    @interface SDWebImageDownloaderOperation : NSOperation <SDWebImageOperation>

    所以不用说,这个类一定是个重头戏。

    但是我们搜索SDWebImageDownloaderOperation,发现SDWebImageManager中的downloadImageWithURL函数并没有返回SDWebImageDownloaderOperation。这一点很让人疑惑。不过我们发现SDWebImageDownloaderOperation遵循SDWebImageOperation协议,会不会和downloadImageWithURL的返回值id<SDWebImageOperation>有关系?而且看到这,我有些迷糊了。函数返回值为id<protocol>,这是什么返回值?什么时候需要这样用?感觉源码阅读进行不下去了。。。先不急,既然注释说返回的是SDWebImageDownloaderOperation,那么肯定就是啦。

    我先在所有工程中搜索SDWebImageDownloaderOperation,发现在SDWebImageDownloader中也有一个downloadImageWithURL函数。而且里面就定义了一个opertion,这个opertion就是SDWebImageDownloaderOperation 类型,并且这个函数也是返回operation的。

    __block SDWebImageDownloaderOperation *operation;

    回到我们的SDWebImageManager中的downloadImageWithURL函数中,搜索downloadImageWithURL,找找看是不是有蛛丝马迹。果然,下面这段代码:

    id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)

    此处的downloadImageWithURL是SDWebImageDownloader的一个方法。

    好,但此为止,我们也只是觉得上面那些东西有联系,但是联系并不是很清晰。而且已经有了一个downloadImageWithURL,还要弄一个干什么?

    太多问题了,我现在也只能大概猜测到底怎么回事了,在继续探索之前,我们先整理这里面的关系:

    QQ20151226-0@2x

    上面文字部分,我用红色标注了两个问题,我们先解决第一个


    问题一:函数返回值为id<protocol>,这是什么返回值?

    其实返回的是一个id类型,只是这个id类型一定要遵循里面的protocol,比如id<SDWebImageOperation>,那么因为SDWebImageDownloaderOperation遵循SDWebImageOperation协议,所以可以作为返回类型。



    问题二:已经有了一个downloadImageWithURL,还要弄一个干什么?

    这个说实话,我也不是很清楚,只能找这两个函数之间的关联了。其实更准确地说是找SDWebImageManager中downloadImageWithURL中的subOperation(SDWebImageDownloaderOperation)和operation的关系。根据这个思路,我发现了subOperation只在这个函数里面出现了两次。第一次是定义的地方,第二次就是:

    operation.cancelBlock = ^{
        [subOperation cancel];
                    
        @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:weakOperation];
        }
    };

    无语,你辛辛苦苦弄了个subOperation,结果就亮了个像,还是cancel,就没了。太没人性了。不过是细想其实是有原因的,我在SDWebImageDownloaderOperation的downloadImageWithURL函数注释中找到了答案:

    @return A cancellable SDWebImageOperation

    一个cancellable的SDWebImageOperation,是不是和这里只用了cancel对应上了。虽然找到了点联系,不过还是流于表面,这与为什么这么做,这么做的理由还不是很清楚。

    我们还是细细分析cancelBlock那段代码

    首先是一个block,operation.cancelBlock有一个对应的setCancelBlock函数:

    - (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
        // 检测self(是一个SDWebImageCombinedOperation类型的operation)是否取消了,如果取消了,就执行对应的cancelBlock函数。
        if (self.isCancelled) {
            if (cancelBlock) {
                cancelBlock();
            }
            _cancelBlock = nil; //不要忘了置cancelBlock为nil,否则会crash
        } else {
            _cancelBlock = [cancelBlock copy];
        }
    }

    也就是说operation如果取消了,那么就会执行subOperation的cancel函数。并且从runningOperations中移除该operation,因为是block,为了避免循环引用,所以使用了weakOperation。runningOperations大概从名字也能猜到,用来存储正在运行的operation。既然当前operation被取消了,肯定要从runningOperations移除的嘛!

    注意此处的operation的类型是SDWebImageCombinedOperation,具体定义如下:

    // 遵循SDWebImageOperation
    @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
    
    @property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
    // 注意SDWebImageCombinedOperation遵循SDWebImageOperation,所以实现了cancel方法
    // 在cancel方法中,主要是调用了cancelBlock,这个设计很值得琢磨
    @property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
    // 根据cacheType获取到image,这里虽然名字用的是cache,但是如果cache没有获取到图片
    // 还是要把image下载下来的。此处只是把通过cache获取image和通过download获取image封装起来
    @property (strong, nonatomic) NSOperation *cacheOperation;
    
    @end

    还有一个问题就是@synchronized是什么?

    知识点:@synchronized

    避免多个线程执行同一段代码,主要防止当前operation会被多次remove,从而造成crash。这里括号内的self.runningOperations是用作互斥信号量。 即此时其他线程不能修改self.runningOperations中的属性。


    虽然看懂了这段代码,可是后面不知道该看什么了。

    所以我还是从头看这段代码(SDWebImageManager中downloadImageWithURL),看能不能找到点什么头绪:

    // 如果调用此方法,而没有传completedBlock,那将是无意义的
        NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    不要将completedBlock参数设为nil,因为这样做是毫无意义的。如果你是想使用downloadImageWithURL来预先获取image,那就应该使用[SDWebImagePrefetcher prefetchURLs],而不是直接调用SDWebImageManager中的downloadImageWithURL函数。

    // 使用NSString对象而非NSURL作为url是常见的错误. 因为某些奇怪的原因,Xcode不会报任何类型不匹配的警告,这里允许传NSString对象给URL。
        if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
    
        // 防止传了一个NSNull值给NSURL
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }

    我觉得此处对于细节地处理很值得学习。

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    这个没啥好说的,避免循环引用,使用了weak。

        BOOL isFailedUrl = NO;
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    
        if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
            dispatch_main_sync_safe(^{
                NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
                completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
            });
            return operation;
        }

    这个failedURLs从字面上理解就是一组下载失败的图片URL。所以这段代码也很好理解,就是如果这个图片url无法下载,那就使用completedBlock进行错误处理。那什么情况下算这个图片url无法下载呢?第一种情况是该url为空,另一种情况就是如果是failedUrl也无法下载,但是要避免无法下载就放入failedUrl的情况,就要设置options为SDWebImageRetryFailed。一般默认image无法下载,这个url就会加入黑名单,但是设置了SDWebImageRetryFailed会禁止添加到黑名单,不停重新下载。

    如果该url可以下载,那么就添加一个新的operation到runningOperations中。

    @synchronized (self.runningOperations) {
            [self.runningOperations addObject:operation];
        }

    剩下的100多行就是为了生成一个cacheOperation。那它到底是何方神圣?它是一个NSOperation,所以加入NSOperationQueue会自动执行。不过我还是全局搜索cacheOperation,发现它在SDWebImageCombinedOperation中的cancel方法中调用了:

    - (void)cancel {
        self.cancelled = YES;
        if (self.cacheOperation) {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        if (self.cancelBlock) {
            self.cancelBlock();
            
            // TODO: this is a temporary fix to #809.
            // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
    //        self.cancelBlock = nil;
            _cancelBlock = nil;
        }
    }

    还记得SDWebImageCombinedOperation遵循SDWebImageOperation协议吗?这就是SDWebImage实现的cancel。而cacheOperation是NSOperation,所以调用自身的cancel。注意是在这才会设置cancelled设为YES。

    好,现在回来看这个queryDiskCacheForKey函数。在此之前,先看上面有段代码,用图片的url来获取cache对应的key,也就是说cache中如果已经有了该图片,那就返回该图片在cache中对应的key,你可以根据这个key去cache中获取图片。

    NSString *key = [self cacheKeyForURL:url];

    获取到key后,你就可以使用queryDiskCacheForKey函数去查找了:

    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
        // 如果doneBlock不存在。那么就return nil。这个处理和downloadImageWithURL的completedBlock很类似
        if (!doneBlock) {
            return nil;
        }
        // 如果key为nil,说明cache中没有该image。所以doneBlock中传入SDImageCacheTypeNone,表示cache中没有图片,要从网络重新获取。
        if (!key) {
            doneBlock(nil, SDImageCacheTypeNone);
            return nil;
        }
    
        // 如果key不为nil
        // 首先在内存cache中查找    
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        // 找到了,就传入SDImageCacheTypeMemory,说是在内存cache中获取的
        if (image) {
            doneBlock(image, SDImageCacheTypeMemory);
            return nil;
        }
    
        // 否则,说明图片就在磁盘cache中。
        NSOperation *operation = [NSOperation new];
        dispatch_async(self.ioQueue, ^{
            if (operation.isCancelled) {
                return;
            }
    
            @autoreleasepool {
                UIImage *diskImage = [self diskImageForKey:key];
                // 如果磁盘中得到了该image,并且还需要缓存到内存中,为了同步最新数据
                if (diskImage && self.shouldCacheImagesInMemory) {
                    // 后面细讲
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
                // 传入SDImageCacheTypeDisk,说明是从磁盘中获取的
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, SDImageCacheTypeDisk);
                });
            }
        });
    
        return operation;
    }

    其实这段代码如果不深究的话,也很容易理解的。我直接把说明写成注释在上面了。不过这里我还有个疑问,就是为啥operation要在查找硬盘缓存时,才创建了一个新的operation?这里我谈谈我的想法:因为“图片可以用”这个状态意味着图片必须在内存中了。图片在网络还是在硬盘,其实相对来说并没有本质区别,最后都是要加进内存的。所以这里就有一个加载到内存的过程,需要产生一个NSOperation,也就理所当然会发生cancel。我这也不是胡乱猜的,函数中有一个self.ioQueue。表明这是一个io序列(dispatch_queue_t)。

    这下再回到downloadImageWithURL里面剩下的代码,就会很轻松了。因为它无非就是要处理上面那几种cache情况嘛。

    我们还是一点点来看done^{}中的代码:

    if (operation.isCancelled) {
        @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:operation];
        }
    
        return;
    }

    这段代码简单,不解释了,随时判断该operation是否已经cancel了。

    下面又是一段巨长的代码,我们先看看if中表示什么:

    if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
        // ...
    }

    我们先看后面那个delegate方法:

    /**
     * 当image无法在缓存中找到,调用该函数控制该image的下载
     *
     * @param imageManager 当前的`SDWebImageManager`
     * @param imageURL     需要下载的image的URL
     *
     * @return 返回NO表示当图片缓存未命中,反而阻止图片下载。如果该函数没实现,相当于返回YES。
     */
    - (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

    这里要着重说明下此处return的含义。

    注意在if里面最后一组||表达式,使用了短路判断(A||B,只要A为真,就不用判断B了),也就是说,如果delegate没有实现上面那个函数,整个表达式就为真,相当于该函数返回了YES。如果delegate实现了该函数,那就执行该函数,并且判断该函数执行结果。如果函数返回NO,那么整个if表达式都为NO,那么当图片缓存未命中时,图片下载反而被阻止。

    目前我看的源码中并没有地方实现了该函数,所以就当if后半段恒为YES。我们主要还是看前面那个||表达式:

    (!image || options & SDWebImageRefreshCached)

    如果没有缓存到image,或者options中有SDWebImageRefreshCached选项,就执行if语句。现在我们深入看看if判断下的代码到底执行了什么,首先又是一个if语句:

    if (image && options & SDWebImageRefreshCached) {
        dispatch_main_sync_safe(^{
            // 如果图片在缓存中找到,但是options中有SDWebImageRefreshCached
    // 那么就尝试重新下载该图片,这样是NSURLCache有机会从服务器端刷新自身缓存。
            completedBlock(image, nil, cacheType, YES, url);
        });
    }

    下面的代码就表示开始要下载图片了。

    首先定义了一个SDWebImageDownloaderOptions枚举值downloaderOptions,并根据options来设置downloaderOptions。基本上SDWebImageOptions和SDWebImageDownloaderOptions是一一对应的。只需要注意最后一个选项SDWebImageRefreshCached,这个得先强制关闭ProgressiveDownload方式。那后面的SDWebImageDownloaderIgnoreCachedResponse是什么意思呢?可能会有这样的疑惑,不是已经从imageCache中获取到了image了吗?还要Ignore干啥?这里简单提下,后面会详解:因为SDWebImage有两种缓存方式,一个是SDImageCache,一个就是NSURLCache,所以知道为什么这个选项是Ignore了吧,因为已经从SDImageCache获取了image,就忽略NSURLCache了。

    if (image && options & SDWebImageRefreshCached) {
            // 相当于downloaderOptions =  downloaderOption & ~SDWebImageDownloaderProgressiveDownload);
            downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
            // 相当于 downloaderOptions = (downloaderOptions | SDWebImageDownloaderIgnoreCachedResponse);
            downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
    }

    然后生成了一个subOperation,这段代码也很长,我大致看了下SDWebImageDownloader中的downloadImageWithURL函数,感觉终于到了 “真正”下载的代码了。为什么这么说了?因为里面代码大部分都是iOS自带框架底层的代码了。总算到头了。不过这段代码我准备下一篇再看。

    直接跳出这个subOperation的赋值语句,来到对应的else if语句:

    else if (image) {
           // 从缓存中获取到了图片,而且不需要刷新缓存的
            // 直接执行completedBlock,其中error置为nil即可。
            dispatch_main_sync_safe(^{
                if (!weakOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
            });
            // 执行完后,说明图片获取成功,可以把当前这个operation溢移除了。
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
        else {
            // 又没有从缓存中获取到图片,shouldDownloadImageForURL又返回NO,不允许下载,悲催!
            // 所以completedBlock中image和error均传入nil。 
            dispatch_main_sync_safe(^{
                if (!weakOperation.isCancelled) {
                    completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
     }

    恩,SDWebImageManager中的downloadImageWithURL函数我们还剩下那个最精彩的SDWebImageDownloader中的downloadImageWithURL函数,留着下一篇阅读。

  • 相关阅读:
    [New Portal]Windows Azure Virtual Machine (13) 在本地使用Hyper-V制作虚拟机模板,并上传至Azure (3)
    [New Portal]Windows Azure Virtual Machine (12) 在本地使用Hyper-V制作虚拟机模板,并上传至Azure (2)
    [New Portal]Windows Azure Virtual Machine (11) 在本地使用Hyper-V制作虚拟机模板,并上传至Azure (1)
    [New Portal]Windows Azure Virtual Machine (10) 自定义Windows Azure Virtual Machine模板
    Android实现推送方式解决方案
    Android Push Notification实现信息推送使用
    自定义圆形的ProgressBar
    Android网络框架Volley
    Android控件系列之RadioButton&RadioGroup
    android CheckBox控件的定义及事件监听
  • 原文地址:https://www.cnblogs.com/polobymulberry/p/5012649.html
Copyright © 2011-2022 走看看