Source Code Learning - SDWebImage

之前看源码的时候,有很多东西研究的不够透彻,所以重新整理学习下

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 图片解压缩
开源库分析
图片解码

请我喝汽水儿