iOS开发首页投稿(暂停使用,暂停投稿)程序员

SDWebImage源码浅析

2017-11-14  本文已影响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底层优化

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];
   } 
 }
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同理执行。

//默认最大并发数
_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;
}
...
[[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;
            }
        }
    }
}

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

上一篇下一篇

猜你喜欢

热点阅读