在开发项目的过程中会用到很多第三方库,比如AFNetWorking,SDWebImage,FMDB等,但一直都没去好好的研究一下,最近刚好项目不是太紧,闲下来可以给自己充充电,先研究一下SDWebImage的底层实现,源码地址:SDWebImage
先介绍一下SDWebImage,我们使用较多的是它提供的UIImageView分类,支持从远程服务器下载并缓存图片。自从iOS5.0开始,NSURLCache也可以处理磁盘缓存,那么SDWebImage的优势在哪?首先NSURLCache是缓存原始数据(raw data)到磁盘或内存,因此每次使用的时候需要将原始数据转换成具体的对象,如UIImage等,这会导致额外的数据解析以及内存占用等,而SDWebImage则是缓存UIImage对象在内存,缓存在NSCache中,同时直接保存压缩过的图片到磁盘中;还有一个问题是当你第一次在UIImageView中使用image对象的时候,图片的解码是在主线程中运行的!而SDWebImage会强制将解码操作放到子线程中。下图是SDWebImage简单的类图关系:
下面从UIImageView的图片加载开始看起,Let's go! 首先我们在给UIImageView设置图片的时候会调用方法: -
1 | <span style= "font-size: 14px;" >( void )sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;<br></span>
|
其中url为远程图片的地址,而placeholder为预显示的图片。 其实还可以添加一些额外的参数,比如图片选项
1 | <span style= "font-size: 14px;" >SDWebImageOptions<br><br> typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {<br> SDWebImageRetryFailed = 1<br></span>
|
一般使用的是SDWebImageRetryFailed | SDWebImageLowPriority,
下面看看具体的函数调用:
1 | <span style= "font-size: 14px;" >- ( void )sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock <br>{<br> [self sd_cancelCurrentImageLoad];
|
可以看出图片是从服务端、内存或者硬盘获取是由SDWebImageManager管理的,这个类有几个重要的属性:
1 | <span style= "font-size: 14px;" >@property (strong, nonatomic, readwrite) SDImageCache *imageCache;
|
manager会根据URL先去imageCache中查找对应的图片,如果没有在使用downloader去下载,并在下载完成缓存图片到imageCache,接着看实现:
1 | <span style= "font-size: 14px;" > - (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options<br> progress:(SDWebImageDownloaderProgressBlock)progressBlock<br> completed:(SDWebImageCompletionWithFinishedBlock)completedBlock<br> {<br> <br>
|
下面先看downloader从网络下载的过程,下载是放在NSOperationQueue中进行的,默认maxConcurrentOperationCount为6,timeout时间为15s:
1 | <span style= "font-size: 14px;" >- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {<br> __block SDWebImageDownloaderOperation *operation;<br> __weak SDWebImageDownloader *wself = self;<br> <br>
|
SDWebImageDownloaderOperation派生自NSOperation,通过NSURLConnection进行图片的下载,为了确保能够处理下载的数据,需要在后台运行runloop:
1 | <span style= "font-size: 14px;" >- ( void )start {<br> <br># if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0<br>
|
下载过程中,在代理 - (void)connection:(NSURLConnection )connection didReceiveData:(NSData )data中将接收到的数据保存到NSMutableData中,[self.imageData appendData:data],下载完成后在该线程完成图片的解码,并在完成的completionBlock中进行imageCache的缓存:
1 | <span style= "font-size: 14px;" >- ( void )connectionDidFinishLoading:(NSURLConnection *)aConnection {<br> SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;<br> @synchronized(self) {<br> CFRunLoopStop(CFRunLoopGetCurrent());
|
SDWebImageCache管理着SDWebImage的缓存,其中内存缓存采用NSCache,同时会创建一个ioQueue负责对硬盘的读写,并且会添加观察者,在收到内存警告、关闭或进入后台时完成对应的处理:
1 | <span style= "font-size: 14px;" >- (id)init {<br> _memCache = [[NSCache alloc] init];<br> _ioQueue = dispatch_queue_create( "com.hackemist.SDWebImageCache" , DISPATCH_QUEUE_SERIAL);<br>
|
查询图片
每次向SDWebImageCache索取图片的时候,会先根据图片URL对应的key值先检查内存中是否有对应的图片,如果有则直接返回;如果没有则在ioQueue中去硬盘中查找,其中文件名是是根据URL生成的MD5值,找到之后先将图片缓存在内存中,然后在把图片返回:
1 | <span style= "font-size: 14px;" >- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {<br> <br>
|
在硬盘查询的时候,会在后台将NSData转成UIImage,并完成相关的解码工作:
1 | <span style= "font-size: 14px;" >- (UIImage *)diskImageForKey:(NSString *)key {<br> NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];<br> if (data) {<br> UIImage *image = [UIImage sd_imageWithData:data];<br> image = [self scaledImageForKey:key image:image];<br> if (self.shouldDecompressImages) {<br> image = [UIImage decodedImageWithImage:image];<br> }<br> return image;<br> }<br> else {<br> return nil;<br> }<br>}<br></span>
|
保存图片
当下载完图片后,会先将图片保存到NSCache中,并把图片像素大小作为该对象的cost值,同时如果需要保存到硬盘,会先判断图片的格式,PNG或者JPEG,并保存对应的NSData到缓存路径中,文件名为URL的MD5值:
1 | <span style= "font-size: 14px;" >- (NSString *)cachedFileNameForKey:(NSString *)key {<br>
|
1 | <span style= "font-size: 14px;" >- ( void )storeImage:(UIImage *)image recalculateFromImage:( BOOL )recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:( BOOL )toDisk <br>{<br>
|
硬盘文件的管理
在程序退出或者进入后台时,会出图片文件进行管理,具体的策略:
1 | <span style= "font-size: 14px;" >- ( void )cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {<br> dispatch_async(self.ioQueue, ^{<br> NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];<br> NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];<br><br>
|
总结
1 | <span style= "font-size: 14px;" >[self.imageView sd_setImageWithURL:[NSURL URLWithString:@ "url" ]<br> placeholderImage:[UIImage imageNamed:@ "placeholder" ]];<br></span>
|
一个简单的接口将其中复杂的实现细节全部隐藏:简单就是美。
采用NSCache作为内存缓
耗时较长的请求,都采用异步形式,在回调函数块中处理请求结果
NSOperation和NSOperationQueue:可以取消任务处理队列中的任务,设置最大并发数,设置operation之间的依赖关系。
图片缓存清理的策略
dispatch_barrier_sync:前面的任务执行结束后它才执行,而且它后面的任务要等它执行完成之后才会执行。
使用weak self strong self 防止retain circle
如果子线程进需要不断处理一些事件,那么设置一个Run Loop是最好的处理方式