前言
有段时间没有写文了, 确实最近有些迷茫的感觉, 总是在纠结如何提高自己的综合竞争力, 看了看计算机基础的书, 拓展了一下其他技术栈的视野, 还饶有兴致的和朋友讨论如何才能称为高级 iOS 工程师当然, 除了扎实的数据结构算法基础, 你需要问自己一个问题: 我能写出像知名开源库一样的代码么?
这个评判标准就相当清楚了, 能写出优秀代码的前提是能读懂优秀的代码
都说开源库最有价值的是思路, 其次是代码细节, 而我觉得, 我们最需要学习的就是代码细节 (大牛绕道, 不想找虐) 知名开源库必然是经过大众的验证, 在严谨性和逻辑性方面的处理都是业界领先水平, 所以, 如果你觉得遇到了所谓的瓶颈, 多去看看开源库, 并且仔细看, 相信你会得到提升
代码既艺术, 本文我将分析 SDWebImage 的实现细节, 采用的是顺藤摸瓜大法
说明: 本文是基于 SDWebImage 4.1.2 版本, 有些解释为了方便会写在代码里面, 考虑到篇幅原因, 有些方法的实现就不贴出来了
本文会涉及到一些线程问题, 如想了解可以看我另一篇文章
iOS 多线程全面解读(二):GCD
建议在电脑上观看, 手机上体验不佳
关于 dispatch_main_async_safe(block)
在 SD 的宏文件里面有一个宏有点意思:
- #define dispatch_main_async_safe(block)\
- if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
- block();\
- } else {\
- dispatch_async(dispatch_get_main_queue(), block);\
- }
因为在老版本的 SD 里面, 它是这样实现的:
- #define dispatch_main_async_safe(block)\
- if ([NSThread isMainThread]) {\
- block();\
- } else {\
- dispatch_async(dispatch_get_main_queue(), block);\
- }
上面是通过比较当前队列和主队列的名字判断是否在主队列;
下面是通过直接通过 [NSThread isMainThread] 来判断当前是否在主线程
他们的区别在于, 上面的判断能必然获取到主队列; 下面的判断必然获取到主线程, 而非必然获取到主队列
显然, 上面的方法更加的安全, 我们更希望获取到主队列来对 UI 进行操作
1 加载图片方法的落脚点
我们使用的一般是 UIImageView+WebCache 文件系列方法,
- - (void)sd_setImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT;
- - (void)sd_setImageWithURL:(nullable NSURL *)url
- placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT;
- ......
而这些方法的落脚点都是在 UIView+WebCache 的以下方法:
- /* 写在前面:
- SD 通过 UIView+WebCacheOperation 文件里面的代码,
- 将很多操作 (Operation) 都记录了下来, 方便对下载缓存等操作进行控制
- 大家可以先不用管对这些操作的记录具体有什么用, 往后面走一走就自然明白了
- */
- - (void)sd_internalSetImageWithURL:(nullable NSURL *)url
- placeholderImage:(nullable UIImage *)placeholder
- options:(SDWebImageOptions)options
- operationKey:(nullable NSString *)operationKey
- setImageBlock:(nullable SDSetImageBlock)setImageBlock
- progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
- completed:(nullable SDExternalCompletionBlock)completedBlock {
- //* 取消 operationKey 对应的下载操作
- NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
- [self sd_cancelImageLoadOperationWithKey:validOperationKey];
- //* 将下载图片的地址通过 runtime 方法存储起来
- objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- //* SDWebImageOptions 是一个配置下载一些功能的枚举, 这里意思是如果不包含 SDWebImageDelayPlaceholder 枚举值, 就会预先显示我们的占位符
- if (!(options & SDWebImageDelayPlaceholder)) {
- dispatch_main_async_safe(^{
- [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
- });
- }
- if (url) {
- /* 是否显示加载视图 sd_showActivityIndicatorView 这个方法是用 runtime 来保证
- 参数唯一性, 也是 SD 常用的做法 */
- if ([self sd_showActivityIndicatorView]) {
- [self sd_addActivityIndicator];
- }
- /* SDWebImageOperation 是一个协议, 里面只有一个'-cancel'方法需要实现,
- SD 里面的操作都会遵守这个协议, 实现'-cancel'方法(比如这里的下载返回的操作对象)
- 下面这个方法是执行下载操作(之后会去看实现, 这里直接看查找完成的回调)*/
- __weak __typeof(self)wself = self;
- id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
- __strong __typeof (wself) sself = wself;
- [sself sd_removeActivityIndicator];
- /* 这里两个 sself 的非空判断很严谨, 因为可能在获取到主队列的时候它已经释放了
- dispatch_main_async_safe 获取到主队列过后, 执行了一系列 UI 相关的操作, 不做赘述 */
- if (!sself) {
- return;
- }
- dispatch_main_async_safe(^{
- if (!sself) {
- return;
- }
- if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
- completedBlock(image, error, cacheType, url);
- return;
- } else if (image) {
- [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
- [sself sd_setNeedsLayout];
- } else {
- if ((options & SDWebImageDelayPlaceholder)) {
- [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
- [sself sd_setNeedsLayout];
- }
- }
- if (completedBlock && finished) {
- completedBlock(image, error, cacheType, url);
- }
- });
- }];
- //* 将该下载操作抽象加入缓存
- [self sd_setImageLoadOperation:operation forKey:validOperationKey];
- } else {
- //* 失败了移除加载视图, 以及回调
- dispatch_main_async_safe(^{
- [self sd_removeActivityIndicator];
- if (completedBlock) {
- NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
- completedBlock(nil, error, SDImageCacheTypeNone, url);
- }
- });
- }
- }
1.1loadImageWithURL:options:progress:completed: 方法
SD 的抽象提取做得很棒, 代码耦合很低上面代码中
[SDWebImageManager.sharedManager loadImageWithURL: .....]
是图片的下载逻辑, 这里进入该方法看一下:
- /* 写在前面:
- 该方法的任务是找到该图片: 先从缓存中找, 找不到再下载
- 理解该方法前, 需要知道该方法是通过 SDWebImageManager.sharedManager 调用的,
- 所以你会在该方法内看到一些线程安全方面的处理
- */
- - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
- options:(SDWebImageOptions)options
- progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
- completed:(nullable SDInternalCompletionBlock)completedBlock {
- //* 断言的运用, 如果你不使用回调请用其它方法替换
- // Invoking this method without a completedBlock is pointless
- NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
- //* 这里两个地方的处理很细节, 防止意外崩溃
- // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
- // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
- if ([url isKindOfClass:NSString.class]) {
- url = [NSURL URLWithString:(NSString *)url];
- }
- // Prevents app crashing on argument type error like sending NSNull instead of NSURL
- if (![url isKindOfClass:NSURL.class]) {
- url = nil;
- }
- //* SDWebImageCombinedOperation 就是实现了 SDWebImageOperation 协议的一个操作抽象
- __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
- __weak SDWebImageCombinedOperation *weakOperation = operation;
- /* self.failedURLs 类型是这样的: NSMutableSet<NSURL *>, 是一个存储下载失败图片 URL 的可变集合,
- 这里用一个 BOOL 值 isFailedUrl 来记录是否下载失败,
- 使用 @synchronized 来保证该内存的访问安全
- */
- BOOL isFailedUrl = NO;
- if (url) {
- @synchronized (self.failedURLs) {
- isFailedUrl = [self.failedURLs containsObject:url];
- }
- }
- //* URL 没有有效链接 options 不包含下载失败重新下载枚举 self.failedURLs 里面有该 URL 综合判断回调(运算符就不废话了, 自己理解就好)
- if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
- [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
- return operation;
- }
- //* 将该操作加入 self.runningOperations 属性, 顾名思义, 运行中的下载操作
- @synchronized (self.runningOperations) {
- [self.runningOperations addObject:operation];
- }
- NSString *key = [self cacheKeyForURL:url];
- //* 下面这个方法就是从缓存中查找图片(之后会去看实现, 这里直接看查找完成的回调)
- operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
- //* 如果该操作抽象需要取消, 就将该操作抽象从 self.runningOperations 移除(线程安全的), 并且 return
- if (operation.isCancelled) {
- [self safelyRemoveOperationFromRunning:operation];
- return;
- }
- /* (如果缓存不存在 || 需要刷新缓存)
- && (如果 self.delegate 没有实现 imageManager:shouldDownloadImageForURL: 代理方法
- || self.delegate 调用代理方法)
- 注意: || 运算符的特性, 如果 || 左边为真, 那么就不会走右边了所以这里如果 self.delegate 没有实现代理方法, 就不会走 self.delegate 调用代理方法
- */
- if ((!cachedImage || options & SDWebImageRefreshCached)
- && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)]
- || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
- //* 英文注释已经很明白了, 如果有缓存但是需要刷新缓存, 先回调一次之前的缓存
- if (cachedImage && options & SDWebImageRefreshCached) {
- // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
- // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
- [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
- }
- /* 将 options 转换为 SDWebImageDownloaderOptions 类型的配置(下载的配置)
- 用到了 |= 运算符, x |= y 相当于 x = x | y,&= 同理
- ~ 是位反运算符
- */
- // download if no image or requested to refresh anyway, and download allowed by delegate
- SDWebImageDownloaderOptions downloaderOptions = 0;
- if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
- if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
- if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
- if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
- if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
- if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
- if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
- if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
- if (cachedImage && options & SDWebImageRefreshCached) {
- // force progressive off if image already cached but forced refreshing
- downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
- // ignore image read from NSURLCache if image if cached but force refreshing
- downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
- }
- //* 哈哈, 下面这个方法才是真正的调用网络下载啦, 我们也先看完成回调的逻辑, 待会儿去看里面的实现
- SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
- __strong __typeof(weakOperation) strongOperation = weakOperation;
- if (!strongOperation || strongOperation.isCancelled) {
- // Do nothing if the operation was cancelled
- // See #699 for more details
- // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
- } else if (error) {
- //* 如果有错误, 加把 URL 加入 self.failedURLs
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
- if ( error.code != NSURLErrorNotConnectedToInternet
- && error.code != NSURLErrorCancelled
- && error.code != NSURLErrorTimedOut
- && error.code != NSURLErrorInternationalRoamingOff
- && error.code != NSURLErrorDataNotAllowed
- && error.code != NSURLErrorCannotFindHost
- && error.code != NSURLErrorCannotConnectToHost
- && error.code != NSURLErrorNetworkConnectionLost) {
- @synchronized (self.failedURLs) {
- [self.failedURLs addObject:url];
- }
- }
- }
- else {
- //* 如果需要重新下载, 从 self.failedURLs 移除该 URL
- if ((options & SDWebImageRetryFailed)) {
- @synchronized (self.failedURLs) {
- [self.failedURLs removeObject:url];
- }
- }
- /* 下面这一坨都是判断是否需要缓存到磁盘呀, 然后做相应的缓存, 然后回调, 这里就不具体说了, 多看一下就懂至于缓存
- */
- BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
- if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
- // Image refresh hit the NSURLCache cache, do not call the completion block
- } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
- if (transformedImage && finished) {
- BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
- // pass nil if the image was transformed, so we can recalculate the data from the image
- [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
- }
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
- });
- } else {
- if (downloadedImage && finished) {
- [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
- }
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
- }
- }
- //* 完成了把操作抽象从 self.runningOperations 移除
- if (finished) {
- [self safelyRemoveOperationFromRunning:strongOperation];
- }
- }];
- //* 给这个操作抽象的 cancelBlock 赋值, 方便以后用
- operation.cancelBlock = ^{
- [self.imageDownloader cancel:subOperationToken];
- __strong __typeof(weakOperation) strongOperation = weakOperation;
- [self safelyRemoveOperationFromRunning:strongOperation];
- };
- } else if (cachedImage) {
- //* 如果有缓存, 就很简单了, 直接回调
- __strong __typeof(weakOperation) strongOperation = weakOperation;
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
- [self safelyRemoveOperationFromRunning:operation];
- } else {
- // Image not in cache and download disallowed by delegate
- __strong __typeof(weakOperation) strongOperation = weakOperation;
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
- [self safelyRemoveOperationFromRunning:operation];
- }
- }];
- return operation;
- }
1.1.1queryCacheOperationForKey: done: 方法
[self.imageCache queryCacheOperationForKey: ......]
行, 进入该方法的实现:
- - (nullable NSOperation * ) queryCacheOperationForKey: (nullable NSString * ) key done: (nullable SDCacheQueryCompletedBlock) doneBlock {
- //* key 不存在 return
- if (!key) {
- if (doneBlock) {
- doneBlock(nil, nil, SDImageCacheTypeNone);
- }
- return nil;
- }
- //* 在内存缓存中找图片, SD 是使用的 NSCache 来进行缓存, 它有着线程安全和自动清除多余内存的好处(关于对 gif 的处理, 不说明了, 篇幅太长)
- // First check the in-memory cache...
- UIImage * image = [self imageFromMemoryCacheForKey: key];
- if (image) {
- NSData * diskData = nil;
- if ([image isGIF]) {
- diskData = [self diskImageDataBySearchingAllPathsForKey: key];
- }
- if (doneBlock) {
- doneBlock(image, diskData, SDImageCacheTypeMemory);
- }
- return nil;
- }
- /* 如果在内存中未找到, 就开始从磁盘找了
- _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
- ioQueue 是一个串行队列
- */
- //* 创建一个任务
- NSOperation * operation = [NSOperation new];
- // 获取串行队列 ioQueue, 异步执行
- dispatch_async(self.ioQueue, ^{
- if (operation.isCancelled) {
- // do not call the completion if cancelled
- return;
- }
- /* 这里使用自动释放池是为了及时的释放中间变量,
- 如果该方法调用频繁, ioQueue 有着大量任务一直不能结束,
- 那么中间变量就不能及时的释放会造成内存负担
- */
- @autoreleasepool {
- NSData * diskData = [self diskImageDataBySearchingAllPathsForKey: key];
- UIImage * diskImage = [self diskImageForKey: key];
- if (diskImage && self.config.shouldCacheImagesInMemory) {
- NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject: diskImage forKey: key cost: cost];
- }
- if (doneBlock) {
- dispatch_async(dispatch_get_main_queue(), ^{
- doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
- });
- }
- }
- });
- //* 把这个任务返回
- return operation;
- }
1.1.2downloadImageWithURL:options: progress:completed: 方法(SDWebImageDownloader 文件)
下载文件的逻辑本身是比较基础的东西, 这里我重点就 SD 如何插入任务和删除任务做一个简单的说明
首先, 在
SDWebImageDownloader
中, 定义了一个并行队列
- _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
- // 取消下载方法
- - (void)cancel:(nullable SDWebImageDownloadToken *)token {
- //* 取消下载操作时, 加入了一个异步的线程栅栏, 以保证该删除逻辑执行的同时, 不会有下载任务在执行, 从而避免了冲突
- dispatch_barrier_async(self.barrierQueue, ^{
- SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
- BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
- if (canceled) {
- [self.URLOperations removeObjectForKey:token.url];
- }
- });
- }
- // 添加下载方法
- - (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
- completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
- forURL:(nullable NSURL *)url
- createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
- // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
- if (url == nil) {
- if (completedBlock != nil) {
- completedBlock(nil, nil, nil, NO);
- }
- return nil;
- }
- __block SDWebImageDownloadToken *token = nil;
- /* 这里添加下载任务, 用了一个同步的线程栅栏, 以保证该任务的执行优先级为最高
- */
- dispatch_barrier_sync(self.barrierQueue, ^{
- SDWebImageDownloaderOperation *operation = self.URLOperations[url];
- if (!operation) {
- operation = createCallback();
- self.URLOperations[url] = operation;
- __weak SDWebImageDownloaderOperation *woperation = operation;
- operation.completionBlock = ^{
- /* 下载任务完成了过后, 还要将 url 从 self.URLOperations 中移除 , 再次用到线程栅栏 */
- dispatch_barrier_sync(self.barrierQueue, ^{
- SDWebImageDownloaderOperation *soperation = woperation;
- if (!soperation) return;
- if (self.URLOperations[url] == soperation) {
- [self.URLOperations removeObjectForKey:url];
- };
- });
- };
- }
- id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
- token = [SDWebImageDownloadToken new];
- token.url = url;
- token.downloadOperationCancelToken = downloadOperationCancelToken;
- });
- return token;
- }
未完待续...
来源: http://www.jianshu.com/p/68a41a7ee2c2