首先我 fork 了 SDWebImage 的源码,见 conintet/SDWebImage,这样在本文的连接中都是链到个人 fork 中,这么作的目的是防止未来 SDWebImage
代码发生变化致使本文的连接不许确。html
有关 SD (SDWebImage 简称为 SD) 的使用方式仍是得参考其 README 或者 wiki。本文只是阅读其源码的笔记。ios
最早分析的就是图片下载部分的代码,由于这是最核心的功能。git
由于 SD 在 UIImageView
上经过 Category
的方式增长了简单易用的 API,相似下面:github
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
复制代码
因而经过几步 Jump to Definition 就能够发现,SD 的图片下载操做是由 SDWebImageDownloaderOperation
来完成的,因而看一下它的初始化方法:objective-c
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock;
复制代码
经过上面的方法签名,能够大概反向的知道:shell
NSURLRequest
,那么极可能内部就使用的 NSURLConnection
来完成的下载progress
、completed
这两个 callback,那么内部势必须要知道下载的进度cancelled
这个 callback,那么内部的下载操做还须要能够取消再看一下 SDWebImageDownloaderOperation
是继承于 NSOperation
,由于下载是一个能够独立出来的计算单元,因此做为 Opreation 是很好理解的。而后在实际的图片下载中,为了下载的效率,下载的 Opreations 之间确定是须要并发的。Operation 默认在其被调用的线程中是同步执行的,不过因为 Operation Queue 的存在,它能够将其中的 Operations 分别 attach 到由系统控制的线程中,而这些由系统控制的线程之间是并发执行的。缓存
查看 SDWebImageDownloaderOperation
源码发现内部果真是使用的 NSURLConnection
,那么因为须要提供 cancelled
的功能以及须要监听下载进度,故必须将 NSURLConnection
的实例配置成异步的方式:安全
具体代码在 L96bash
// 配置异步 NSURLConnection 的方式
// 实例化一个 NSURLConnection,并将自身(SDWebImageDownloaderOperation)设置为 NSURLConnection 实例的委托
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
// 由于上一步的 startImmediately:NO,因此这里手动的触发 start
// 这样的效果和直接 startImmediately:YES 是同样的
[self.connection start];
// 由于上面两步结合起来或者直接 startImmediately:YES 的结果就是下载例程将会在当前 Run Loop 上以默认的模式进行调度,
// 而在 iOS 中除了主线程以外的线程都是默认没有运行 Run Loop 的,因此须要手动的运行一下
CFRunLoopRun();
// 以后的代码将会被 CFRunLoopRun() 所阻塞,这样 operation 所在的线程
// 就不会自动的退出,因而须要额外的代码在下载完成以后手动的中止 RunLoop 使得
// operation 所在的线程能够退出
复制代码
对于下载进度的监听,SDWebImageDownloaderOperation
是经过将自身设置为 NSURLConnection
委托的形式完成的:多线程
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 在这一委托方法的实现中,SDWebImageDownloaderOperation
主要是获取服务端响应的 meta 信息,尝试根据响应的 statusCode 对下载过程进行预判,好比若是是 304 状态码直接从本地缓存中返回图片。可是这里的代码写的有些繁琐了,而且性能上也是存在些问题。首先能够看下这幅概览图:
上面就是 URL Loading System 的层次结构,可见 NSHTTPURLResponse
是 NSURLResponse
惟一的子类,而且含有其父类没有的 statusCode
方法。因而使用 isKindOfClass:
来判断参数是不是 NSHTTPURLResponse
就能够了,使用 respondsToSelector:
没有额外的好处并且丢失了性能,见 Performance penalty using respondsToSelector
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 经过实现这个委托方法,就能够知道有 new response chunk 被接收,因而能够向外提供 progress
,另外 SD 还实现了 display image progressively,按照代码中的描述,出自于这里 Progressive image download with ImageIO,其中有一小段是说 iOS 的实现相对于 Mac 须要点额外的步骤,而我将其示例代码下载了以后,在注释掉其中关于 iOS 适配的部分代码后运行,发现注释掉也是能够的:
/// Create the image
CGImageRef image = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
if (image) {
//#ifdef __IPHONE_4_0 // iOS
// CGImageRef imgTmp = [self createTransitoryImage:image];
// if (imgTmp) {
// [_delegate downloadedImageUpdated:imgTmp];
// CGImageRelease(imgTmp);
// }
//#else // Mac OS
// [_delegate downloadedImageUpdated:image];
//#endif
[_delegate downloadedImageUpdated:image];
CGImageRelease(image);
}
复制代码
也就是说这段 L290 代码实际是有一点性能问题的,应该找到一个临界的版本号以此适配老版本,而不是直接 TARGET_OS_IPHONE
。
还有一点在使用时须要注意的就是,若是须要得到具体的 progress
百分比,那么在 new chunk 到达的时候,除了须要知道已经下载了的 chunks 的 size 总和以外,还须要知道 Content-Length
,也就是在这里试图经过响应的 meta 信息(HTTP Headers)中获取 expectedContentLength
。
而根据 HTTP 协议的描述 [1, 2],若是服务端的响应采用了 chunked 的方式,那么客户端实现必须忽略服务端响应中的 Content-Length
(若是有的话。按照标准定义,在使用 chunked 时,服务端也应该不返回 Content-Length
,固然通常状况下也无法返回),换句话说,若是服务端响应的图片信息使用 chunked transfer encoding 的话,那么客户端在图片没有彻底下载好以前就没法知道图片的总大小,因而试图显示一个下载百分比的进度条就不行了。这段算是 tips 吧。
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection,须要知道下载完成的时间点,故实现了这个委托方法
在另外的一些委托方法中,SD 完成了取消下载的相应操做,以及当请求的 HTTPS 证书不可信时的操做,以及当服务端资源须要访问受权时的操做。
SD 经过 SDWebImageDownloaderOperation
将图片的下载操做封装成 NSOperation
,在内部经过设置 NSURLConnection
为异步的方式,并将自身设置为 NSURLConnection
委托,从而向外部提供下载进度控制的功能。
下一步须要分析的就是 SD 的缓存机制,首先从 SD 的 README 中得知 SD 提供了常见了 two-levels cache 机制,即 memory-disk 的方式。在上一段分析下载的过程里,发现 SD 下载图片仍是借由的 NSURLConnection
,从 Understanding Cache Access 得知,iOS 中的 URL loading system 已经自带了 two-levels cache 的机制,那么为何 SD 须要本身再实现一套呢?SD 本身是这样解释的,完整的解释见 How is SDWebImage better than X?,大概的意思就是:
虽然 NSURLCache
提供了 two-levels cache,可是它缓存的内容是 raw bytes,也就是说从 NSURLCache
中取出的是图片的 raw bytes,若是须要使用图片还须要进行进一步的操做,好比解析图片的信息,使其成为在 iOS 中可使用的形式。而 SD 的缓存的则是将解析后的能够在 iOS 中直接使用的图片,这样从缓存中取回的内容就不须要在解析一遍了,从而进一步节约了系统资源。
进一步了解 two-levels cache 或者 N-levels cache,其核心思想就是将须要缓存的内容放到多个 cache storages 中,而后在取出缓存内容时,尽可能的从响应速度较快的 storage 中取回。那么很明显,对于 memory-disk 这样的 two-levels cache,无非就是将须要缓存的内容同时放到 memory 和 disk 中,而后取回的时候先尝试较快的 storage,那么势必先检索 memory cache storage,若是 memory cache 没有命中的话,则尝试 disk cache storage。下一步就是分析 SD 中具体是如何完成这些工做的。
首先 SD 中使用 SDWebImageManager
去集中管理图片的下载操做,而且 SDWebImageManager
使用了单例的模式,在其初始化操做是这样的:
- (id)init {
if ((self = [super init])) {
// 初始化 two-levels cache,它以 SDImageCache 的单例去操做
_imageCache = [self createCache];
// 以单例的形式初始化 SDWebImageDownloader
_imageDownloader = [SDWebImageDownloader sharedDownloader];
// 存放失败的 URLs,为了 re-try 的判断
_failedURLs = [NSMutableSet new];
// 正在运行的 operations,方便统一的管理
_runningOperations = [NSMutableArray new];
}
return self;
}
复制代码
执行下载操做的是 SDWebImageManager
中的这个方法(具体的实如今 L110):
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
复制代码
在 downloadImageWithURL
的具体实现中,使用了 SDWebImageCombinedOperation
来统一管理两个操做(主要是取消的功能),一个操做就是先尝试从缓存中取回图片,另外一个操做就是若是缓存没有命中,尝试从源地址下载的操做。这样只要取消 SDWebImageCombinedOperation
就会同时取消那两个操做。
在下载的 subOperation
中,使用了 weakOperation
见 L183
这是由于 这里,若是在 subOperation
中没有使用 weakOperation
的话,那么就会发生 retain cycle
:
retain retain
+---------------------------------+ +---------------------+ +----------------------+
| SDWebImageCombinedOperation +-----------> cancelBlock +-----------> subOperation |
+----------------^----------------+ +---------------------+ +-----------+----------+
| |
| |
| |
| retain |
+--------------------------------------------------------------------------+
复制代码
另外因为须要在 self.runningOperation
中 add/remove
SDWebImageCombinedOperation
的实例,因此加上了 __block
修饰。
因为 SDWebImageManager
是单例的形式,而其可能在多线程的状况下被调用,因此对于其非线程安全的属性,在操做时使用了 @synchronized
来确保数据的完整性。
具体的业务逻辑是这样的:
completedBlock
,而后再继续进行下载操做。其实这一步放得有些散了,它是和 L180 以及 L216 搭配起来的。经过 L180
,当发现 Response 是被 NSURLCache
缓存的,那么 L216
的条件就会知足,为何会知足呢?由于 这里,因而 downloadedImage
是 nil
。
知足条件了因而就什么也没作(要作的在 L159
已经被作了)。也就是说一旦设置了 SDWebImageRefreshCached
选项,那么在使用 NSURLConnection
下载的时候,发现 Response 是此前缓存的,那么就直接从 SD 的缓存中返回处理好的图片,这么作的缘由上文已经说过了 NSURLCache
的缓存是数据的 raw bytes,而 SD 中缓存的图片数据是 out of the box。 4. 若是新下载了图片,那么确定是要先将其存储在 SD 缓存中,SD 提供了缓存选项可让调用者决定是单存 memory 或 disk 或 both,见 L237。
上面主要是分析了 SDWebImageManager
在下载图片时的操做,即先检索本地 SD 缓存,而后再根据下载选项决定是否从源地址进行下载,以及下载好图片以后将其存放到 SD 缓存中。
在第一节中介绍了 SD 将下载操做封装为了 SDWebImageDownloaderOperation
。SD 内部在使用时,并非直接操做 SDWebImageDownloaderOperation
的,而是使用的 SDWebImageDownloader
单例,在 SDWebImageDownloader
单例初始化的时候,产生了一个 NSOperationQueue
,见 L67,而且设置了对了的并发数为 6
,见 L68。而后在须要下载的时候,将 SDWebImageDownloaderOperation
实例添加到了其内部的下载队列中,这要就完成了并发下载的功能。
如今开始分析下 SD 中的一些关于缓存操做的细节。检索本地 SD 缓存分为两步,当检索 memory cache storage 时,采用的是同步的方式,这是由于内存缓存的操做速度是很快的,当检索 disk cache storage 时,SD 使用的是异步的方式,见 L372。SD 将缓存存储以及其相关的操做封装为 SDImageCache
而且以单例的模式进行操做,SDImageCache
的初始化在 SDWebImageManager
的初始化中进行调用。
有一点须要注意的就是,SD 中实现的 sharedXXX
方法并不能表示一个确切的单例模式,具体的描述见 Create singleton using GCD's dispatch_once in Objective C,若是用其余面向对象语言描述的话就是,必须将构造函数隐藏起来不要让外部调用到,好比设置成 private
,而后提供一个相似 getSingleton
的静态方法。不过就像上面的连接中描述的同样,若是口头约定老是使用 sharedXXX
方法来获取实例对象的话那也没有太大的问题。
对于异步的检索磁盘的方式,SD 采用的是 GCD,首先在 SDImageCache
初始化时建立了一个 ioQueue
,注意 SD 中采用的是一个 serial queue
,见 L99。使用 serial queue
的目的就是免得使用锁去管理磁盘数据的读写了。
对于内存缓存,SD 实现了一个内部类 AutoPurgeCache
,它继承自 NSCache
,功能就是在经过 Notification
来接受内存不足的通知,而后清除自身存储缓存所占用的内存空间。可是注意到一个细节,好比在 L106,看到下面的代码:
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
复制代码
为何须要在主线程上 postNotificationName:
(注:如遇到方法的签名我没有写全的状况请没必要在乎) 呢?
具体的内容在 Notification Programming Topics,大概的意思就是:
Regular notification centers deliver notifications on the thread in which the notification was posted. Distributed notification centers deliver notifications on the main thread. At times, you may require notifications to be delivered on a particular thread that is determined by you instead of the notification center. For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.
上面的一段引用其实说了几点内容,不过当前只须要知道第一句的意思:一般状况下 notification center 会把 posted notifications 派送给与 post 动做所在的同一线程中的 observers。而上面的 L106 中的代码能够看出,它指望的 observers 是在主线程的,那么 observers 就能够在主线程中更新 UI 来给用户相关的进度提示。
那为何须要 dispatch_async
呢?这是由于 Notification Centers 中描述的:
A notification center delivers notifications to observers synchronously. In other words, when posting a notification, control does not return to the poster until all observers have received and processed the notification. To send notifications asynchronously use a notification queue, which is described in Notification Queues
再看 AutoPurgeCache
中注册的 observer L24,observer 注册在 AutoPurgeCache
运行时所在的线程,根据上面的第一段引用中的描述,对于 local notification 而言,postor 和 receiver 须要在同一线程,因而就猜想是否是对于系统通知而言,会在全部的线程上进行 notify。可是没有在 Apple Doc 中找到明确的相关文字描述,不过进过测试确实对于系统通知而言,notifition center 会对进程中的全部线程进行 notify。下面是测试的代码:
@interface Worker : NSThread
@end
@implementation Worker
- (void)main
{
NSLog(@"Worker is running...");
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"Worker is exiting...");
}
- (void)testNotification
{
NSLog(@"testNotification");
}
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
Worker* worker = [[Worker alloc] init];
[worker start];
}
复制代码
能够运行模拟器而后 Hardware -> Simulate Memory Warning
就能够看到子线程是能够接收到通知的。
以上就是我阅读源码后的分析,虽然没有面面俱到,也仍是但愿能有所帮助。
上面有一段这样说到:
另外因为须要在 self.runningOperation 中 add/remove SDWebImageCombinedOperation 的实例,因此加上了 __block 修饰
我今天回头看了一下,发现我以前那样的描述是不对的。
首先能够看下这里的描述,大概意思就是说若是须要让那些被 block 所 captured 变量是 mutable 的,那么就须要使用 __block
前缀去修饰。
那么看看上面提到的 SD 中的代码,简化后就是这样:
// 这里的 __block 不须要
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}];
return operation;
复制代码
注意到在 cacheOperation
那一行产生的 block,它对 operation
进行了 capture,可是在 block 内部并无改变 operation
的指向。因此这里的 __block
是不须要的。Obj 对象在 block 是以引用去操做的,能够想象是对象的内存地址被捕获,若是是这样就须要加上 __block
:
__block SDWebImageCombinedOperation *operation = nil;
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
// 捕获这 operation,然而咱们须要改变它的内容
// 把它的内容变成新对象的地址
// 因此上面使用了 __block 前缀修饰
operation = [SDWebImageCombinedOperation new]
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}];
return operation;
复制代码
我看可使用下面的代码来验证下上面的说法:
//
// main.m
// __block
//
// Created by mconintet on 11/24/15.
// Copyright © 2015 mconintet. All rights reserved.
//
#import <Foundation/Foundation.h>
int main(int argc, const char* argv[])
{
@autoreleasepool
{
static NSMutableArray* arr;
static dispatch_once_t once;
dispatch_once(&once, ^{
arr = [[NSMutableArray alloc] init];
});
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
NSInteger opCount = 3;
for (NSInteger i = opCount; i > 0; i--) {
NSOperation* op = [[NSOperation alloc] init];
dispatch_async(queue, ^{
[arr addObject:op];
});
dispatch_async(queue, ^{
[arr removeObject:op];
if (![arr count]) {
dispatch_semaphore_signal(sema);
}
});
}
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"arr count: %ld", [arr count]);
}
return 0;
}
复制代码
对比下这段代码:
//
// main.m
// __block
//
// Created by mconintet on 11/24/15.
// Copyright © 2015 mconintet. All rights reserved.
//
#import <Foundation/Foundation.h>
int main(int argc, const char* argv[])
{
@autoreleasepool
{
static NSMutableArray* arr;
static dispatch_once_t once;
dispatch_once(&once, ^{
arr = [[NSMutableArray alloc] init];
});
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
NSInteger opCount = 3;
for (NSInteger i = opCount; i > 0; i--) {
NSOperation* op;
dispatch_async(queue, ^{
op = [[NSOperation alloc] init]
[arr addObject:op];
});
dispatch_async(queue, ^{
[arr removeObject:op];
if (![arr count]) {
dispatch_semaphore_signal(sema);
}
});
}
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"arr count: %ld", [arr count]);
}
return 0;
}
复制代码
你会发现后一段代码会被 IDE 提示:
为何不能赋值?由于指针的捕获也是做为了 const
,和基本类型同样。
总结起来讲就是,objc 对象在 block 中捕获的是指向其真实地址的指针,指针以 const
的形式被捕获,不使用 __block
修饰就没法改变指针的内容,可是对于指针指向的对象,它们的内容仍是能够改变的。
上面的关于 NSNotification 的说明有些纰漏,修正见 NSNotificationCenter