SDWebImage分析--源代码详细分析

阳英朗
2023-12-01

SDWebImage源代码分析

前言

关于网上的源代码分析也应该是不少的了,不过对于这个经典的第三方图片下载缓存库的作者还是相当敬佩的。这里还是想就个人理解来分析下,当做笔记加深理解也好。想看大概流程就好的可以看我上一篇博客:传送门:SDWebImage分析–库处理流程分析

一、UIImage + WebCache 入口:

我们根据设置Image的时候跳转代码到定义位置其实可以看到几乎所有类型的设置情况都是指向一个函数,只不过是根据你需要的类型作者自行帮你加入了默认项:

[self sd_setImageWithURL:url 
        placeholderImage:placeholder 
                options:options 
               progress:nil 
              completed:completedBlock];

打开这个方法的定义代码:

- (void)sd_setImageWithURL:(NSURL *)url 
          placeholderImage:(UIImage *)placeholder 
                   options:(SDWebImageOptions)options 
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock 
                 completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_cancelCurrentImageLoad];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }

    if (url) {
        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url 
             options:options 
            progress:progressBlock 
           completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            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(^{
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
            if (completedBlock) {
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

这里代码挺长的。所以我们就按重点来分析。第一句的[self sd_cancelCurrentImageLoad]; ,我们跳转到该代码来看:

    // Cancel in progress downloader from queue
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey:key];
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }

按照这个代码分析应该是在下载的时候取消当前所有的相同下载队列。但这里我有个问题:如果有某个图片下载还没完成又有同样的下载请求进来就会被取消,这样循环的话这个图片不就没办法被下载。或许分析到最后我们能解决这个疑惑,继续看接下来的代码。
第二行: objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

诶这个函数挺陌生的。我查了一下资料:
category与associative作为objective-c的扩展机制的两个特性:
category即类型,可以通过它来扩展方法;associative,可以通过它来扩展属性。
objc_getAssociatedObject、objc_setAssociatedObject、objc_removeAssociatedObjects都是Obj-c中的外联方法:
object 参数作为待扩展的对象实例,key作为该对象实例的属性的键,而value就是对象实例的属性的值,policy作为关联的策略。
plicy对应的枚举策略有:
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
看了有基础的也就应该大概懂这是什么意思了。这里就拓展下具体就不多说了。
其实这个用的也算是比较少的东西了。我觉得Category跟Protocol已经可以实现大部分的需求了。
不过这样看这个也好像挺有用处的。这里就当做顺便增加下知识储备了。

第三句:

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }
这里有必要看一下SDWebImageOptions这个枚举值都是什么东西,我尽可能根据我很普通的英语来粗略翻译下了:
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    //下载失败则重试,默认是当一个URL下载失败后会被加入黑名单
    SDWebImageRetryFailed = 1 << 0,
    //在用户交互期间延迟下载
    SDWebImageLowPriority = 1 << 1,
    //只允许图片缓存在内存而不允许缓存在磁盘
    SDWebImageCacheMemoryOnly = 1 << 2,
    //采用边下载边显示,默认是下载完成再一次性显示
    SDWebImageProgressiveDownload = 1 << 3,
    //这个太长了,大概意思就是忽略缓存
    SDWebImageRefreshCached = 1 << 4,
    //在iOS4.0+允许应用在后台进行一些操作,这个选项就是允许在后台继续下载
    SDWebImageContinueInBackground = 1 << 5,
    //这个不会翻译。原文:
    //Handles cookies stored in NSHTTPCookieStore by setting
    //NSMutableURLRequest.HTTPShouldHandleCookies = YES;
    SDWebImageHandleCookies = 1 << 6,
    //允许不受信任的SSL证书。实际情况慎用
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,
    //使用高级别的线程权限,默认是等待当前线程完成再进行
    SDWebImageHighPriority = 1 << 8,
    //等待图片下载完成后再显示预加载图片(不明白这个设置有多大用途)
    SDWebImageDelayPlaceholder = 1 << 9,
    SDWebImageTransformAnimatedImage = 1 << 10,
    //下载完成后手动设置图片,默认是下载完成后自动放到ImageView上
    SDWebImageAvoidAutoSetImage = 1 << 11
};

接下来的代码片:

