039* SDWebImage底层原理、查找、超出释放、下载编码

1:底层原理

在之前我写过SDWebImage的使用方法,主要是用与获取网络图片,没有看过的朋友可以看看。

这篇文章将主要介绍SDWebImage的实现原理,主要针对于获取网络图片的原理,如果没有第三方我们该怎么去做,当然我知识用文字去介绍,我想花大把的时间去深入理解我们用不到的东西,是很不值得的,不过兴趣的朋友可以去其他博客上查找相应信息,毕竟学无止境。好了下面开始进入正题。

1)当我门需要获取网络图片的时候,我们首先需要的便是URl没有URl什么都没有,获得URL后我们SDWebImage实现的并不是直接去请求网路,而是检查图片缓存中有没有和URl相关的图片,如果有则直接返回image,如果没有则进行下一步。

2)当图片缓存中没有图片时,SDWebImage依旧不会直从网络上获取,而是检查沙盒中是否存在图片,如果存在,则把沙盒中对应的图片存进image缓存中,然后按着第一步的判断进行。

3)如果沙盒中也不存在,则显示占位图,然后根据图片的下载队列缓存判断是否正在下载,如果下载则等待,避免二次下载。如果不存则创建下载队列,下载完毕后将下载操作从队列中清除,并且将image存入图片缓存中。

4)刷新UI(当然根据实际情况操作)将image存入沙盒缓存。

1、入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

2、进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载queryDiskCacheForKey:delegate:userInfo:.

3、先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

4、SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

5、如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

6、根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

7、如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

8、如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

9、共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

10、图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

11、connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

12、图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

13、在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

14、通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

15、SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

16、SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。

17、SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

2:为什么要对UIImage进行解码呢?难道不能直接使用吗?

在我们使用 UIImage 的时候,创建的图片通常不会直接加载到内存,而是在渲染的时候默认在主线程上再进行解码并加载到内存。这就会导致 UIImage 在渲染的时候效率上不是那么高效。为了提高效率所以在SDWebImage中就采取在子线程中进行解码图片。

这里再介绍下为什么创建图像的时候是需要解码的因为一般下载的图片或者是我们手动拖进工程的图片都是PNG 或者JPEG或者是其他格式的图片,这些图片都是经过编码压缩后的图片数据,并不是我们的控件可以直接显示的位图,如果我们直接使用加载渲染图片到手机上的时候,系统默认会在主线程立即进行图片的解码工作,这个过程就是把图片数据解码成可以供给控件直接显示的位图数据,由于这个解码操作比较耗时,并且默认是在主线程进行,所以如果加载过多的图片的话肯定是会发生卡顿现象的。 

3:其他问题

1:磁盘目录位于哪里?

缓存在磁盘沙盒目录下Library/Caches
二级目录为~/Library/Caches/default/com.hackemist.SDWebImageCache.default

- (instancetype)init { return [self initWithNamespace:@"default"]; //   ~Library/Caches/default }

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns { NSString *path = [self makeDiskCachePath:ns]; return [self initWithNamespace:ns diskCacheDirectory:path];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory { if ((self = [super init])) { NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns] // Init the disk cache if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else { NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        } //  _diskCachePath = ~/Library/Caches/default/com.hackemist.SDWebImageCache.default }

2:最大并发数、超时时长?

_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadTimeout = 15.0;

3:图片如何命名?缓存是url。

//写入缓存 NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];

写入磁盘时、用url的MD5编码作为key。可以防止文件名过长

- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key { const char *str = key.UTF8String; if (str == NULL) {
        str = "";
    } unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r); NSURL *keyURL = [NSURL URLWithString:key]; NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension; NSString *filename = [NSString stringWithFormat:@"xxxxxxxxxxxxxxxx%@",
                          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], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]]; return filename; //key == https://gss2.bdstatic.com/-fo3dSag_xI4khGkpoWK1HF6hhy/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=034361ab922397ddc274905638ebd9d2/d31b0ef41bd5ad64dddebb.jpg; //filename == f029945f95894e152771806785bc4f18.jpg; }

4:如何识别图片类型?

通过NSData数据的第一个字符进行判断。

+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data { if (!data) { return SDImageFormatUndefined;
    } // File signatures table: http://www.garykessler.net/library/file_sigs.html uint8_t c;
    [data getBytes:&c length:1]; switch (c) { case 0xFF: return SDImageFormatJPEG; case 0x89: return SDImageFormatPNG; case 0x47: return SDImageFormatGIF; case 0x49: case 0x4D: return SDImageFormatTIFF; case 0x52: { if (data.length >= 12) { //RIFF....WEBP NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { return SDImageFormatWebP;
                }
            } break;
        } case 0x00: { if (data.length >= 12) { //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding]; if ([testString isEqualToString:@"ftypheic"]
                    || [testString isEqualToString:@"ftypheix"]
                    || [testString isEqualToString:@"ftyphevc"]
                    || [testString isEqualToString:@"ftyphevx"]) { return SDImageFormatHEIC;
                }
            } break;
        }
    } return SDImageFormatUndefined;
}

