美文网首页
SDWebImage的源码阅读

SDWebImage的源码阅读

作者: Sweet丶 | 来源:发表于2021-01-11 17:16 被阅读0次
一、使用

我们在项目中使用SDWebImage时首先会配置项目缓存策略

    // 设置图片缓存时间 3天 ===> 默认是7天
    [SDImageCacheConfig defaultCacheConfig].maxDiskAge = 3*60*60*24;
    // 设置图片本地缓存 最多 250M===> 默认为0,无限制
    [SDImageCacheConfig defaultCacheConfig].maxDiskSize = 1024*250;
    // 设置缓存图片个数最多1000张===> 默认为0,无限制
    [SDImageCacheConfig defaultCacheConfig].maxMemoryCount = 1000;

其次是使用

[self.ImageView sd_setImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:[UIImage imageNamed:@"me_login_avatar"]];
二、实现原理

下面是通过源码来解读SDWebImage如何实现图片下载、图片解码、图片内存缓存、图片磁盘缓存、图片磁盘定期清理功能的?

1. imageView调用方法sd_setImageWithURL:时,内部是调用UIView+WebCache中的sd_internalSetImageWithURL:, 里面会做的事情有:
// UIImageView+WebCache调用下面方法
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder;

// 下面是简化的UIView+WebCache中的方法
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 1.查找自身View的最近的下载操作,如果有则取消
    validOperationKey = NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    // 2.设置占位图
    dispatch_main_async_safe(^{
        [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
    });
    // 3.通过[SDWebImageManager sharedManager];创建下载操作opration
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options 
    context:context progress:combinedProgressBlock 
    completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType) {
      //... 
    }];
    // 4.将创建好的下载操作opration保存在字典中
    [self sd_setImageLoadOperation:operation forKey:validOperationKey];//validOperationKey是自身类名
}
2. 在创建下载任务operation时做了什么?
  • SDWebImageManager创建下载操作opration
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock {
    //1.创建SDWebImageCombinedOperation* operation里面负责取消下载操作功能
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    //2.将 operation存放到Manager的正在下载操作数组中
    [self.runningOperations addObject:operation];
    //3.调用callCacheProcessForOperation:去执行查找缓存和下载
    [self callCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
}
  • self.imageCache执行查找缓存
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 1.判断是否设置的只允许新的下载
    BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
    if (shouldQueryCache) {
        // 2.根据url得到查找缓存的key,未设置cacheKeyFilter则为url.absoluteString
        NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
        @weakify(operation);
        // 3.让self.imageCache去执行缓存查找,默认cache = [SDImageCache sharedImageCache];
        operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            if (!operation || operation.isCancelled) {
                [self safelyRemoveOperationFromRunning:operation];
                return;
            }
            // 4.去执行下载任务
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // 2.去执行下载任务
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}
  • 创建下载任务并将回调completedBlock
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(nullable SDInternalCompletionBlock)completedBlock {
    if (shouldDownload) {
        // 1.创建下载任务:self.imageLoader = [SDWebImageDownloader sharedDownloader];
        operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                // ...
            }];
    } else if (cachedImage) {
      //2.不需要下载且有缓存图片:调用completedBlock并且将operation移除
    }else{
    // 3.不需要下载且没有缓存图片:调用completedBlock并且将operation移除
    }
}
  • SDWebImageDownloader下载图片任务创建
/// 上一步中调用来到这里(- requestImageWithURL:options:context:progress:completed:)
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // 1.根据url取出对应的下载操作
    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
    if (!operation || operation.isFinished || operation.isCancelled) {
        // 2.没有URL对应的下载操作,则新建一个下载opration == SDWebImageDownloaderOperation类型
        operation = [self createDownloaderOperationWithUrl:url options:options context:context];
        operation.completionBlock = ^{// 任务完成后将operation从self.URLOperations移除
            [self.URLOperations removeObjectForKey:url];
        };
        // 3.将任务保存在self.URLOperations
        self.URLOperations[url] = operation;
        // 4.将下载操作添加到downloadQueue,添加到queue中时就开始下载了
        [self.downloadQueue addOperation:operation];
    }
    // 5.创建SDWebImageDownloadToken *token来负责记录url、request
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    token.downloader = self;
}
  • 创建下载任务SDWebImageDownloaderOperation细节
- (NSOperation *)createDownloaderOperationWithUrl:(nonnull NSURL *)ur
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context {
  // 1.创建mutableRequest
  NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
  // 2. 创建SDWebImageDownloaderOperation对象operation
  operationClass = [SDWebImageDownloaderOperation class];
  NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
  // 3.将self.config.urlCredential证书赋值给operation.credential
  operation.credential = self.config.urlCredential;
}
  • SDWebImageDownloaderOperation如何写下载任务的?
/// 重写了start方法,SDWebImageDownloaderOperation对象添加到queue时就会调用start
- (void)start {
    // 1. 创建下载任务NSURLSessionDataTask
    self.dataTask = [session dataTaskWithRequest:self.request];
    [self.dataTask resume];// 任务开启
}
  • 下载请求时session代理回调的处理, 因为里面主要是做下载进度和结果回调处理的、图片解码
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    // 简化过了
    dispatch_async(self.coderQueue, ^{
       @autoreleasepool {
          UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
          [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
       }
  });
}

/// 解码图片:在self.coderQueue中执行,如果不解码直接设置为Imageview.image,系统会在主线程解码导致滑动卡顿。

