一 题外话

  之前写过一篇最新版SDWebImage的使用,也简单的介绍了一下原理。这两天正梳理自己的知识网络,觉得有必要再阅读一下源码,一是看具体实现,二是学习一下优秀开源代码的代码风格,比如接口设计,设计模式,变量命名等等。

  既然是第一篇,就要制定一个阅读源码的计划,以什么顺序阅读完全部代码。我们从最常见的入口切入sd_setImageWithURL,一路下去,最后再阅读没有设计到的部分。

  在开始之前强烈建议先去读我之前的文章:最新版SDWebImage的使用。心里有个大概再去探讨细节,效果更佳。

二 入口

  我们为什么使用SDWebImage,是因为他帮我们实现了图片的二级缓存,使我们加载图片更流畅。当然你也可以使用SDWebImage中几个很棒的工具类,比如SDWebImageDownloader,用来下载图片。或者SDImageCache用来缓存图片或者NSData。我们先来看看UIImageView+WebCache中的基本方法:

  在UIImageView+WebCache类的最上面,很贴心的贴了一个使用例子,这也是我们很常见的tableViewCell加载图片的场景

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = @"MyIdentifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier]; if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier]
autorelease];
} // Here we use the provided sd_setImageWithURL: method to load the web image
// Ensure you use a placeholder image otherwise cells will be initialized with no image
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://example.com/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder"]]; cell.textLabel.text = @"My Text";
return cell;
}

而sd_setImageWithURL: placeholderImage:,也是我们最常使用的方法,我们看看除了这个外的其他方法:

//最基本方法
- (void)sd_setImageWithURL:(NSURL *)url; //带placeholder,优先显示placeholder,下载完成后显示原图
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; //多了缓存策略options
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options; //多了下载完成后的block回调
- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock; //多了下载完成后的block回调
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock; //多了下载完成后的block回调
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock; //多了下载过程progressBlock回调
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock; //多了下载过程progressBlock回调
- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

这个接口设计很经典,也是看了这个接口设计,回头重构了自己项目中社交分享模块的接口。而下面这个方法,也可以看做是外观模式的具体体现。

- (void)sd_setImageWithURL:(NSURL *)url;

废话少说,进去看看实现。可以看到,所有方法都指向

- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)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) { // check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
} __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) {
[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:- userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}

可以看到第一步是取消当前下载,本着不放过任何一个细节的精神,我们看看是怎么取消当前下载的

- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// 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];
}
}

  我们看到,operationDictionary就是多个线程的集合。在SDWebImageManager的downloadImageWithURL方法中创建operation并返回,保存在operationDictionary中。然后我们从operationDictionary中通过key "UIImageViewImageLoad" 取出负责下载的所有operation,然后cancel掉。并将下载的所有operation从operationDictionary中移除。特别值得注意的是,当前类是UIImageView的category,我们知道,category不能增加属性,只能增加方法,那么operationDictionary是哪里来的呢。答案是:objc_setAssociatedObject,对象关联,动态的给UIImageView添加新属性。在SDWebImage中有很多这种用法,看到你就要知道,这就是动态增加了属性。

  继续看如何cancel,SDWebImageOperation是一个协议,而SDWebImageDownloaderOperation实现了这个协议。我们上面说的从operationDictionary中通过key "UIImageViewImageLoad" 取出负责下载的所有queue,其实就是SDWebImageDownloaderOperation的实例的集合。熟悉NSOperation的都知道,SDWebImageDownloaderOperation继承于NSOperation,每一个SDWebImageDownloaderOperation的实例都是一个新线程。NSOperation其实自己也抽象了cancel方法。- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key方法中,[operation cancel];这里的operation其实是SDWebImageManager的

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url

                                         options:(SDWebImageOptions)options

                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock

                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

这个方法中创建的SDWebImageCombinedOperation实例,

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

再看看SDWebImageCombinedOperation的定义和实现

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation; @end @implementation SDWebImageCombinedOperation - (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
// check if the operation is already cancelled, then we just call the cancelBlock
if (self.isCancelled) {
if (cancelBlock) {
cancelBlock();
}
_cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
} else {
_cancelBlock = [cancelBlock copy];
}
} - (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;
}
} @end

我们看到,调用该operation的cancel方法,其实是执行cancelBlock,我们就看看,他的cancelBlock传入的是什么东西。在SDWebImageManager的downloadImageWithURL方法中,我们找到了赋值的地方

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

我们看到block里面调用了[subOperation cancel];而这个subOperation,是SDWebImageDownloader的downloadImageWithURL方法创建并返回的SDWebImageDownloaderOperation对象,它是NSOperation的子类。终于到了我们熟悉的对象。究其原因,cancle的时候其实就是SDWebImageDownloaderOperation的实例cancel,具体实现如下:

- (void)cancel {
@synchronized (self) {
if (self.thread) {
[self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
else {
[self cancelInternal];
}
}
}
- (void)cancelInternalAndStop {
if (self.isFinished) return;
[self cancelInternal];
} - (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.cancelBlock) self.cancelBlock(); if (self.dataTask) {
[self.dataTask cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
}); // As we cancelled the connection, its callback won't be called and thus won't
// maintain the isFinished and isExecuting flags.
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
} [self reset];
}

