SDWebImage 源码解读之网络请求层SDWebImageD
本章开始将介绍SDWebImage库中的核心类SDWebImageDownloaderOperation,该类是SDWebImage库进行网络加载的底层操作核心所在,理解了该类的实现思想,就能知道SDWebImage库的网络加载策略是如何的。首先解释一下涉及到的一些基本概念和方法:
•OC中的反射机制
反射是计算机程序在运行时检查、内省、修改自身结构和行为的一种能力。这样的能力可以让一个给定的程序动态地适应不同的情况。OC语言中的OC对象,都继承自NSObject类。这个类为我们提供了一些基础的方法和协议,我们可以直接调用从这个类继承过来的方法。本篇文章中讲到的反射方法,就在NSObject和Foundation框架中。Foundation框架为我们提供了一些反射的API方法,我们可以通过这些API执行将字符串转为SEL等操作。由于OC语言的动态性,这些操作都是发生在运行时的。
// SEL和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
// Class和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
// Protocol和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0);
FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NS_AVAILABLE(10_5, 2_0);
// 假设从服务器获取JSON串,通过这个JSON串获取需要创建的类为ViewController,并且调用这个类的getDataList方法。
Class class = NSClassFromString(@"ViewController");
ViewController *vc = [[class alloc] init];
SEL selector = NSSelectorFromString(@"getDataList");
[vc performSelector:selector];
通过这些方法,我们可以在运行时选择创建实例,并动态选择调用哪个方法。这些操作甚至可以由服务器传回来的参数来控制,我们可以将服务器传回来的类名和方法名,实例为我们的对象。
使用反射机制的优点:
1.可以弱化连接,他并不会把没有的Framework也link到程序中
2.不需要使用import,因为类是动态加载的,只要存在就可以加载
使用反射机制的缺点:
1.不利于维护。使用反射模糊了程序内部实际发生的事情,隐藏了程序的逻辑。这种绕过源码的方式比直接代码更为复杂,增加了维护成本。
2.性能较差。使用反射匹配字符串间接命中内存比直接命中内存的方式要慢。当然,这个程度取决于使用场景,如果只是作为程序中很少涉及的部分,这个性能上的影响可以忽略不计。但是,如果在性能很关键的应用核心逻辑中使用反射,性能问题就尤其重要了。
•NSOperation
NSOperation是一个抽象类,它不能直接使用,必须使用NSOperation子类。任务以NSOperation实例的形式提交到操作队列,可以简单地认为NSOperation是单个的工作单元。在iOS SDK里,提供两个NSOperation的具体子类。这些类可以直接使用,也可以继承NSOperation来创建自己的类来执行操作。我们可以直接使用的两个类:
•NSInvocationOperation
-(id)initWithTarget:(id)targetselector:(SEL)selobject:(id)arg;//创建NSInvocationOperation对象
-(void)start;//调用start方法开始执行操作
一旦执行操作,就会调用target的sel方法
代码:
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
[op start];
- (void)run {
NSLog(@"------%@", [NSThread currentThread]);
}
注意:默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作,只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作,此类仅当了解,在开发中并不常用
•NSBlockOperation
+(id)blockOperationWithBlock:(void(^)(void))block;//创建NSBlockOperation对象
-(void)addExecutionBlock:(void(^)(void))block;//通过addExecutionBlock:方法添加更多的操作
代码:
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载1------%@", [NSThread currentThread]);// 在主线程
}];
// 添加额外的任务(在子线程执行)
[op addExecutionBlock:^{
NSLog(@"下载2------%@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"下载3------%@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"下载4------%@", [NSThread currentThread]);
}];
[op start];
注意:只要NSBlockOperation封装的操作数 >1,就会异步执行操作
•自定义子类继承NSOperation,实现内部相应main的方法封装操作
如果是自定义类继承于NSOperation, 那么需要将操作写到自定义类的main方法中,重写main方法,重写-(void)main方法时需自己创建自动释放池(因为如果是异步操作,无法访问主线程的自动释放池),经常通过-(BOOL)isCancelled方法检测操作是否被取消,对取消做出响应,这种实现方式封装操作, 可以提高代码的复用性。
1.创建类JPOperation,继承NSOperation
@interface JPOperation : NSOperation
@end
#import "JPOperation.h"
@implementation JPOperation
- (void)main {
NSLog(@"%s, %@", __func__,[NSThread currentThread]);// 我们要重写main方法,封装操作
}
@end
2.使用自定义的NSOperation
// 1.封装操作
JPOperation *op1 = [[JPOperation alloc] init];
// 2.执行操作
[op1 start];
JPOperation *op2 = [[JPOperation alloc] init];
[op2 start];
ps:NSOperation通常是配合NSOperationQueue实现多线程编程,如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的- (void)start方法。
•iOS开启后台任务机制
App进入后台,会停止所有线程,用 NSThread 的 detachNewThreadSelector:toTar get:withObject:类方法创建的线程也被挂起了。如果想在后台完成一个长期任务,就必须调用 UIApplication 的 beginBackgroundTaskWithExpirationHandler:实例方法,来向 iOS 借点时间。默认情况下,如果在这个期限内,长期任务没有被完成,iOS 将终止程序。只要调用了此函数系统就会允许app的所有线程继续执行,直到任务结束。下面有个例子:
1.在项目的AppDelegate.h文件中,声明一个 UIBackgroundTaskIdentifier ,相当于一个借据,告诉iOS,我们的程序将要借更多的时间来完成 Long-Running Task 任务。
@property (nonatomic, unsafe_unretained) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
@property (nonatomic, strong) NSTimer *myTimer;
2.在项目的AppDelegate.m文件中,注意在applicationDidEnterBackground方法中,完成借据的流程,即:
self. backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^( void) {
[self endBackgroundTask];
}];
当应用程序留给后台的时间快要到结束时(应用程序留给后台执行的时间是有限的), 这个Block块将被执行,我们需要在Block块中执行一些清理工作。如果清理工作失败了,那么将导致程序挂掉,清理工作需要在主线程中用同步的方式来进行。默认情况下,如果在这个期限内,长期任务没有被完成,iOS 将终止程序,需要在Block中来释放公共的资源、存储用户数据、停止我们定义的定时器(timers)、并且存储在程序终止前的相关信息。完成后,要告诉iOS,任务完成,提交完成申请。每个对 beginBackgroundTaskWithExpirationHandler:方法的调用,必须要相应的调用 endBackgroundTask:方法来告诉应用程序你已经执行完成了。同时要销毁后台任务标识符
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
strongSelf.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
•@synchronized(self)的用法
@synchronized主要是考虑多线程的程序,这个指令可以将{ } 内的代码限制在一个线程执行,如果某个线程没有执行完,其他的线程如果需要执行就得等着。其作用是创建一个互斥锁,保证此时没有其它线程对self对象进行修改。这个是objective-c的一个锁定令牌,防止self对象在同一时间内被其它线程访问,起到线程的保护作用。
OC支持程序中的多线程。这就意味着两个线程有可能同时修改同一个对象,这将在程序中导致严重的问题。为了避免这种多个线程同时执行同一段代码的情况,OC提供了@synchronized()指令。
指令@synchronized()通过对一段代码的使用进行加锁。其他试图执行该段代码的线程都会被阻塞,直到加锁线程退出执行该段被保护的代码段,也就是说@synchronized()代码块中的最后一条语句已经被执行完毕的时候。
指令@synchronized()需要一个参数。该参数可以是任何的OC对象,包括self。这个对象就是互斥信号量。他能够让一个线程对一段代码进行保护,避免别的线程执行该段代码。针对程序中的不同的关键代码段,我们应该分别使用不同的信号量。只有在应用程序编程执行多线程之前就创建好所有需要的互斥信号量对象来避免线程间的竞争才是最安全的。以下代码中使用self作为互斥信号量来实现当前对象对实例方法访问的同步。
-(void)criticalMethod{
@synchronized(self) {
//关键代码;
}
}
使用自定义的信号量来对方法加锁:
Account *account = [AccountaccoutFromString :[accountFiled stringValue]];
//获取信号量
id accountSemaphore = [Account semaphore];
@synchronized(accountSemaphore) {
//关键代码
}
Objective-C中的同步特性是支持递归的。一个线程是可以以递归的方式多次使用同一个信号量的;其他的线程会被阻塞知道这个线程释放了自己所有的和该信号量相关的锁,也就是说通过正常执行或者是通过异常处理的方式退出了所有的@synchronized()代码块。当在@synchronized()代码块中抛出异常的时候, Objective-C运行时会捕获到该异常,并释放信号量,并把该异常重新抛出给下一个异常处理者。
•KVO机制之手动通知
KVO是Key-Value Observing的简称,翻译成中文就是键值观察。这是iOS支持的一种机制,用来做什么呢?我们在开发应用时经常需要进行通信,比如一个model的某个数据变化了,界面上要进行相应的变化,但是如果我们程序并不知道数据什么时候会进行变化,总不能一直循环判断有没有变化吧,那么就需要在数据变化时给controlller发送一个通知,告知我变化了,你可以更新显示内容了,通知的方式有很多种,比如Notification也是其中一种方式,本文要讲解的KVO也是其中一种很轻巧的方式。他的实现机制为可能改变的数据增加一个观察者,在上面的说法中这个观察者就是controller,它去观察这个数据有没有发生变化,一旦发生变化,就会得到一个信号,从而获取到变化的数据,进行自己要做的操作。
添加了观察者后,只要发生改变就会自动通知观察者,但有时候我们并不是什么改变都希望得到通知,或者有时候是希望变化到什么情况后再通知,这就需要改变通知的机制。默认的实现模式为自动通知的模式,要自定义何时进行通知,就要改成手动通知的模式。要改成手动通知,首先要在被观察者的模型中重写一个方法 automaticallyNotifiesObserversForKey :,下面有个例子:
// StudentModel.m
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = NO;
if ([key isEqualToString:@"score"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
这里我们在学生模型的实现文件中重写了这个方法,判断当观察的key是score分数时,就将自动通知关闭,其余的情况还是根据父类来进行判断,这样写比较保险。这样在我们改变学生模型的分数时,就不会自动触发通知了,要触发通知,需要自己进行设置:
// 按钮响应
- (void)changeScore {
[self willChangeValueForKey:@"score"];// 改为手动通知
[self.studentModel setValue:@"99.0" forKey:@"score"];
[self didChangeValueForKey:@"score"];// 改为手动通知
}
// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {
if ([keyPath isEqualToString:@"score"]) {
self.scoreLabel.text = [NSString stringWithFormat:@"Score:%@", [self.studentModel valueForKey:@"score"]];
}
NSLog(@"%@", context);// 通过context获取被观察者传递的内容
}
下面开始对SDWebImageDownloaderOperation类的实现思路进行解读,由于SDWebImageDownloaderOperation是自定义的Operation,并被添加到队列中进行异步执行的,所以该类的关键思路是在- (void)start方法里进行执行,其初始化方法比较简单,只是简单做了赋值的操作,接下来会详细讲解- (void)start方法,先贴上代码
这个方法一开始就使用了@synchronized()指令对网络请求初始化操作进行了线程锁操作,所有对该对象的使用或修改,都必须得保证已经进行了网络请求初始化,才能进行下一步操作,否则会出现线程不安全的问题。在SDWebImage库的视图类的WebCache类中,像UIImageView+WebCache类,在进行图片下载过程中会频繁的去修改,创建,移除一个Operation,这种操作是是通过关联机制异步去执行的,如果不能保证执行网络请求任务的线程安全问题,就会出现同一个Operation被随意篡改,这是很危险的。
在该指令中,会对一个Operation对象任务进行判断,任务的执行是否被取消,如果是被取消的,就会进行初始化信息的重置工作,保证数据跟操作的一致性,如果不是被取消的就会进行网络请求的初始化工作,值得一提的是,作者考虑到,iOS系统中,程序一旦进入后台挂起状态,程序请求的所有线程操作都不会执行,网络请求到的数据不能及时的对UI层面进行刷新功能,就意味着功能会存在缺陷。出于这个考虑,作者借用了iOS的开启后台任务机制,向系统申请更长的后台线程执行时间,以保证线程即使处于后台状态,也能进行线程操作,保证线程执行的顺畅性,并在系统允许的最后期限内,对资源进行释放操作。
在执行完[self.dataTask resume]之后,作者会对self.dataTask的值存在与否进行判断,若该值存在,则会开始调用self.progressBlock进行进度操作,并发送SDWebImageDownloadStartNotification通知,以保证相关界面能进行相关联的操作,否者的话着直接执行完成错误的操作,此时的任何操作都不涉及到网络请求。
所有的操作都达到预期之后,便会进行网络请求操作,这里使用的网络操作类是NSURLSession,作者对于网络数据的操作是放在NSURLSession类对应的几个委托方法中,下面先来看看一个代理方法:
当请求的图片URL存在并得到响应时,该方法就会被调用,也就是说该方法是用来执行网络请求刚开始得到响应的回调操作,网络请求得到响应后会返回一个(NSURLResponse *)response的对象,该对象中包含了本次图片数据的一下基本信息。从代码中可以看到,response对象存在一个statusCode的属性值,该值代表的是http网络请求的状态码,作者只比较了400跟304的状态码,这里作者写的有点不好,就是没有去指明对应状态码的含义,毕竟状态码的数值太多,不进行注释的话确实不知道是什么意思(详细的http网络请求的状态码的查看地址:http://blog.csdn.net/zhangmengleiblog/article/details/52513227),简单说下小于400的状态码表示网络请求是成功的。304表示的是自从上次请求后,请求的网页未被修改过。服务器返回此响应时,不会返回网页内容。
在if判断中,进行的是正常响应下的操作,代码中会先去获取图片的实际内容大小,如果存在的情况下,则会再次调用self.progressBlock,这个时候我们就知道了图片的实际大小,而不是一开始设置的无穷大,同时会发送SDWebImageDownloadReceiveResponseNotification去告诉对应页面当前已经拿到数据了,可以进行对应状态的操作了。else中则进行的是网络请求错误下的操作,同样是进行数据清楚重置操作,发送SDWebImageDownloadStopNotification通知告知对应页面要执行网络停止情况下的对应操作。
最后则是通过下面代码结束此次回调,
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
该代码的作用是告知网络请求操作,数据没有问题,可以把数据下载下来了
这个代理方法是用来监听图片数据的下载过程,图片URL得到响应后接着就是把数据进行下载的操作了。在该方法中,获取到的每一个图片的数据都会被保存到self.imageData中,如果图片的内容是存在,用户也实现了下载完成的block的话就会进行图片的绘制工作,绘制完的图像会转化成UIImage对象,然后按照SDWebImage库提供的图片缩放的方法对图片按照指定的大小进行缩放(如果需要进行压缩的话,会使用对应的压缩方法进行压缩),在回传给UI层,之后便是再次调用self.progressBlock,刷新一下进度操作。
该回调方法的作用这是用来表示是否需要对改网络响应进行缓存操作,在所有的网络请求之后最后会回调一下的方法进行最后的界面刷新操作跟数据清除。
总结:合理的使用iOS提供的各种机制,能够更好的实现功能。