3. 下载任务operation完成后做了什么?
// 下载成功后completed的block会调用下面方法
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                      url:(nonnull NSURL *)url
                                  options:(SDWebImageOptions)options
                                  context:(SDWebImageContext *)context
                          downloadedImage:(nullable UIImage *)downloadedImage
                           downloadedData:(nullable NSData *)downloadedData
                                 finished:(BOOL)finished
                                 progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                completed:(nullable SDInternalCompletionBlock)completedBlock {
//   这里简写了逻辑
    [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:storeCacheType completion:nil];
completionBlock(image, data, err0or, cacheType, finished, url);
}
4. 下载完后completionBlock中做了什么? 将图片设置到view上。
5. 下载任务创建前查找缓存时做了什么?
- (NSOperation *)queryCacheOperationForKey:(NSString *)key options:(SDImageCacheOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(SDImageCacheQueryCompletionBlock)doneBlock {
    UIImage *image;
    // 1. 首先从内存中根据key查找:key是url.absoluteString
    if (queryCacheType != SDImageCacheTypeDisk) {
        image = [self imageFromMemoryCacheForKey:key];
    }
    // 2.写好queryDiskBlock
    void(^queryDiskBlock)(void) =  ^{
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            if (image) {
                //2.1 如果是MemoryCache中有,则使用内存缓存的图片
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                //2.2 将从磁盘中找出的图片解码为diskImage
                cacheType = SDImageCacheTypeDisk;
                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = diskImage.sd_memoryCost;
                  // 2.3将解码后的图片设置到MemoryCache中
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
            }
            // 2.4 回调查找完成block
            if (doneBlock) {
                doneBlock(diskImage, diskData, cacheType);
            }
        }
    };
    // 3.将任务添加到self.ioQueue中,让子线程执行queryDiskBlock
    if (shouldQueryDiskSync) {
        dispatch_sync(self.ioQueue, queryDiskBlock);
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
}
  • 从内存中查找图片的细节
SDMemoryCache类,继承于NSCache类,用法是一样的,这个类中新增的功能
[self.memCache objectForKey:key];
  • 从磁盘中查找的细节
- (NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    NSData *data = [self.diskCache dataForKey:key];
    if (data) {
        return data;
    }
}

// SDDiskCache类中
- (NSData *)dataForKey:(NSString *)key {
  // 根据这个key,创建filepath:
    NSString *filePath = [self cachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }
    return nil;
}

// filePath是怎么创建的?文件夹+文件名
if (directory != nil) {
// 1.首先是文件夹:如果有自己设置则是自己设置的文件夹/ns
    _diskCachePath = [directory stringByAppendingPathComponent:ns];
 } else {
// 2.默认创建的文件夹~/Library/Caches/com.hackemist.SDImageCache/default
    NSString *path = [[[self userCacheDirectory] stringByAppendingPathComponent:@"com.hackemist.SDImageCache"] stringByAppendingPathComponent:ns];
    _diskCachePath = path;
}
// 文件名:因为key=url. absoluteString,所以这里的文件名就是url经过MD5后+文件类型名后缀
static inline NSString *SDDiskCacheFileNameForKey(NSString * key) {
    const char *str = key.UTF8String;
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSURL *keyURL = [NSURL URLWithString:key];
// ext==文件类型名
    NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
    NSString *filename = [NSString stringWithFormat:@"%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], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
    return filename;
}
三、缓存策略的实现

SDWebImage缓存部分实现源码解析这篇博客已经讲得很详细了。这里罗列一下重要事项:

  1. 有内存缓存SDMemoryCache、内存缓存弱引用weakCache和磁盘缓存SDDiskCache三种方式。
  2. 对于内存缓存, 监听了系统内存警告的通知,收到通知时清楚所有的图片内存缓存。
/// 清除的时候在内存中仍然维持着一份弱引用weakCache,只要这些弱引用的对象仍然被其他对象(比如UIImageView)所持有,那仍然会在该类中找到。
[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveMemoryWarning:)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
    //移除父类的对象
    [super removeAllObjects];
}
  1. 磁盘写入图片时,首先是将图片转化成NSData,然后再写入文件中。读取时先读取出NSData,再解码为图片。
  2. 清除图片缓存的时机是程序进后台和退出的时候
        //注册通知,大意就是在程序进后台和退出的时候,清理一下磁盘
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];

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



- (void)applicationWillTerminate:(NSNotification *)notification {
    [self deleteOldFilesWithCompletionBlock:nil];
}
- (void)applicationDidEnterBackground:(NSNotification *)notification {
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
    [self deleteOldFilesWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}
  1. 删除时是先删除过期的文件,再判断总存储图片的大小来一个一个删,直到符合设定的最大存储的一半.

  2. 下载时图片最大并发下载数和超时时间默认

- (instancetype)init {
    self = [super init];
    if (self) {
        _maxConcurrentDownloads = 6; // 6张图片
        _downloadTimeout = 15.0; // 15秒超时
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;// 下载任务是先进先出
    }
    return self;
}
四、扩展使用

图片解码的细节,及如何支持gif、webp图片格式的加载。SDWebImage的图片解码源码阅读

阅读源码时会涉及到的技术点:
NSOperation、NSOperationQueue详尽总结

相关文章

网友评论

      本文标题:SDWebImage的源码阅读

      本文链接:https://www.haomeiwen.com/subject/gkqooktx.html