id <SDWebImageOperation> operation = 
            [SDWebImageManager.sharedManager downloadImageWithURL:url 
                                                          options:options 
                                                         progress:progressBlock 
                                                        completed:^(UIImage *image, NSError *error, 
                                                        SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) 

这里便是共享一个SDWebImageManager来进行图片的缓存查找或者下载了。并将这个operation加入到我们在看第一句的函数中那个可变的操作队列字典中。好的,这里就到了最复杂的地方了,让我们跳转到这个manager的函数:

二、委托到SDWebImageManager进行查找缓存下载图片:
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

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

先检查下URL有没有在黑名单里面,然后根据URL获得的一个Key去磁盘查找是否存在这个图片

NSString *key = [self cacheKeyForURL:url];

    operation.cacheOperation = 
                [self.imageCache queryDiskCacheForKey:key 
                                                 done:^(UIImage *image, SDImageCacheType cacheType)

根据这个函数跳转到其实现的地方:

三、通过SDWebImageCache查找缓存:
// 核心代码。
    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) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

可以看到这里是先用传递过来的Key先进行内存缓存查找是否存在这个图片。如果查找成功则返回图片去显示。如果没有,那么这里是新开一个NSOperation的串行队列去磁盘上查找(这里是把io操作用异步线程推到后台,不会干扰到主线程),这里还有个小细节就是作者在这里新建了一个自动释放池,把查找操作放在里面,等查找操作完成后自动销毁产生的额外数据。至于为什么要有这个必要后面再来细致研究下。追踪 [self diskImageForKey:key] 这个函数发现了它的图片存储路径函数为:

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
                                                    r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]];

    return filename;
}

有做过安全加密之类算法的一看大概就知道这个是什么了。这就是将文件名进行MD5加密,在信息传输方面也很广泛。对URL进行MD5转换之后就能得到一个唯一的128位长的字符串。对URL进行MD5转换有什么好处呢,可能你觉得每个图片的URL应该也是确定的啊,但每个URL的长度都不一样吧,有的特别长也浪费空间,我想得不是很深,但这样做应该也有便于统一存取的角度吧。然后接下来的工作就是如果从内存照到图片数据则返回,返回的是一个NSData类型,SDImageCache会先对data进行解压处理成相应的图片格式。获得图片的NSData之后这里有一个小片段具体是我还不甚理解的,应该是涉及到图片的解压方面。

        UIImage *image = [UIImage sd_imageWithData:data];
        image = [self scaledImageForKey:key image:image];
        if (self.shouldDecompressImages) {
            image = [UIImage decodedImageWithImage:image];
        }
        return image;

从磁盘找到图片后则将图片加入到内存缓存之中备用,并通过block返回找到的Image。这里有个函数可以注意下:

 [self.memCache setObject:diskImage forKey:key cost:cost]; 
 这个函数是NSCache中提供的缓存存储函数。有没有感觉很熟悉?
 这让我想到KVO模式中常用到的setValue。

上述查找过程完毕返回结果后,执行以下代码:

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

即是如果上述操作都找不到缓存图片,则通知实现了SDWebImageManagerDelegate 协议的对象根据URL进行图片下载。如果委托没有响应,则进行以下代码到进行图片下载:

四、新建或共享一个SDWebImageDownloader下载图片:
 id <SDWebImageOperation> subOperation = 
                 [self.imageDownloader downloadImageWithURL:url 
                                                    options:downloaderOptions
                                                   progress:progressBlock 
                                                  completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)

进入这个函数的实现过程可以看到一个个人觉得挺好的函数,作者把下载过程都放在这个函数的实现回调中:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock 
          andCompletedBlock:(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;
        }

        // Handle single download of simultaneous download request for the same URL
        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();
        }
    });
}

URLCallbacks是一个包含每个图片多组回调信息的字典,key是图片的URL地址,value则是一个数组。由于允许多个图片同时下载,因此可能会有多个线程同时操作URLCallbacks属性。为了保证URLCallbacks操作(添加、删除)的线程安全性,SDWebImageDownloader将这些操作作为一个个任务放到barrierQueue队列中。哦对了,上面提到的NSCache缓存存储步骤因为是线程安全的,所以在存取查找的时候不需要进行额外的维护。

void dispatch_barrier_async( dispatch_queue_t queue, dispatch_block_t block);
//这个函数可以设置同步执行的block,在它之后加入队列的block,则等到这个block执行完毕后才开始执行。

而图片的下载根据头文件的定义有两种方式:

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    //先进先出,即队列模式(默认属性)
    SDWebImageDownloaderFIFOExecutionOrder,
    //先进后出,即栈模式
    SDWebImageDownloaderLIFOExecutionOrder
};

下载部分则是使用NSURLConnetion来进行了。每一个图片的下载都是在一个独立队列中,从而实现并发下载。这一部分就不细说了,有兴趣的可以自己专研下源代码。在github上搜索SDWebImage即可找到原作者的源码。

SDWebImage之中还编写了清除缓存的方法,如果应用收到内存警告则会自动删除内存缓存的图片(按时间先后顺序)。该库也提供了很多方法供我们去查询与删除图片缓存,其机制也是大同小异,主要实现方法也应该是跟上述讲到的查找缓存的方法类似。具体方法主要有:
//查找
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
//移除
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
//清理磁盘
- (void)clearDisk;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
小结

看到这谨密细致的代码暂时真是只有膜拜的份。

 类似资料: