SDWebImage源码解读

图片显示、缓存应该是每个App必不可少的功能了,而SDWebImage就是用Objective-C实现该功能的一个库,在iOS App开发中几乎都用到了它,与AFNetworking一样几乎成为了App开发标配三方库。

特性

SDWebImage具有的特性是相当诱人的,此处直接引用官方对它的介绍:

Features

  • Categories for UIImageView, UIButton, MKAnnotationView adding web image and cache management
  • An asynchronous image downloader
  • An asynchronous memory + disk image caching with automatic cache expiration handling
  • A background image decompression
  • A guarantee that the same URL won’t be downloaded several times
  • A guarantee that bogus URLs won’t be retried again and again
  • A guarantee that main thread will never be blocked
  • Performances!
  • Use GCD and ARC

原理

SDWebImage默认缓存实现是非常激进的,它忽略HTTP服务器返回的所有cache-control头,然后没有时间限制地缓存返回的图片,这就暗示了你的图片的URL是静态的,也就是说每个URL指向的图片内容是不会改变的,如果指向的图片内容改变了,那么组成该URL的某个部分肯定也发生了改变。

缓存的存储形式

SDWebImage实现了2种缓存:内存缓存和磁盘缓存。内存缓存重启App就没了,用的是系统的NSCache,系统会根据设置的countLimit、totalCostLimit对存储的内容进行缓存及释放;磁盘缓存方式就是把接收到的图片数据存储到文件中,每张图片存储一个文件,这些文件都位于指定缓存路径的目录下,文件名是MD5(URL)。

缓存移除策略

  • 内存缓存移除:当监听到系统的UIApplicationDidReceiveMemoryWarningNotification通知时清除所有的内存缓存,如果设置了内存缓存的countLimit、totalCostLimit属性,则在没收到内存告警通知时都由系统进行移除处理。

  • 磁盘缓存移除:在将要结束App和App进入后台后会进行磁盘过期文件删除,所谓过期文件就是指超过1周的文件(默认实现是1周),如果设置了最大的缓存size,那么当把过期的文件全部删除后剩余文件的总大小依然超过最大size的话则会根据剩余文件的修改日期进行排序依次删除文件,一直到剩余文件的总大小为size/2。

    SDWebImage使用方法

    SDWebImage官方已经给出了How To Use,此处直接引用:

    import

    [imageView sd_setImageWithURL:[NSURL URLWithString:@”http://www.domain.com/path/to/image.jpg“]

    placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
    

##SDWebImage运行流程

SDWebImage官方画了一个时序图,

从该图中可以很好地了解它的大概运动流程:首先UIImageView的分类会设置一个图片对应的URL,然后进行一些内部处理,加载该URL对应的图片,在加载URL对应的图片时会查询URL图片信息是否已经缓存了,如果缓存了则直接使用缓存,如果没有缓存则请求网络加载图片,网络返回图片数据后把数据存储到磁盘上并且返回图片数据给上层使用。

##SDWebImage中使用到技术

category

为了方便让使用者使用,SDWebImage实现了一个UIImageView+WebCache分类,让使用者不需要引入新的类就实现了UIIImageView图片加载功能。可能有人会说用继承不好吗?继承确实不好,因为此处了业务逻辑只是想让某个类具有图片加载的方式而已,就是只需新增一个方法(行为)而已不需要定义额外很多属性,category更强调的是组合,我们应该优先使用组合而不是继承,category是化继承为组合的最佳方式。

在category中是不能直接定义属性的,如果想要定义属性那么需调用runtime API,SDWebImage中就使用了大量category的属性,比如UIView+WebCache中存储、获取当前图片的URL的方法是:

    static char imageURLKey;
    //setter
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //getter
    - (nullable NSURL *)sd_imageURL {
    return objc_getAssociatedObject(self, &imageURLKey);
}

通过用objc_setAssociatedObjectobjc_getAssociatedObject方法就实现了为UIView+WebCache分类中新增sd_imageURL属性。

多线程技术

  • 为操作共享资源创建串行队列
    操作共享资源除了使用锁技术,还可以用GCD串行队列。这两者有什么区别呢?如果用锁的话当访问共享资源出现竞争时会卡住等待访问的线程,而使用dispatch_async GCD串行队列的话则可避免此类问题,提升性能。SDImageCache中创建了IO串行队列(GCD),用于对文件安全操作(查询、增加、删除,比如当网络返回数据准备存储到磁盘时,此时切换进入后台开始删除文件,就有可能出现错误)。
  • 多读单写提高性能
    假如有一个用户类User,该类有个好友属性@property(nonatomic,strong) NSMutableArray *friends;有多个线程会操作friends属性,那么我们为了让friends多线程安全可能会这么写:

    //获取好友列表
    -(NSArray)fetchFriends
    {
    @synchronized (_friends) {
    return _friends;
    }
    }
    //增加好友
    -(void)addFriend:(NSString
    )friend
    {
    @synchronized (_friends) {
    [_friends addObject:friend];
    }
    }

如果写成这样的话那么当前只能有一个线程在访问,不管是读还是写,但如果当前业务中绝大多数场景都是读取好友列表的话,那么这种写法就很影响性能了,我们知道多线程操作共享数据危险是指的对共享数据的写操作(读操作不会改变共享数据的状态所以是安全的)。所以一种正确的写法应该是:多个线程可以同时进行读操作,但是写操作同一时刻只能有一个。iOS中想要实现这种方案是很容易的,具体做法是:创建一个GCD并行队列,写操作时调用dispatch_barrier_async API,读操作时调用dispatch_sync API。SDWebImage中就使用了这种多读单写的技术,具体可以查看SDWebImageDownloaderOperation类。

//创建一个并行队列
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
//对self.callbackBlocks进行写操作
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;
}
//对self.callbackBlocks进行读操作
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
    __block NSMutableArray<id> *callbacks = nil;
    dispatch_sync(self.barrierQueue, ^{
        // We need to remove [NSNull null] because there might not always be a progress block for each callback
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
        [callbacks removeObjectIdenticalTo:[NSNull null]];
    });
    return [callbacks copy];    // strip mutability here
}