最重要的就是这句:[self.dataTask cancel]。self.dataTask是NSURLSessionTask的实例,这里就是取消网络请求。说这么多,仅仅是取消了下载图片的网络请求。

已经写了不少了,把大头戏放到下一篇。下一篇我们主要分析二级缓存的实现和SDWebImageDownloader的异步加载。

SDWebImage源码阅读-第一篇的更多相关文章

  1. Flask源码阅读-第一篇(flask包下的__main__.py)

    源码: # -*- coding: utf-8 -*-""" flask.__main__ ~~~~~~~~~~~~~~ Alias for flask.run for ...

  2. SDWebImage源码阅读-第二篇

    一  SDWebImageManager的downloadImageWithURL的方法 上一篇,我们刚开了个头,分析了一下开始加载图片之前如何取消其他正在下载的任务,接着,我们回到 - (void) ...

  3. SDWebImage源码阅读-第三篇

    这一篇讲讲不常用的一些方法. 1 sd_setImageWithPreviousCachedImageWithURL: placeholderImage: options: progress: com ...

  4. 【原】SDWebImage源码阅读(二)

    [原]SDWebImage源码阅读(二) 本文转载请注明出处 —— polobymulberry-博客园 1. 解决上一篇遗留的坑 上一篇中对sd_setImageWithURL函数简单分析了一下,还 ...

  5. 【原】SDWebImage源码阅读(五)

    [原]SDWebImage源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 前面的代码并没有特意去讲SDWebImage的缓存机制,主要是想单独开一章节专门讲 ...

  6. 【原】SDWebImage源码阅读(四)

    [原]SDWebImage源码阅读(四) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 SDWebImage中主要实现了NSURLConnectionDataDelega ...

  7. 【原】SDWebImage源码阅读(三)

    [原]SDWebImage源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1.SDWebImageDownloader中的downloadImageWithURL 我们 ...

  8. 【原】SDWebImage源码阅读(一)

    [原]SDWebImage源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 一直没有系统地读过整套源码,就感觉像一直看零碎的知识点,没有系统读过一本专业经典书 ...

  9. SDWebImage 源码阅读分享

    SDWebImage 源码阅读分享 疑问列表 SDWebImage 整体框架图,主要的类包含哪些 SDWebImage 如何进行缓存管理,过期失效策略,缓存更新 SDWebImage 如何多线程处理的 ...

随机推荐

  1. Entity Framework查询注意

    首先我们看下where的方法,直接查看定义(定义如下),其实一种是对IEnumerable的扩展,一种是对IQueryable的扩展,直接看最常用的,其实区别就在IEnumerable的扩展的参数是系 ...

  2. knockout中viewmodel跟子model相互调用

    用knockout写前端复杂js逻辑的确很方便,而且html界面也很清爽. 在ko中对于复杂的业务逻辑我会给viewmodel创建一些子model对象,但是viewmodel跟子model怎样相互调用 ...

  3. 商城项目实战 | 2.2 Android 仿京东商城——自定义 Toolbar (二)

    本文为菜鸟窝作者刘婷的连载."商城项目实战"系列来聊聊仿"京东淘宝的购物商城"如何实现. 上一篇文章<商城项目实战 | 2.1 Android 仿京东商城 ...

  4. (知识点)JavaScript原型和原型链

    〇 每个函数都拥有prototype属性,而该属性所储存的就是原型对象 1)原型属性—— 上面我们测试了foo()函数的 1) length属性(length属性除了可以用在数组中,还可以用于记录函数 ...

  5. 关于quotename的用法

    首先,sqlserver里的标识符有一定的规则,比如  你 create table abc 123(...) 那么中间含有空格,它不是符合规则的. 你会写做 create table [abc 12 ...

  6. C++随机数rand(), srand()

    c++产生随机数会用到rand(), srand()函数,下面总结两个函数特性和使用. 1. rand() #include <iostream> #include <cstdlib ...

  7. 【2017-05-03】winform打印控件、事件对象和事件数据、MDI窗体容器

    一.打印控件 第一步先把打印对象搞出来. - printDocument    打印对象(将要打印的内容放到该对象里,从该对象里取内容打印) 设置他的PrintPage事件(对于要打印的每一页触发一次 ...

  8. Java AOP (1) compile time weaving 【Java 切面编程 (1) 编译期织入】

    According to wikipedia  aspect-oriented programming (AOP) is a programming paradigm that aims to inc ...

  9. 如何使用MySQL触发器trigger

    阅读目录:触发器trigger的使用 创建触发器 单一执行语句.多执行语句 new.old详解 查看触发器 删除触发器:慎用触发器,不用就删除 Q:什么是触发器? A: 触发器是与表有关的数据库对象, ...

  10. redis之sentinel概述

    一.配置sentinel 修改的是这条: 对应: 上面那条配置需要注意:<master-name>:监控主节点的名称 <ip>:监控主节点的ip   <redis-por ...