5:所查找到的图片的来源?

typedef NS_ENUM(NSInteger, SDImageCacheType) { /**
     * 从网上下载
    */ SDImageCacheTypeNone, /**
     * 从磁盘获得
     */ SDImageCacheTypeDisk, /**
     * 从内存获得
     */ SDImageCacheTypeMemory
};

6:所有下载的图片都将被写入缓存?磁盘呢?何时缓存的?

磁盘不是强制写入。从枚举SDWebImageOptions可见

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { /**
     *  禁用磁盘缓存
     */ SDWebImageCacheMemoryOnly = 1 << 2,
}

7:而Memory缓存应该是必须写入的(因为我并没找到哪里可以禁止)。
缓存的时间点、有两个(开发者也可以主动缓存)、且都是由SDWebImageManager进行。
其一是下载成功后、自动保存。或者开发者通过代理处理图片并返回后缓存

- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;


=========>>SDWebImageManager //获取转换用户后的图片 
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

//用户处理成功 if (transformedImage && finished) { BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; //用户处理的后若未生成新的图片、则保存下载的二进制文件。 //不然则由imageCache内部生成二进制文件保存 [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil]; }

其二是当缓存中没有、但是从硬盘中查询到了图片。

@autoreleasepool { //搜索硬盘 NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage = [self diskImageForKey:key]; //缓存到内存、默认为YES if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); //使用NSChache缓存。 [self.memCache setObject:diskImage forKey:key cost:cost];
        } if (doneBlock) { dispatch_async(dispatch_get_main_queue(), ^{
               doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
             });
        }
}

8:磁盘缓存的时长?清理操作的时间点?

默认为一周

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

调用的时机为

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification object:nil];
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification object:nil];

也就是当程序退出到后台、或者被杀死的时候。
这里、还有另外一个点。150秒

- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication"); 
  
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)])
{

  return; }
UIApplication
*application = [UIApplication performSelector:@selector(sharedApplication)]; //后台任务标识--注册一个后台任务
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
/
  /
超时(大概150秒?)自动结束后台任务 //结束后台任务 [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; [self deleteOldFilesWithCompletionBlock:^{ //结束后台任务 [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; }

正常程序在进入后台后、虽然可以继续执行任务。但是在时间很短内就会被挂起待机。
Long-Running可以让系统为app再多分配一些时间来处理一些耗时任务。

9:磁盘清理的原则?

首先、通过时间进行清理。(最后修改时间>一周)
然后、根据占据内存大小进行清理。(如果占据内存大于上限、则按时间排序、删除到上限的1/2。)
这里我并没有看到使用频率优先级判断、所以应该是没有。

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock { //异步清理超时图片 dispatch_async(self.ioQueue, ^{ //获取磁盘目录 NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; //NSURLIsDirectoryKey 判断是否为目录 //NSURLContentModificationDateKey 判断最后修改时间 //NSURLTotalFileAllocatedSizeKey 判断文件大小 NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]; //模具器--遍历磁盘路径下的文件 NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; //计算一周前(需要释放)、的时间 NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge]; //保存缓存文件Dic NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary]; //缓存总大小 NSUInteger currentCacheSize = 0; //需要删除的url路径 NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; //遍历磁盘文件枚举器 for (NSURL *fileURL in fileEnumerator) { NSError *error; //获取每个文件所对应的三个参数(resourceKeys) NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error]; // Skip directories and errors. if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) { //如果是文件夹则跳过 continue;
            } // Remove files that are older than the expiration date; NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { //如果时间超过指定日期、加入删除数组。跳过 [urlsToDelete addObject:fileURL]; continue;
            } //获取文件大小、并且把路径与大小存入字典。 // Store a reference to this file and account for its total size. NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        } //遍历删除文件 for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        } //如果剩余文件大小仍超过阈值 //优先删除最老的文件 if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) { // Target half of our maximum cache size for this cleanup pass. const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2; // 将剩余的文件按修改时间排序 NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }]; // 删除文件 for (NSURL *fileURL in sortedFiles) { if ([_fileManager removeItemAtURL:fileURL error:nil]) { NSDictionary *resourceValues = cacheFiles[fileURL]; NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue; //直到低于阈值的二分之一 if (currentCacheSize < desiredCacheSize) { break;
                    }
                }
            }
        } //回调给主线程 if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

posted on 2018-07-17 09:34  风zk  阅读(185)  评论(0编辑  收藏  举报

导航