SDWebImage源码解读

整体架构

按照分组方式,可以分为几组

定义通用宏和方法

  • SDWebImageCompat: 宏定义和C语言的一些工具方法。
  • SDWebImageOperation:定义通用的Operation协议,主要就是一个方法,cancel。从而在cancel的时候,可以面向协议编程。

下载

  • SDWebImageDownloader:实际的下载功能和配置提供者,使用了单例的设计模式。
  • SDWebImageDownloaderOperation:继承自NSOperation,是一个异步的NSOperation,封装了NSURLSession进行实际的下载任务。

缓存处理

  • SDImageCache:继承自NSCache,实际处理内存cache和磁盘cache。
  • SDImageCacheConfig:缓存处理的配置。
  • SDWebImageCoder:定义了编码解码的协议,从而可以实现面向协议编程。

功能类

  • SDWebImageManager:宏观的从整体上管理整个框架的类。
  • SDWebImagePrefetcher:图片的预加载管理。

加载GIF动图

  • FLAnimatedImage:处理加载GIF动图的逻辑。

图片的编码解码处理

  • SDWebImageCodersManager:实际的编码解码功能处理,使用了单例模式。

Category

  • 类别用来为UIView和UIImageView等”添加”属性来存储必要的信息,同时暴露出接口,进行实际的操作。

用类别来提供接口往往是最方便的,因为用户只需要import这个文件,就可以像使用原生SDK那样去开发,不需要修改原有的什么代码。
面向对象开发有一个原则是-单一功能原则,所以不管是在开发一个Lib或者开发App的时候,尽量保证各个模块之前功能单一,这样会降低耦合。

sd_setImageWithURL的加载逻辑

1.取消正在加载的图片

[self sd_cancelImageLoadOperationWithKey:validOperationKey];

方法源代码如下,这里的key是FLAnimatedImageView。

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        //使用NSMapTable存储当前的operation
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        //使用@synchronized保证线程安全,后面会讲到
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                //这里属于面向协议编程,不关心具体的类,只关心遵守某个协议
                [operation cancel];
            }
            @synchronized (self) {
                //删除对应的key
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}

我们看一下SDOperationsDictionary的数据结构

- (SDOperationsDictionary *)sd_operationDictionary {
    @synchronized(self) {
        SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
        if (operations) {
            return operations;
        }
        operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
        objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return operations;
    }
}

SDOperationsDictionary通过runtime关联对象机制来为UIView添加的属性。它的数据结构是NSMapTable。后面会讲到。

2. 如果有PlaceHolder,设置placeHolder

if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
}

dispatch_main_async_safe是一个宏定义,会判断是否是并行队列,不是的话异步切换到主队列执行。

#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(queue)) {\
        block();\
    } else {\
        dispatch_async(queue, block);\
    }
#endif

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
#endif

3. 根据SDImageCache来查缓存,看看是否有图片

operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {//异步返回查询的结果}

queryCacheOperationForKey返回一个NSOperation,之所以这样,是因为从磁盘或者内存查询的过程是异步的,后面可能需要cancel,所以这样做。
我们再看看queryCacheOperationForKey这个方法是怎么实现的?

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    
    // First check the in-memory cache...
    //先从检查内存缓存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    //如果不需要检查磁盘直接返回
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    //创建一个NSOperation,因为从磁盘查询的过程是异步的,后面可能需要cancel
    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        @autoreleasepool {
            //从磁盘中查询
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            // 从磁盘读的图片要解码
            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) {
                //回归到主线程行,进行doneBlock操作
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, cacheType);
                });
            }
        }
    };
    
    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {//切换到ioQueue,进行异步磁盘查询操作
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    return operation;
}

这里使用到了@autoreleasepool,后面讲解。
可以看到从缓存中读取图片首先从内存读,内存没有再去磁盘中读,磁盘读到后要去解码,然后再将图片写入内存缓存中。
解压图片:

- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context {
    if (data) {
        UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context);
}}

4. 创建下载任务

//downloadToken用于取消下载的操作
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
//下载完成
if (downloadedImage && finished) {
                            //是否需要序列化成data
                            if (self.cacheSerializer) {                             dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    @autoreleasepool {
                                        NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                        //保存至内存或磁盘
                                        [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                    }
                                });
                            } else {
                                //如果不需要保存序列化,直接保存至内存或磁盘
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        //回归到主线程行,进行completedBlock操作
                        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];

                if (finished) {//完成之后要记得移除这个operation
                    [self safelyRemoveOperationFromRunning:strongSubOperation];
                }
}

safelyRemoveOperationFromRunning:为了保证多线程安全,移除的时候加上锁,这个锁是通过信号量实现的。这个方法的源代码如下:

- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
    if (!operation) {
        return;
    }
    LOCK(self.runningOperationsLock);
    [self.runningOperations removeObject:operation];
    UNLOCK(self.runningOperationsLock);
}

#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);

self.runningOperations的数据结构是NSMutableSet。里面都是SDWebImageCombinedOperation对象

@property (strong, nonatomic, nonnull) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations;

接下来,我们来看看实际的下载operation是什么样子的
也就是这个方法:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

这个方法之所以返回SDWebImageDownloadToken,应该主要是为了返回后面取消下载操作用的。
URLOperations的数据结构是一个NSMutableDictionary,key是图片url,value是一个operation

4.1由于有各种各样的block回调,例如下载进度的回调,完成的回调,所以需要一个数据结构来存储这些回调

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock

其中,用来存储回调的数据结构是一个NSMutableDictionary,其中key是图片的url,value是回调的数组
举个例子,存储后应该是这样的,

@{
        @"http://imageurl":[
                            @{
                                @"progress":progressBlock1,
                                @"completed":completedBlock1,
                            },
                            @{
                                @"progress":progressBlock2,
                                @"completed":completedBlock2,
                              },
                           ],
            //其他
}

如何做到url防护的

BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }
//如果不是有效的url就直接返回
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

failedURLs是个NSMutableSet类型的,里面存放着请求失败的url。所以每次在请求之前先去failedURLs检查是否包含这个url

4.3如何保证同一个url不被下载两次:

在创建操作之前,先去URLOperations,如果取不到或者已经完成,再去创建。因为同一个url对应的operation就只有一个

NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // 去URLOperations去取operation,如果取不到或者已经完成才去创建operation
    if (!operation || operation.isFinished) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            //完成之后移除operation
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        [self.downloadQueue addOperation:operation];
    }

这样的话可以保证一个URL在多次下载的时候,只进行多次回调,而不会进行多次网络请求

4.4 对于同一个url,在第一次调用sd_setImage的时候进行,创建网络请求SDWebImageDownloaderOperation

[[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];

在看看Progress回调:

if (!self.imageData) {
        self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
    }
    [self.imageData appendData:data];

    //渐进式下载
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
        // Get the image data
        __block NSData *imageData = [self.imageData copy];
        // Get the total bytes downloaded
        const NSInteger totalSize = imageData.length;
        // Get the finish status
        BOOL finished = (totalSize >= self.expectedSize);
        
        if (!self.progressiveCoder) {
            // 创建渐进解码实例
            for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
                if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
                    [((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
                    self.progressiveCoder = [[[coder class] alloc] init];
                    break;
                }
            }
        }
        
        //在coderQueue队列解码图片
        dispatch_async(self.coderQueue, ^{
            @autoreleasepool {
                UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
                if (image) {
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    if (self.shouldDecompressImages) {
                        image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
                    }
                    //异步切换到主线程上进行回调
                    [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
                }
            }
        });
    }

    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }

completion回调:

@synchronized(self) {
        self.dataTask = nil;
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            //发送停止下载的通知
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
            if (!error) {
                //发送停止下载完成的通知
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
            }
        });
    }
    
    //保证可以取到下载完成的block
    if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
        //用__block来修饰imageData,保证在能在block中修改这个变量
        __block NSData *imageData = [self.imageData copy];
        if (imageData) {
            // 在coderQueue队列解码图片
            dispatch_async(self.coderQueue, ^{
                @autoreleasepool {
                    //图片解码
                    UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    
                    BOOL shouldDecode = YES;
                    // 不强制解压GIF和webp
                    if (image.images) {
                        shouldDecode = NO;
                    }
                    //解压图片
                    if (shouldDecode) {
                        if (self.shouldDecompressImages) {
                            BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                        }
                    }
                    CGSize imageSize = image.size;
                    if (imageSize.width == 0 || imageSize.height == 0) {
                        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                    } else {
                        [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                    }
                    [self done];
                }
            });
            
        }
    }

4.5 下载图片完成后,根据需要图片解码和处理图片格式,回调给Imageview

//图片解码
                    UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    
                    BOOL shouldDecode = YES;
                    // 不强制解压GIF和webp
                    if (image.images) {
                        shouldDecode = NO;
                    }
                    
                    if (shouldDecode) {
                        if (self.shouldDecompressImages) {
                            BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                            //解压图片
                            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                        }
                    }

更新

基于最新的版本:
当下载完成后,会将图片存入内存:

[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];

再看一下这个方法callStoreCacheProcessForOperation:

// Store cache process
- (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 {
    // the target image store cache type
    SDImageCacheType storeCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextStoreCacheType]) {
        storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
    }

这里注意一下缓存策略,默认是内存和磁盘都存的

总结下整个调用过程

  • 取消上一次调用
  • 设置placeHolder
  • 保存此次operation
  • cache查询是否已经下载过了,先检查内存,后检查磁盘(从磁盘读取后会解码)
  • 利用NSURLSession来下载图片,根据需要解码,回调给imageview,存储到缓存(包括内存和磁盘)

线程管理

整个SDWebImage一共有四个队列

  • Main queue,主队列,在这个队列上进行UIKit对象的更新,发送notification
  • ioQueue,用在图片的磁盘操作
  • downloadQueue(NSOperationQueue),用来全局的管理下载的任务
  • coderQueue专门复杂解压图片的队列。
    注意:barrierQueue已经被废掉了,统一使用信号量来确保线程的安全。

图片解码

传统的UIImage进行解码都是在主线程上进行的,比如

UIImage * image = [UIImage imageNamed:@"123.jpg"]
self.imageView.image = image;

在这个时候,图片其实并没有解码。而是,当图片实际需要显示到屏幕上的时候,CPU才会进行解码,绘制成纹理什么的,交给GPU渲染。这其实是很占用主线程CPU时间的,而众所周知,主线程的时间真的很宝贵

现在,我们看看SDWebImage是如何在后台进行解码的 :
在coderQueue进行异步解压图片,解压成功后切换到主线程上回调给调用方。
注意解码操作是在一个单独的队列coderQueue里面处理的

//在coderQueue队列解压图片
        dispatch_async(self.coderQueue, ^{
            @autoreleasepool {
                UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
                if (image) {
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    if (self.shouldDecompressImages) {
                        image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
                    }
                    //异步切换到主线程上进行回调
                    [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
                }
            }
        });

incrementallyDecodedImageWithData方法:

- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
    if (!_imageSource) {
        _imageSource = CGImageSourceCreateIncremental(NULL);
    }
    UIImage *image;
    
    // Update the data source, we must pass ALL the data, not just the new bytes
    CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
    
    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);
            
            _orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue];

        }
    }
    
    if (_width + _height > 0) {
        // Create the image
        CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
        
        if (partialImageRef) {
            image = [[UIImage alloc] initWithCGImage:partialImageRef scale:1 orientation:_orientation];
            CGImageRelease(partialImageRef);
            image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
        }
    }
    
    if (finished) {
        if (_imageSource) {
            CFRelease(_imageSource);
            _imageSource = NULL;
        }
    }
    
    return image;
}

解压图片

//解压图片
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool{
        
        CGImageRef imageRef = image.CGImage;
        // device color space
        CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
        //是否有alpha通道
        BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
        // iOS display alpha info (BRGA8888/BGRX8888)
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     0,
                                                     colorspaceRef,
                                                     bitmapInfo);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        //解压图片
        UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}

缓存处理(SDImageCache)

缓存处理 包含两块:

  • 内存缓存(SDMemoryCache):
  • 磁盘缓存
    内存缓存集成自NSCache。添加了在收到内存警告通知UIApplicationDidReceiveMemoryWarningNotification的时候自动removeAllObjects。

再看看磁盘缓存是如何做的?
磁盘缓存是基于文件系统NSFileManager对象的,也就是说图片是以普通文件的方式存储到沙盒里的。

下面看一下这几个问题:

1.磁盘缓存的默认路径是啥:

/Library/Caches/default/com.hackemist.SDWebImageCache.default/

2.SDWebImage 缓存图片的名称如何 避免重名?
缓存图片的名称是对key做了一次md5加密处理

- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)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;
    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;
}

3.SDWebImage Disk默认缓存时长? Disk清理操作时间点? Disk清理原则?
默认缓存时长一周:

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

在App关闭的时候或者app退到后台的时候(后台清理):

//在App关闭的时候清除过期图片
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        //在App进入后台的时候,后台处理过期图片
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];

Disk清理原则:

1.获取文件的modify时间,然后比较下过期时间,如果过期了就删除。
2.当磁盘缓存超过阈值后,根据最后访问的时间排序,删除最老的访问图片。

SDWebImage 如何 区分图片格式?

将数据data转为十六进制数据,取第一个字节数据进行判断。

//根据data获取图片格式:
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    // File signatures table: http://www.garykessler.net/library/file_sigs.html
    uint8_t c;
    //将数据data转为十六进制数据,取第一个字节数据进行判断。
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        
    }
    return SDImageFormatUndefined;
}

SDWebImageDownloader的最大并发数和超时时长

_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";

/**
 *  The timeout value (in seconds) for the download operation. Default: 15.0.
 */
@property (assign, nonatomic) NSTimeInterval downloadTimeout;

最大并发下载量6个。下载超时时长15s。

NSMapTable

NSMapTable类似于NSDictionary,但是NSDictionary只提供了key->value的映射。NSMapTable还提供了对象->对象的映射。
NSDictionary的局限性:
NSDictionary 中存储的 object 位置是由 key 来索引的。由于对象存储在特定位置,NSDictionary 中要求 key 的值不能改变(否则 object 的位置会错误)。为了保证这一点,NSDictionary 会始终复制 key 到自己私有空间。限制:

  • 你只能使用 OC 对象作为 NSDictionary 的 key,并且必须支持 NSCopying 协议。
  • Key必须小而高效,以保证拷贝复制的时候不会造成CPU和内存的负担。(因此key最好是值类型,最好是NSNumber或者NSString作为NSDictionary的Key)
  • 会保持对Object的强引用,即Object的引用计数+1。

NSMapTable优势:

  • 能够处理obj->obj的映射
  • 能够对Key和value保持弱引用,当key或者value被释放的时候,此entry对会自动从NSMapTable中移除。
  • 能够以包含任意指针对象

NSMapTable对象到对象的映射:
比如一个 NSMapTable 的构造如下:

NSMapTable *keyToObjectMapping =
    [NSMapTable mapTableWithKeyOptions:NSMapTableCopyIn
                          valueOptions:NSMapTableStrongMemory];

这将会和 NSMutableDictionary 用起来一样一样的,复制 key,并对它的 object 引用计数 +1。
NSPointerFunctionsOptions:

NSMapTableCopyIn
NSMapTableStrongMemory
NSPointerFunctionsWeakMemory

可以通过设置NSPointerFunctionsOptions来指定的对象的内存管理方式。

我们看看SDWebImage怎么使用的NSMapTable:

        // Use a strong-weak maptable storing the secondary cache. Follow the doc that NSCache does not copy keys
        // This is useful when the memory warning, the cache was purged. However, the image instance can be retained by other instance such as imageViews and alive.
        // At this case, we can sync weak cache back and do not need to load from disk cache
        self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

key的内存管理方式是NSPointerFunctionsStrongMemory,当一个对象添加到NSMapTable中后,key的引用技术+1。
value内存管理方式是NSPointerFunctionsWeakMemory,当一个对象添加到NSMapTable中后,key的引用技术不会+1。

这样使用的意义在哪呢:
1.遵循NSCache不复制key的文档。
2.当收到内存警告,缓存被清理的时候,可以保存image实例。这个时候我们可以同步弱缓存表,不需要从磁盘加载。

@autoreleasepool

@autoreleasepool {
            //从磁盘中查询
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            diskImage = [self diskImageForKey:key data:diskData options:options];
            
            if (doneBlock) {
                //回归到主线程行,进行doneBlock操作
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, cacheType);
                });
            }
        }

如果不使用autoreleasepool,已经创建的临时变量就无法释放,要等到下次runloop结束时,才会清空系统的自动释放池中的临时变量,但是这个时间是不确定的,这就会导致内存爆发式的增长。如果autoreleasepool,等待autoreleasepool结束时,里面的临时变量就会释放。因为autoreleasepool的作用就是加速局部变量的释放。
具体可以看一下我这篇文章autoreleasepool

读取Memory和Disk的时候如何保证线程安全

Memory是通过信号量。

LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];
UNLOCK(self.weakCacheLock);

Disk操作是在一个单独的IO 队列去处理的。
存图片至磁盘

- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    if (!imageData || !key) {
        return;
    }
    dispatch_sync(self.ioQueue, ^{
        [self _storeImageDataToDisk:imageData forKey:key];
    });
}

取图片:

dispatch_sync(self.ioQueue, ^{
        imageData = [self diskImageDataBySearchingAllPathsForKey:key];
    });

dispatch_semaphore_t信号量

通过宏定义使用信号量(创建,提高,降低)

#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);

使用

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    LOCK(self.callbacksLock);
    [self.callbackBlocks addObject:callbacks];
    UNLOCK(self.callbacksLock);
    return callbacks;
}

更多信号量可以看这篇文章

FOUNDATION_EXPORT

.h文件中声明

FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStartNotification;

.m文件中是这样实现的

NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";

如以上代码所示,FOUNDATION_EXPORT是用来定义常量的,众所周知,#define也可以定义常量。
他们的主要区别在哪呢?

  • 使用FOUNDATION_EXPORT定义常量在检测字符串的值是否相等的时候效率更快,可以直接使用(myString == SDWebImageDownloadStartNotification)来比较。而define定义的常量如果要比较的话,就得使用[myString isEqualToString:SDWebImageDownloadStartNotification],效率更低一点,因为前者是比较指针地址,后者是比较每一个字符。
  • FOUNDATION_EXPORT是可以兼容c++编程的。
  • 过多的使用宏定义会产生过多的二进制文件。

@synchronized

1.为啥要引入@synchronized
Objective-C支持程序中的多线程。这就意味着两个线程有可能同时修改同一个对象,这将在程序中导致严重的问题。为了避免这种多个线程同时执行同一段代码的情况,Objective-C提供了@synchronized()指令。

2.参数
指令@synchronized()需要一个参数。该参数可以使任何的Objective-C对象,包括self。这个对象就是互斥信号量。他能够让一个线程对一段代码进行保护,避免别的线程执行该段代码。针对程序中的不同的关键代码段,我们应该分别使用不同的信号量。只有在应用程序编程执行多线程之前就创建好所有需要的互斥信号量对象来避免线程间的竞争才是最安全的。

- (void)cancel {
    @synchronized (self) {
        [self cancelInternal];
    }
}
  • @synchronized 的作用是创建一个互斥锁,保证此时没有其它线程对self对象进行修改。这个是objective-c的一个锁定令牌,防止self对象在同一时间内被其它线程访问,起到线程的保护作用。
  • @synchronized 主要用于多线程的程序,这个指令可以将{ } 内的代码限制在一个线程执行,如果某个线程没有执行完,其他的线程如果需要执行就得等着。

@synthesize:

ios6之后 LLVM 编译器会新增加一个技术,叫自动合成技术,会给每个属性添加@synthesize,即

@synthesize propertyName = _propertyName;

也就是说会自动生成一个带下划线的实例变量,同时为属性生成gettersetter方法。当然这些都是默认实现的。
如果我们不想使用编译器为我们生成的实例变量,我们就可以在代码中显示的起一个别名:

@synthesize propertyName = _anotherPropertyName;

如果我们想要阻止编译器自动合成,可以使用@dynamic,使用场景就是你想自己实现gettersetter方法。
5.#pragma clang diagnostic push与#pragma clang diagnostic pop

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
            if (self.options & SDWebImageDownloaderHighPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            } else if (self.options & SDWebImageDownloaderLowPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityLow;
            }
        }
#pragma clang diagnostic pop

表示在这个区间里忽略一些特定的clang的编译警告,因为SDWebImage作为一个库被其他项目引用,所以不能全局忽略clang的一些警告,只能在有需要的时候局部这样做。

NS_ENUM && NS_OPTIONS

NS_ENUM多用于一般枚举,NS_OPTIONS多用于同一个枚举变量可以同时赋值多个枚举成员的情况。

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,
    SDWebImageLowPriority = 1 << 1,
    SDWebImageCacheMemoryOnly = 1 << 2,
    SDWebImageProgressiveDownload = 1 << 3,
}

这里的NS_OPTIONS是用位运算的方式定义的。


//用“或”运算同时赋值多个选项
SDWebImageOptions option = SDWebImageRetryFailed | SDWebImageLowPriority | SDWebImageCacheMemoryOnly | SDWebImageProgressiveDownload;
 
//用“与”运算取出对应位
if (option & SDWebImageRetryFailed) {
    NSLog(@"SDWebImageRetryFailed");
}
if (option & SDWebImageLowPriority) {
    NSLog(@"SDWebImageLowPriority");
}
if (option & SDWebImageCacheMemoryOnly) {
    NSLog(@"SDWebImageCacheMemoryOnly");
}
if (option & SDWebImageProgressiveDownload) {
    NSLog(@"SDWebImageProgressiveDownload");
}

这样,用位运算,就可以同时支持多个值。

相关面试题

假如一个界面里面有很多图片,如何优先下载最大的图片。

方法1:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

可以将options设置成SDWebImageDownloaderHighPriority。就是说将这个操作设成高优先级的。

方法2:给大的图片做标记,让它先sd_setImage

dispatch_queue_get_label

获取队列标签,经常用户判断是否是当前队列:

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

判断当前队列是否是主队列,如果是就直接在该队列执行,如果不是,异步回到主队列执行

判断当前队列还有一种方式:
dispatch_queue_set_specificdispatch_queue_set_specific,比如:

static void * JDHybridQueueKey = &JDHybridQueueKey;
dispatch_queue_set_specific(dispatch_get_global_queue(0, 0), JDHybridQueueKey, JDHybridQueueKey, NULL);
// 判断是否在当前队列上
if (dispatch_get_specific(JDHybridQueueKe y) == JDHybridQueueKey) {
}

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容