SDWebImage源码浅析

作者: 夜满西楼 | 来源:发表于2017-11-14 16:21 被阅读683次

    1、平时开发的过程中用到过三方库吗?
    2、使用三方库的过程中遇到过什么问题吗?
    3、有读过优秀三方的源码么?
    4、知道三方库底层怎么实现的吗?

    写在开始之前

    在很多水友相亲的过程中,经常会被问到类似的问题,有些人能够言简意赅的把某框架的优缺点表达出来(心中:我凑,还好我昨天背了一下,这个逼我一定要装好),有些人却还是停留在简单使用API的阶段,具体怎么实现却支支吾吾的说不清楚(心中万马奔腾,麻痹的,这么底层的东西也要问吗?)
    iOS日常开发中,常用的开源三方库有很多AFNetworking、SDWebImage、MJRefresh、YYKit系列等,今天我们就先来说说SDWebImage
    SDWebImage的源码第一次看还是大概2年前,当时还是用的NSURLConnection来下载图片的,时光荏苒,SDWebImage早已改变成了NSURLSession来下载图片,并且不断的优化,Github也多了1W多Star,而我还是当年那个小菜逼,说多了都是眼泪

    最近一次面试的时候,被问到一个问题,
    面试官:UITableView的5个cell同时下载一个相同图片,SDWebImage底层怎么处理的?
    我:之前看过,记不清了(当时我的表情是懵逼的,之前只是草草的看过一遍,而且还有很多地方都看不懂)
    面试官:没关系,如果要是你自己做,你会怎么做?
    我:弄一个串行队列,第一个任务下载完之后,缓存,然后后边的任务就可以直接取缓存
    面试官:如果我同时需要5个图片的下载进度呢?
    我:当是真心有点凌乱了,然后思(懵)考(逼)了2分钟
    面试官:好吧,今天的面试就先到这里吧

    带着面试的问题,我老老实实的又从GitHub上下载个SDWebImage-4.2.2,花了一天的时间,又看了一遍,现在把看明白的东西记录一下。

    1、SDWebImage工作流程

    这是GitHub上提供的,我顺手给牵了过来,再说了,文化人的事,能叫偷么?

    虽然SDWebImage的主要工作流程很多水友都能说出个大概,我还是简单说一下我的理解吧,像我这种大龄程序员,说不定哪天就忘了,将来还能回来翻翻笔记。

    Step1:调用加载图片API sd_setImageWithURL: 系列

     [imageView sd_setImageWithURL:[NSURL  URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                 placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
    

    调用API之后,SDWebImage会判断是否显示占位图,如果让显示就先显示占位图

    // 如果options != SDWebImageDelayPlaceholder
    if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
            });
        }
    

    写到这里再啰嗦几句,说出来你可能不信,options & SDWebImageDelayPlaceholder这种写法,在昨天之前我是看不懂的,看到逻辑与&,我努力回想了下当年的秃顶计算机老师是怎么讲的,但是没想起来,后来琢磨了下,应该是0 & 0 = 0, 1 & 0 = 0, 1 & 1 = 1,结合SDWebImageDelayPlaceholder的定义

         * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
         * of the placeholder image until after the image has finished loading.
         */
        SDWebImageDelayPlaceholder = 1 << 9,
    
    1 << 9 ==> 0000 0000 0000 0001 << 9 ==> 0000 0010 0000 0000
    
    假设options = SDWebImageRetryFailed = 1 << 0,
    options & SDWebImageDelayPlaceholder 
    ==>    0000 0000 0000 0001 
        &  0000 0010 0000 0000
    ===========================
           0000 0000 0000 0000  = 0
    
    假设options = SDWebImageDelayPlaceholder  = 1 << 9
    options & SDWebImageDelayPlaceholder 
    ==>    0000 0010 0000 0000 
        &  0000 0010 0000 0000
    =========================== 
           0000 0010 0000 0000  = 1 << 9 = !0
    
    option逻辑与(&)上自己本身结果是自己,非零,options逻辑与(&)上非自身的其他枚举值,结果都是0,这个逻辑于(&)在这里跟 == 的作用是一样的,就是判断options的枚举值,
    options & SDWebImageDelayPlaceholder ==> options == SDWebImageDelayPlaceholder
    what the fk? 
    原来只是个options == SDWebImageDelayPlaceholder判断,哎,都是吃文化低的亏,啰嗦了这么多也不知道表达清楚没。
    

    Step2:SDImageCache以URL为key在imageCache中查找图片

    首先在memCache中查找,如果能找到image,执行回调block,把image传回去,如果memCache中没有找到,会开启一个异步线程去磁盘上查找,如果找到image,保存到memCache中,如果没有找到,返回nil,查找完成。

    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        NSData *diskData = nil;
        if (image.images) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
    
        @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);
                });
            }
        }
    });
    

    Step3:SDWebImageDownloader下载图片

    如果内存和磁盘上都没有查询到URLString对应的image,就会让imageDownloader去下载图片,根据URL创建一个request,然后根据request创建一个sessionTask开始下载

    NSTimeInterval timeoutInterval = sself.downloadTimeout;
    if (timeoutInterval == 0.0) {
        timeoutInterval = 15.0;
    }
    
    // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
    NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                cachePolicy:cachePolicy
                                                            timeoutInterval:timeoutInterval];
    
    request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
    request.HTTPShouldUsePipelining = YES;
    if (sself.headersFilter) {
        request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
    }
    else {
        request.allHTTPHeaderFields = sself.HTTPHeaders;
    }
    SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
    operation.shouldDecompressImages = sself.shouldDecompressImages;
    
    if (sself.urlCredential) {
        operation.credential = sself.urlCredential;
    } else if (sself.username && sself.password) {
        operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
    }
    
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    
    [sself.downloadQueue addOperation:operation];
    

    operation添加到operationQueue中,自动开启下载任务,下载的过程中通过进度block回传下载进度,下载完成后解码转码,调用下载完成回调block把image对象回传。

    Step4:SDImageCache存储图片

    默认转码后的图片会缓存到内存中,如果同时需要缓存到磁盘上,才会开启异步IO队列通过NSFileManager把图片写入到本地磁盘,磁盘上图片的名字是经过MD5处理后的URLString。

    // if memory cache is enabled
    if (self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                NSData *data = imageData;
                if (!data && image) {
                    // If we do not have any data to detect image format, use PNG format
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
                }
                [self storeImageDataToDisk:data forKey:key];
            }
            
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    }
    
    - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
        if (!imageData || !key) {
            return;
        }
        
        [self checkIfQueueIsIOQueue];
        
        if (![_fileManager fileExistsAtPath:_diskCachePath]) {
            [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        
        // get cache Path for image key
        NSString *cachePathForKey = [self defaultCachePathForKey:key];
        // transform to NSUrl
        NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
        
        [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
        
        // disable iCloud backup
        if (self.config.shouldDisableiCloud) {
            [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
        }
    }
    

    这就是SDWebImage加载一张图片的大致流程了,其实SDWebImage里面做了很多细节优化处理。让我们接着往下look

    3、SDWebImage底层优化

    • 1、无效URL的处理
      @property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
      SDWebImageManager维护了一个黑名单存放图片下载失败的URL,每次根据URLString查询图片的时候,会先去黑名单中查询,目标URL是否在黑名单中
    BOOL isFailedUrl = NO;
        if (url) {
            //为了线程安全
            @synchronized (self.failedURLs) {
                isFailedUrl = [self.failedURLs containsObject:url];
            }
        }
    
    

    如果URL在黑名单中,直接执行回调Block,回传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];
       } 
     }
    
    • 2、高并发相同URL请求的处理
      @property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
      SD内部每一个下载请求,对应一个SDWebImageDownloaderOperation,SDWebImageDownloader通过URLOperations属性来维护这个operation。
    dispatch_barrier_sync(self.barrierQueue, ^{
        //根据URLString查询是否有正在下载的操作
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {
            operation = createCallback();
            //把下载操作添加到URLOperations中
            self.URLOperations[url] = operation;
    
            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
                dispatch_barrier_sync(self.barrierQueue, ^{
                    SDWebImageDownloaderOperation *soperation = woperation;
                    if (!soperation) return;
                    if (self.URLOperations[url] == soperation) {
                        //下完成后,把该URL的下载操作从URLOperations移除
                        [self.URLOperations removeObjectForKey:url];
                    };
                });
            };
        }
        //已经有该URLString对应的下载任务存在,保存新任务的进度回调block和完成回调block
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });
    

    NSURLSessionDataDelegate- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data方法中可以看到如下代码

    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        //循环执行相同URL的进度回调Block
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
    

    简单的说,就是相同的URL只开启一个下载任务,在下载的过程中,把下载进度分别通知给该URL对应的其他操作,既节约流量、又兼顾所有任务的下载进度。下载完成的回调block同理执行。

    • 3、高并发不同URL请求的处理
      UITableView的多个cell同时加载图片的时候,就会出现高并发的情况
    //默认最大并发数
    _downloadQueue.maxConcurrentOperationCount = 6;
    
    //session的初始化
    self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                     delegate:self
                                                delegateQueue:nil];
    

    系统API对delegateQueue参数的描述是If nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.
    SDWebImage的高并发下载任务是在一个并行队列,默认支持最大的并发数是6,默认并发任务执行顺序是FIFO(first in first out),如果设置任务的执行顺序为LIFO(last in first out)

    if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
        //设置操作之间的依赖,新添加的operation被旧的operation依赖,来实现后进先出
        [sself.lastAddedOperation addDependency:operation];
        sself.lastAddedOperation = operation;
    }
    

    SDWebImageDownloader在接收到NSURLSessionDataDelegate代理方法回调的时候,通过NSURLSessionDataTask获取到对应的SDWebImageDownloaderOperation,把delegate方法转发给SDWebImageDownloaderOperation,避免数据错乱。

    ...
    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
        // Identify the operation that runs this task and pass it the delegate method
        SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
        //把代理方法转发给SDWebImageDownloaderOperation
        [dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
    }
    
    //根据NSURLSessionTask获取对应的SDWebImageDownloaderOperation
    - (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
        SDWebImageDownloaderOperation *returnOperation = nil;
        for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
            if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
                returnOperation = operation;
                break;
            }
        }
        return returnOperation;
    }
    ...
    
    • 4、缓存管理策略
      SDWebImage的内存管理由SDImageCache负责,分别监听了内存警告、应用将释放、进入后台三个通知
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(clearMemory)
                                                 name:UIApplicationDidReceiveMemoryWarningNotification
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(deleteOldFiles)
                                                 name:UIApplicationWillTerminateNotification
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(backgroundDeleteOldFiles)
                                                 name:UIApplicationDidEnterBackgroundNotification
                                               object:nil];
    
    //收到内存警告通知,把内存中缓存的图片清空
    - (void)clearMemory {
        [self.memCache removeAllObjects];
    }
    

    SDImageCache图片磁盘缓存的时长默认是1周

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

    在每次收到进入后台、应用将要释放通知后,SDWebImage会检查磁盘上的图片,如果过期就清理

    // Remove files that are older than the expiration date;
    //如果图片过期,记录过期图片URL
    NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
    if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
        [urlsToDelete addObject:fileURL];
        continue;
    }
    
    //循环删除过期图片
    for (NSURL *fileURL in urlsToDelete) {
        [_fileManager removeItemAtURL:fileURL error:nil];
    }
    

    如果设置最大的缓存空间,在收到进入后台、应用将要释放通知后,判断使用当前空间使用超过设置的最大空间的50%后,开始清理,按照修改时间排序后,从修改时间最早的开始清理,直到使用空间小于缓存空间50%后结束。

    // If our remaining disk cache exceeds a configured maximum size, perform a second
    // size-based cleanup pass.  We delete the oldest files first.
    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;
    
        // Sort the remaining cache files by their last modification time (oldest first).
        NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                 usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                     return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                 }];
    
        // Delete files until we fall below our desired cache size.
        for (NSURL *fileURL in sortedFiles) {
            if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
    
                if (currentCacheSize < desiredCacheSize) {
                    break;
                }
            }
        }
    }
    
    • 5、解码转码


    说实话,解码转码这块看的还不太明白,等我看明白了,再回来补上...

    相关文章

      网友评论

        本文标题:SDWebImage源码浅析

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