异步下载器

SDWebImageDownloader是一个异步下载器,是通过NSOperationQueue和自定义NSOperation类(SDWebImageDownloaderOperation)实现的,SDWebImageDownloaderOperation被抽象为一个网络请求,那SDWebImageDownloaderOperation是如何实现的?SDWebImageDownloaderOperation是继承自NSOperation(NSOperation是一个抽象类)的,所以SDWebImageDownloaderOperation需实现一些NSOperation定义好的接口:

//是否是异步,因为图片下载是一个网络请求,所以执行task肯定是一个异步操作,如果是不是异步的话那么当执行完start和main方法后该Operation就认为结束了,就从NSOperationQueue中删除了,这显然不是我们想要的,我们想要的是当从网络上接收完数据后才结束。
- (BOOL)isConcurrent {
    return YES;
}        
//实现start方法,start方法也就是NSOperation真正想要执行的task,此处是创建一个NSURLSessionTask用来请求图片数据。另外因该Ooperation是异步运行task,所以比需实现start方法,因为只有在start方法中才能设置executing的状态
- (void)start{
...
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
[self.dataTask resume];
...
}
//手动实现finished的KVO,为的是告之NSOperationQueue该Operation已经完成,可以从队列中退出了,可以执行其它等待的Operation了。
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
//手动实现executing的KVO,为的是向外界告之自己的运行状态
- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}
//实现cancel方法是让没有开始执行的NSOperation可以取消执行,当然如果是对正在执行的Operation进行cancel操作,那么是否能取消执行就依赖具体的start、main方法的实现了
- (void)cancel {
@synchronized (self) {
    [self cancelInternal];
}
}

weak-strong dance

为了避免循环引用引发的内存泄漏,SDWebImage中使用了大量的weak-strong dance,比如如下代码:

1
__weak SDWebImageDownloader *wself = self;
    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }