断断续续看了SDWebImage的源码,下面按照当初阅读思路,写写SDWebImage中的一些实现逻辑以及基础技术。
SDWebImage框架主要用于加载网络图片,主要是调用UIImageView+WebCache.h中的类别方法即可:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
它主要做了两个事情:
-
异步下载图片
-
异步缓存到本地
那就来看看怎么实现第一点的,在xcode中点击上述方法,在UIImageView+WebCache.m中找到以下代码
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];
...省略...
if (url) {
__weak UIImageView *wself = self; //避免循环引用
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
if (image) { //若图片存在,在主线程中刷新UI
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
...省略...
}
}
这里插播一点,上面的代码用到了宏定义的dispatch_main_sync_safe:
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
目的是想在主线程中执行操作,为什么要判断当前线程是否就是主线程中呢?
如果在主线程中执行dispatch_sync(dispatch_get_main_queue(), block) 同步操作时,会出现死锁问题,因为主线程正在执行当前代码,根本无法将block添加到主队列中。
另外,由于宏定义的原因,断点是跳不进宏定义里面的block。
继续看,先忽略掉一些细节,在上面代码可以看到是调用了SDWebImageManager中的方法:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
点击跟进去,看到以下代码:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
...省略...
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
...省略...
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
//查询本地缓存是否有该URL地址对应的图片
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
...省略...
//当图片不存在或标志位设置了需要更新图片缓存,下载图片
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
...省略....
//调用SDImageDownloader的方法请求下载图片
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
if (weakOperation.isCancelled) {
}
else if (error) { //下载出错
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
//如果不是因为网络异常,那么该URL地址下载图片失败,添加到failedURLs中
if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) {
@synchronized (self.failedURLs) {
if (![self.failedURLs containsObject:url]) {
[self.failedURLs addObject:url];
}
}
}
}
else {
...进行一些处理,主要是缓存图片...
if (finished) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
}
else if (image) { //查询本地缓存后发现图片已经存在
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
else { //缓存没有对应图片,且代理不允许下载图片(代理可决定当图片不在缓存时,是否下载该图片)
// Image not in cache and download disallowed by delegate
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
return operation;
}
上面省略掉了判断该URL地址是否有缓存图片相关功能的代码,暂时忽略缓存部分。从上面看到,它调用了专门负责网络请求下载图片的SDImageDownloader类的方法,去请求下载图片
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
该方法和SDWebImageManager类的下载方法表面上还长的一模一样,额,不多说,接着看
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak SDWebImageDownloader *wself = self;
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.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
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
//创建下载操作
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
//下载进度回调,使用同步的方式
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
//下载完成回调,要等到barrierQueue队列中的进度block执行完后才能执行,并从URLCallbacks字典中移除该URL对应的block信息
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
//下载取消回调,移除该URL对应的block信息
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
operation.shouldDecompressImages = wself.shouldDecompressImages; //是否解压图片
if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
//设置操作的优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//添加操作到队列中
[wself.downloadQueue addOperation:operation];
//通过设置操作的依赖关系将下载顺序改为后进先出
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation]; //最后一个操作依赖于当前操作的完成,这样就后进先出了
wself.lastAddedOperation = operation;
}
}];
return operation;
}
上面的代码主要功能是创建下载operation,把它添加到下载队列,那么下载队列是什么时候创建的呢?
SDWebImageDownloader类在初始化的时候,初始化了一个下载队列,并设置了最大并发数,请看:
- (id)init {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class]; //下载操作类
_shouldDecompressImages = YES; //默认解压图片
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder; //默认下载顺序是先进先出
_downloadQueue = [NSOperationQueue new]; //下载队列
_downloadQueue.maxConcurrentOperationCount = 6; //最大并发数
_URLCallbacks = [NSMutableDictionary new];
_HTTPHeaders = [NSMutableDictionary dictionaryWithObject:@"image/webp,image/*;q=0.8" forKey:@"Accept"]; //http头部参数
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT); //自定义GCD队列
_downloadTimeout = 15.0; //下载超时时间
}
return self;
}
其中_URLCallbacks是用来保存图片下载的回调信息,一般是一个URL对应一张图片下载,包含了下载进度的block和完成的block;来看看上面代码中调用的私有方法,你就会明白了:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
...省略...
//使用dispatch_barrier_sync来保证同一时间只有一个线程能对URLCallbacks进行操作
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) { //该URL没有对应的回调信息,则是第一次下载
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// Handle single download of simultaneous download request for the same URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; //进度回调
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; //完成回调
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) { //第一次下载
createCallback();
}
});
}
在以上代码中可以看到,凡是对URLCallbacks的添加移除操作都是使用dispatch_barrier_sync 函数,保证了线程安全性。
接下来看看SDWebImageDownloaderOperation类是怎么发起网络请求下载数据的:
- (void)start {
@synchronized (self) { //加锁,因为多线程并发执行
if (self.isCancelled) { //该操作取消后需要设置finished状态为YES
self.finished = YES;
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
//在后台执行
if ([self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self; //声明一个weak变量指向self,避免循环引用
self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself; //block中不允许self被释放了,所以强引用;block执行完,strongself会被释放
if (sself) {
[sself cancel];
[[UIApplication sharedApplication] endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
//NSUrlConnection开始请求加载数据
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
// Make sure to run the runloop in our background thread so it can process downloaded data
// Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
// not waking up the runloop, leading to dead threads (see [https://github.com/rs/SDWebImage/issues/466)]()
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else { //启动当前线程的runloop,去接收异步回调事件,否则执行完start方法,线程结束,delegate(也就是当前对象)接收不到返回的数据了
CFRunLoopRun();
}
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else { //connection初始化失败
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
SDWebImageDownloaderOperation:下载操作类,继承了NSOperation,并重写了start方法,所以必须手动管理操作的状态(executing与finished属性),检测isCancelled的状态。
对于图片的下载,使用的是NSUrlConnection,它实现了NSURLConnectionDataDelegate协议的几个:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; //收到响应
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; //接收数据
- (void)connectionDidFinishLoading:(NSURLConnection *)connection; //完成加载
下面看看是怎么接收处理数据的:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.imageData appendData:data]; //追加数据
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
...省略...
// Get the total bytes downloaded (获取数据总大小)
const NSInteger totalSize = self.imageData.length;
// Update the data source, we must pass ALL the data, not just the new bytes
//更新数据源,传入目前接收到的所有数据
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
if (width + height == 0) { //首次获取到数据
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = -1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &height); //高度
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &width); //宽度
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
CFRelease(properties);
// When we draw to Core Graphics, we lose orientation information,
// which means the image below born of initWithCGIImage will be
// oriented incorrectly sometimes. (Unlike the image born of initWithData
// in connectionDidFinishLoading.) So save it here and pass it on later.
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; //保存方向信息
}
}
// 继续接收数据
if (width + height > 0 && totalSize < self.expectedSize) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// Workaround for iOS anamorphic image
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
if (partialImageRef) { //对图片进行缩放,解码操作
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) { //可以让图片边显示边下载
self.completedBlock(image, nil, nil, NO); //还没下载完
}
});
}
}
CFRelease(imageSource);
}
if (self.progressBlock) { //下载进度回调
self.progressBlock(self.imageData.length, self.expectedSize);
}
}
再来看看结束加载数据的方法:
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
@synchronized(self) {
//停止当前线程的runloop,与上面的CFRunLoopRun配套使用
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
self.connection = nil;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];
});
}
if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
responseFromCached = NO;
}
if (completionBlock) {
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
completionBlock(nil, nil, nil, YES);
}
else {
UIImage *image = [UIImage sd_imageWithData:self.imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image]; //缩放
// Do not force decoding animated GIFs (GIF图片不解码)
if (!image.images) {
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
completionBlock(nil, nil, [NSError errorWithDomain:@"SDWebImageErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
}
else {
completionBlock(image, self.imageData, nil, YES);
}
}
}
self.completionBlock = nil;
[self done];
}
上面涉及到了对图片的解码操作,因为我们用到的图片格式一般为PNG或JPG,都是经过编码压缩过的,而显示图片的时候,要进行解码,耗时较大,在这里直接解码的话,显示的时候就不需要再解码了,节省了时间,不过需要占用较多空间。
至此,也就了解了SDWebImage异步下载图片的大概流程。
以上个人见解,水平有限,如有错漏,欢迎指出,就酱~~~