之前看源码的时候,有很多东西研究的不够透彻,所以重新整理学习下
NSURLSession 工作模式
NSURLSessionConfiguration 分为三种工作模式
default: 可以使用磁盘缓存、身份认证、Cookie
ephemeral(及时模式): 数据只能保存到内存,无法存储到磁盘。
background: 可以在后台执行上传或下载任务
后进先出
后进先出通过 NSOperation 的依赖实现
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
initialize 方法
即使创建多个实例,initialize 只会执行一次,如果希望仅运行一次的话调用该方法。
#import "Animal.h";
@implementation Animal
+(void) initialize {
NSLog(@"Animal initialize");
}
-(void) init {
NSLog(@"Animal init");
}
@end
// In Test class
- (void)test {
Animal *animal1 = [[Animal alloc] init];
Animal *animal2 = [[Animal alloc] init];
}
输出
Animal initialize
Animal init
Animal init
一般某个全局状态无法在编译期初始化,可以放在 initialize 里面。而 SDWebImageDownloader 中实现 initialize 方法,用来注册观察者对象
总结下 load 和 initialize 的区别
load 方法
- 所有引入的类及分类都会被调用 load 方法,并且仅调用一次。
- load 方法的调用在 main 函数之前调用
- 执行子类的 load 方法前,会先执行所有父类的 load 方法,调用顺序为:1.父类 2.子类 3.分类
- load 方法中不要调用其他类,因为各个类的载入顺序可能不同
- 如果类本身没有实现 load 方法,那么系统就不会调用该方法,不管父类有没有实现
- 执行 load 方法时会程序会被阻塞,直到所有类的 load 方法执行完毕才会继续
initialize 方法
- 使用类之前系统会调用该方法,并且仅调用一次
- 惰性调用,只有当程序使用相关类时才会调用
- 运行期系统会确保 initialize 方法是在线程安全的环境中执行,即只有执行 initialize 的那个线程可以操作类或类实例。其他线程都要先阻塞,等待 initialize 执行完
- 如果类未实现 initialize 方法,而其父类实现了该方法,那么会运行父类的该方法
渐进下载图片效果
Option 为 SDWebImageDownloaderProgressiveDownload 时可以实现渐进下载效果
实现的代码如下
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// Get the total bytes downloaded
const NSInteger totalSize = self.imageData.length;
// Update the data source, we must pass ALL the data, not just the new bytes
CGImageSourceRef imageSource = CGImageSourceCreateIncremental(NULL);
CGImageSourceUpdateData(imageSource, (__bridge CFDataRef)self.imageData, totalSize == self.expectedSize);
if (width + height == 0) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
// .. get values
CFRelease(properties);
// .. set orientation
}
}
if (width + height > 0 && totalSize < self.expectedSize) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
// .. Workaround for iOS anamorphic image
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
// .. scale and decode
image = [UIImage decodedImageWithImage:scaledImage];
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
}
流程主要是在接收到下载数据后,创建空的图片源 CGImageSourceCreateIncremental(NULL),之后更新图片源 CGImageSourceUpdateData(imageSource, false),最后创建图片用来显示CGImageSourceCreateImageAtIndex() 。
程序进入后台需要申请更长时间完成工作
if ([self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[[UIApplication sharedApplication] endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
beginBackgroundTaskWithExpirationHandler endBackgroundTask 一定要成对出现
GCD barrier
dispatch_barrier_async 比较常见,那么 dispatch_barrier_sync 又是做什么的,区别是什么呢?看文档可知 dispatch_barrier_async 不用执行完内部方法即可返回,dispatch_barrier_sync 需要等到执行完内部方法才会返回
dispatch_aync(queue, ^{
NSLog(@"In 1");
});
dispatch_barrier_sync(queue,^{
for (int i = 0; i < 100000; i++) {
// ...
}
NSLog(@"In barrier");
});
NSLog(@"In 2");
输出结果为
In 1
In barrier
In 2
如果 dispatch_barrier_sync 改为 dispatch_barrier_async
输出结果为
In 1
In 2
In barrier
线程锁
// 头文件中定义
@property (strong, nonatomic) NSMutableArray *failedURLs;
// 实现文件的函数中在多线程环境下使用,对参数加锁,保证临界区内代码的线程安全
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
failedURLs 没有设置为 atomic,由于 atomic 不是一个绝对线程安全的锁,它只是对 setter 和 getter 方法中的调用时线程安全的,对于 containsObject 无法保证线程安全,所以使用 synchronized 来保证线程安全
缓存的流程
- 通过 category 机制提供简单的 API 给外部调用
- 判断内存缓存中(使用 NSCache)是否已经存在要下载的图片,存在则直接返回图片
- 内存中没有找到的话,查看硬盘中是否存在,存在则返回图片
- 返回找到的图片前,判断是否需要解压图片(shouldDecompressImages),默认条件下需要解压图片则对图片进行解压
- 再判断是否需要内存缓存(shouldCacheImagesInMemory 属性),默认条件下则保存到内存
- 在判断是否需要使用磁盘缓存 (SDWebImageOptions 中的 SDWebImageCacheMemoryOnly),默认条件下则将链接地址作 MD5 后作为 key 保存到磁盘中。
- 另外,图片下载失败后,会记录下这个地址到 failedURLs,下载时可以选择是否下载失败的地址不再重新尝试下载
一些调用方法
- shouldDecompressImages 是否需要解压图片,默认是 YES
在缓存前对图片进行解码或者说解压缩,是由于图片被渲染到控件时,会有一个解压操作,这个解压过程会有一些效率问题,如果在将图片保存到缓存前就进行了解压缩,对于 TableView 等滑动时的流畅效果会有提高,这就是所谓的空间换时间的做法。
一张图片解压需要开辟的内存大小为
bitmapWidth bitmapHeight 位图的每一行所占的字节数( 例如 4 byte 或 2 byte)
例如一张 1500 * 500 大小的 PNG,需要 3000000B = 2.8M。
当显示尺寸较大的图片时,最好不要开启 shouldDecompressImages,否则可能导致内存剧增,从而出现内存警告甚至崩溃。
- maxCacheAge 缓存的有效时间是1周
清理缓存的方式见 SDImageCache cleanDiskWithCompletionBlock 方法,先删除1周前的文件,如果文件还超出设置的最大缓存容量,则将磁盘文件按大小排序后按大小顺序删除文件,直到缓存空间小于最大磁盘容量
可以给定一组链接,下载后按照动画方式展示
-(void)sd_setAnimationImagesWithURLs:arrayOfURLs
可以给定一个链接,如果已经有过缓存,则将缓存图片先展示,而不是展示占位图
-(void)sd_setImageWithPreviousCachedImageWithURL:placeholderImage:options: progress: completed;
downloadTimeout 超时时间是 15s
如果下载一些图片后不着急使用,即不需要设置 completeBlock 时使用 SDWebImagePrefetcher 它的下载优先级也是低的
保存图片的时候用链接地址作为 key,如果想过滤掉链接地址中的部分内容,设置 filter 即可,例如 key 需要过滤掉 query 参数,设置 filter 即可
[[SDWebImageManager sharedManager] setCacheKeyFilter:^(NSURL *url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path]; return [url absoluteString];
}];
图片下载失败后,会记录下这个地址到 failedURLs,下载时可以选择是否下载失败的地址不再重新尝试下载
保存后的文件名:先对链接进行 UTF8 编码,将中文等字符进行转换,之后进行 MD5,将 MD5 的结果作为文件名
Macros 那些常用的宏
判断是否支持了垃圾回收
#ifdef __OBJC_GC__
#error SDWebImage does not support Objective-C Garbage Collection
#endif
从 Objective C 2.0 开始,提供了一种内存管理形式垃圾回收,在那个还没有 ARC 的时代,使用它则不用考虑有关保持和释放对象、自动释放池或保持计数的问题了。但是这种机制只支持 OS X (Mac OS X 10.5 - 10.8) 系统,而且在早期的 Xcode 的编译设置中还能看到设置 GC 的选项,现在已经没有该设置了。所以这算是个历史问题。
#error token-sequence
其主要的作用是在编译的时候输出编译错误信息 token-sequence,从方便程序员检查程序中出现的错误。 该指令常和 ifdef 和 ifndef 搭配使用。写代码的时候如果要提醒有个错误必须修改,也可以临时写个该指令,提交代码前再删掉。
判断系统版本
#if __IPHONE_OS_VERSION_MIN_REQUIRED != 20000 && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_5_0
#error SDWebImage doesn't support Deployment Target version < 5.0
#endif
SDWebImage 3.0 版本只支持 iOS 7 了,这里看来是没有调整。20000 对应版本 2.0,2.2.1 对应的是 20201。至于为什么要和 2.0 比较下,还未想通。
判断是否支持了 ARC
#if !__has_feature(objc_arc)
#error SDWebImage is ARC only. Either turn on ARC for the project or use -fobjc-arc flag
#endif
忽视 performSelector may cause a leak because its selector is unknown 警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
#pragma clang diagnostic pop
另外还可以通过函数指针方式避免该警告
SEL selector = NSSelectorFromString(@"sharedActivityIndicator");
IMP imp = [NSClassFromString(@"SDNetworkActivityIndicator") methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(NSClassFromString(@"SDNetworkActivityIndicator"), selector);
2017 更新以下内容
decodedImageWithImage 的目的
由于图片加载后当绘制的时候才将图片数据解压成 ARGB 图像,所以在每次绘图时,会有一个解压图片操作,在绘制的时候同时做解码操作就会导致效率问题。为了提高效率通过 SDWebImageDecoder 将数据解压,将解码后的图片直接保存到内存中,缺点是占用内存较多,是一种空间换时间的做法。
参考文献
NSURLSession 教程
NSURLSession 使用说明
使用 NSURLSession
iOS 图片解压缩
开源库分析
图片解码