美文网首页
常见三方框架总结-SDWebImage

常见三方框架总结-SDWebImage

作者: lmfei | 来源:发表于2020-03-04 23:33 被阅读0次

    SDWebImage是一个非常好用的图片下载框架,这里记录一下,一是加深印象方,二是方便自己回顾

    下面两张图是SDWebImage官网上的图,第一张是类图,第二张是时序图

    SDWebImageClassDiagram
    通过类图,我们来看下主要模块
    • UIKit的扩展 - 方便我们调用SDWebImage的功能类
    • SDWebImageManager - 该功能模块是用来分配任务
    • SDImageCache - 缓存模块
    • SDWebImageDownloader - 下载模块
    SDWebImageSequenceDiagram
    通过时序图,我们来看下图片的加载流程
    1. 拿到图片URL,先去内存查找,如果找到返回图片,否则进行下一步
    2. 去磁盘查找图片,如果找到返回图片,并写入内存方便下次查找,否则进行下一步
    3. 进行网络请求,请求得到图片,写入磁盘存入内存方便下次查找,并返回图片

    下面我们主要分析下缓存模块与下载模块

    缓存模块

    内存缓存-SDMemoryCache:该类继承自NSCache

    设置内存

    - (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
        [super setObject:obj forKey:key cost:g];
        if (!self.config.shouldUseWeakMemoryCache) {
            return;
        }
        if (key && obj) {
            // Store weak cache
            SD_LOCK(self.weakCacheLock);
            [self.weakCache setObject:obj forKey:key];
            SD_UNLOCK(self.weakCacheLock);
        }
    }
    

    shouldUseWeakMemoryCache这个值可以控制是否将obj存入weakCache对象,如果设置为否,则只存入NSCache中,这将影响我们下面的查找流程

    - (id)objectForKey:(id)key {
        id obj = [super objectForKey:key];
        if (!self.config.shouldUseWeakMemoryCache) {
            return obj;
        }
        if (key && !obj) {
            // Check weak cache
            SD_LOCK(self.weakCacheLock);
            obj = [self.weakCache objectForKey:key];
            SD_UNLOCK(self.weakCacheLock);
            if (obj) {
                // Sync cache
                NSUInteger cost = 0;
                if ([obj isKindOfClass:[UIImage class]]) {
                    cost = [(UIImage *)obj sd_memoryCost];
                }
                [super setObject:obj forKey:key cost:cost];
            }
        }
        return obj;
    }
    

    因为NSCache的清理我们不可控,所以我们存入的值可能会被清理掉,这个时候如果我们shouldUseWeakMemoryCache这个值为YES,那我们在查找存入NSCache的值时,如果查找为空,就会去weakCache里进行一次查找。

    注意:
    NSMapTable:可以为我们提供更多的内存语义

    self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
    

    当前的value是弱引用的,不会对之外的对象产生影响,当value被释放时,NSMapTable会删除这个key-value

    内存缓存的总结,首先内存缓存类SDMemoryCache继承自NSCache,并定义了一个NSMapTable的属性weakCache,然后通过config里的shouldUseWeakMemoryCache这个属性去控制,我们存入的内容是否存入到我们自己的weakCache中,如果存入,那系统清理NSCache取不到存入NSCache中值时,就会去weakCache中去取。

    磁盘缓存

    创建一个目录,

    - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
        return [self initWithNamespace:ns diskCacheDirectory:nil];
    }
    
    - (nullable NSString *)userCacheDirectory {
        NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        return paths.firstObject;
    }
    

    并以图片路径的MD5为图片名称

    static inline NSString * _Nonnull SDDiskCacheFileNameForKey(NSString * _Nullable 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;
        // File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
        if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
            ext = nil;
        }
        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;
    }
    

    在了解了内存与磁盘模块后,我们来看下这个双缓存是如何一个查找流程:

    1. SDWebImageManager中loadImageWithURL ->
      callCacheProcessForOperation
    - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                     url:(nonnull NSURL *)url
                                 options:(SDWebImageOptions)options
                                 context:(nullable SDWebImageContext *)context
                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
                               completed:(nullable SDInternalCompletionBlock)completedBlock {
        // Check whether we should query cache
        BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
        if (shouldQueryCache) {
            NSString *key = [self cacheKeyForURL:url context:context];
            @weakify(operation);
            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) {
                    // Image combined operation cancelled by user
                    [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                    [self safelyRemoveOperationFromRunning:operation];
                    return;
                }
                // Continue download process
                [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
            }];
        } else {
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
        }
    }
    

    在上述方法中执行了SDImageCache的实例imageCache的queryImageForKey方法,再调用queryCacheOperationForKey方法

    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
        ...   
        // First check the in-memory cache...
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        
        if (image) {
            if (options & SDImageCacheDecodeFirstFrameOnly) {
                // Ensure static image
                Class animatedImageClass = image.class;
                if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
    #if SD_MAC
                    image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
    #else
                    image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
    #endif
                }
            } else if (options & SDImageCacheMatchAnimatedImageClass) {
                // Check image class matching
                Class animatedImageClass = image.class;
                Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
                if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
                    image = nil;
                }
            }
        }
    
        BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
        if (shouldQueryMemoryOnly) {
            if (doneBlock) {
                doneBlock(image, nil, SDImageCacheTypeMemory);
            }
            return nil;
        }
        
        // Second check the disk cache...
        NSOperation *operation = [NSOperation new];
        // Check whether we need to synchronously query disk
        // 1. in-memory cache hit & memoryDataSync
        // 2. in-memory cache miss & diskDataSync
        BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                    (!image && options & SDImageCacheQueryDiskDataSync));
        void(^queryDiskBlock)(void) =  ^{
            if (operation.isCancelled) {
                if (doneBlock) {
                    doneBlock(nil, nil, SDImageCacheTypeNone);
                }
                return;
            }
            
            @autoreleasepool {
                NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
                UIImage *diskImage;
                SDImageCacheType cacheType = SDImageCacheTypeNone;
                if (image) {
                    // the image is from in-memory cache, but need image data
                    diskImage = image;
                    cacheType = SDImageCacheTypeMemory;
                } else if (diskData) {
                    cacheType = SDImageCacheTypeDisk;
                    // decode image data only if in-memory cache missed
                    diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                    if (diskImage && self.config.shouldCacheImagesInMemory) {
                        NSUInteger cost = diskImage.sd_memoryCost;
                        [self.memoryCache setObject:diskImage forKey:key cost:cost];
                    }
                }
                
                if (doneBlock) {
                    if (shouldQueryDiskSync) {
                        doneBlock(diskImage, diskData, cacheType);
                    } else {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            doneBlock(diskImage, diskData, cacheType);
                        });
                    }
                }
            }
        };
        
        // Query in ioQueue to keep IO-safe
        if (shouldQueryDiskSync) {
            dispatch_sync(self.ioQueue, queryDiskBlock);
        } else {
            dispatch_async(self.ioQueue, queryDiskBlock);
        }
        
        return operation;
    }
    

    至此,完成图片缓存查找的流程。

    - (void)storeImage:(nullable UIImage *)image
             imageData:(nullable NSData *)imageData
                forKey:(nullable NSString *)key
              toMemory:(BOOL)toMemory
                toDisk:(BOOL)toDisk
            completion:(nullable SDWebImageNoParamsBlock)completionBlock {
        ...
        // if memory cache is enabled
        if (toMemory && self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = image.sd_memoryCost;
            [self.memoryCache setObject:image forKey:key cost:cost];
        }
        
        if (toDisk) {
           ...
           [self _storeImageDataToDisk:data forKey:key];
           ... 
        
        } else {
            if (completionBlock) {
                completionBlock();
            }
        }
    }
    

    通过上面的源码,我们知道了图片在内存中存储的是UIImage对象,在磁盘中存储的则是NSData数据流

    下载流程

    两个重要的类:
    SDWebImageDownloader - 配置下载相关的参数、控制下载队列的先后顺序及最大任务数
    SDWebImageDownloaderOperation - 具体的下载任务

    下载流程代码大体执行顺序
    调用SDWebImageManager的loadImageWithURL
    -callCacheProcessForOperation
    -缓存查找失败后
    -callDownloadProcessForOperation
    -调用imageLoader的requestImageWithURL
    -downloadImageWithURL
    到此downloadImageWithURL会创建任务,并将任务加入OperationQueue,当任务执行时,调用start方法,开始发起请求

    注:
    downloadImageWithURL中调用的createDownloaderOperationWithUrl方法创建一个Operation对象时,可以控制任务执行的优先级

    - (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                      options:(SDWebImageDownloaderOptions)options
                                                                                      context:(nullable SDWebImageContext *)context {
        ...
        if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation
            // This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations
            // Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder
            for (NSOperation *pendingOperation in self.downloadQueue.operations) {
                [pendingOperation addDependency:operation];
            }
        }
        
        return operation;
    }
    

    这个方法最后,当任务优先级选择LIFO,也就是后进先出时,会将所有任务执行条件依赖于当前任务执行结束后

    - (void)URLSession:(NSURLSession *)session
              dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveResponse:(NSURLResponse *)response
     completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
        // Identify the operation that runs this task and pass it the delegate method
        NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
        if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
            [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
        } else {
            if (completionHandler) {
                completionHandler(NSURLSessionResponseAllow);
            }
        }
    }
    

    这个地方将SDWebImageDownloader中请求代理的响应统一放到了SDWebImageDownloaderOperation类中

    生活如此美好,今天就点到为止。。。

    相关文章

      网友评论

          本文标题:常见三方框架总结-SDWebImage

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