SDWebImage实现原理:
入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.
先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。
SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。
如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。
根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。
共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
SDWebImage的源码解析:
SD中重要的类:
SDWebImageDownloader:图片的下载SDWebImageManager:图片的管理 SDImageCache:图片的缓存
先讲解图片的缓存以及清理机制SDImageCache,源码比较简单,主要有以下几点:
1: 图片的存储,如果允许内存的缓存机制,先存储到内存的缓存中(NSCache),并给予图片成本cost。如果允许磁盘的缓存,再存储到磁盘的中。默认情况下两种缓存方式均为true。为了保持图片分辨率,需要对png和jpeg等图片采用对应的压缩机制,png有一个独特的签名,第一个8字节的PNG文件总是包含以下(十进制)值:137 80 78 71 13 10 26 10。
- (void)storeImage:(UIImage*)image recalculateFromImage:(BOOL)recalculate imageData:(NSData*)imageData forKey:(NSString*)key toDisk:(BOOL)toDisk {
if(!image || !key) {
return;
}
if(self.shouldCacheImagesInMemory) {
NSUIntegercost =SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if(toDisk) {
dispatch_async(self.ioQueue, ^{
NSData*data = imageData;
if(image && (recalculate || !data)) {
#ifTARGET_OS_IPHONE
int alphaInfo =CGImageGetAlphaInfo(image.CGImage);
BOOLhasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOLimageIsPng = hasAlpha;
if([imageData length] >= [kPNGSignatureData length]) {
imageIsPng =ImageDataHasPNGPreffix(imageData);
}
if(imageIsPng) {
data =UIImagePNGRepresentation(image);
}else{
data =UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageReprepresentationOfImageRepsInArray:image.representations usingType:NSJPEGFileTypeproperties:nil];
#endif
}
[selfstoreImageDataToDisk:data forKey:key];
});
}
}
2:磁盘的缓存,把图片的url当成key,通过MD5加密,生成一个32位的16进制数转成一个唯一的字符串,来作为存储本地磁盘的文件名,沙河路径拼接此文件名就是图片存储在磁盘的唯一路径。
- (NSString*)cachedFileNameForKey:(NSString*)key {
const char *str = [keyUTF8String];
if(str ==NULL) {
str ="";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString*filename = [NSStringstringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"": [NSStringstringWithFormat:@".%@", [key pathExtension]]];
returnfilename;
}
3:内存缓存,其实就是一个 NSCache,NSCache在系统内存紧张(较低)时,会自动释放对象,相当于可变字典,只不过多了cost参数而已。cost代表指定该key值对应的成本,用于计算记录在缓存中的所有对象的总成本。
if(self.shouldCacheImagesInMemory) {
NSUIntegercost =SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
4:查找缓存的过程,先查内存里(NSCache)有没有,有就返回,没有就继续查磁盘文件有没有,有的话先放到内存里,然后返回。用一个单独的队列来进行磁盘读写。
- (NSOperation*)queryDiskCacheForKey:(NSString*)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if(!doneBlock) {
returnnil;
}
if(!key) {
doneBlock(nil,SDImageCacheTypeNone);
returnnil;
}
UIImage*image = [selfimageFromMemoryCacheForKey:key];//先从NSCache内存的缓存中读取
if(image) {
doneBlock(image,SDImageCacheTypeMemory);// 如果读到直接block回去,并返回读取的位置
returnnil;
}
NSOperation*operation = [NSOperationnew];
//如果内存的缓存里没有读到的话,由于磁盘数据较多,避免线程的卡顿,需要开启单独的异步队列从磁盘中读取。
dispatch_async(self.ioQueue, ^{
if(operation.isCancelled) {
return;
}
@autoreleasepool{
UIImage*diskImage = [selfdiskImageForKey:key];//从磁盘中读取,key是图片链接,会根据第一步生成对应的唯一的文件名,从沙河中获取
if(diskImage &&self.shouldCacheImagesInMemory) {
NSUIntegercost =SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];//如果从沙河中读取到图片,先将其存储到内存的缓存中,方便下次的读取
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage,SDImageCacheTypeDisk);//读取成功,返回主线程并block回去
});
}
});
returnoperation;
}
5: 图片的解码,从磁盘中读取时,以及从网络上下载完成图片时,都会先解码再返回
- (UIImage*)diskImageForKey:(NSString*)key {
NSData*data = [selfdiskImageDataBySearchingAllPathsForKey:key];//从磁盘里读取存储的data数据
if(data) {
UIImage*image = [UIImagesd_imageWithData:data];
image = [selfscaledImageForKey:key image:image];
if(self.shouldDecompressImages) {
image = [UIImagedecodedImageWithImage:image];//图片的解码,详见SDWebImageDecoder这个类
}
returnimage;
}else{
returnnil;
}
}
6:自动清理缓存的时机 ,无论是APP退到后台,或者APP被直接杀掉,都会进行图片的清理
[[NSNotificationCenterdefaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenterdefaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenterdefaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
该方法里会检查图片的有效期,默认是7天,如果过期则删除。 用到了NSURLContentModificationDateKey这个key,表示文件的修改时间,多数情况下都是图片文件的创建时间,因为基本下载好了以后就不会去修改了。
staticconstNSIntegerkDefaultCacheMaxCacheAge =60*60*24*7;// 1 week
另如果你设置了最大的图片存储空间,那么系统也会在同一时间点做检查并清理。即使未过期,也会清理一些,按照文件创建的时间来排序做清理,更早创建的优先被清理。比较有意思的就是,清理工作会持续到图片只占你设定值的一半。