SDWebImage 源码学习

1.综述

类目 选项
时间 耗时6天
版本 v3.7.0

SDWebImage是一个处理服务端加载图片的库,主要解决了以下几个问题:

  1. 图片从网路加载,预加载、同时下载多张、失败重试等
  2. 图片缓存,支持内存和磁盘
  3. 图片显示优化,解压缩、渐进显示、gif处理等
  4. 提供常见UI控件的扩展,方便使用

大致分为4个大模块,各模块的类名如下

模块 类名
下载 SDWeImageManager、SDWebImageDownloader、SDWebImageDownloaderOperation、SDWebImagePrefetcher
缓存 SDImageCache
图片处理 SDWebImageCompat、SDWebImageDecoder
控件扩展 UIButton(webCache)等

核心类及功能如下

类名 功能
SDWeImageManager 头文件有注释,It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache).,绑定downloader和imageCache
SDWebImageDownloader 异步下载器,维护下载配置
SDWebImageDownloaderOperation 默认operation,供内部operationQueue使用
SDWebImagePrefetcher 预加载urls到cache中,低优先级下载
SDImageCache 支持缓存图片到内存和磁盘,异步缓存到磁盘,iCloud备份配置,清理内存和磁盘
SDWebImageCompat 处理图片scale,根据url后的@2x和@3x字符串处理
SDWebImageDecoder 生成解压缩图片,可以提升imageView图片渲染速度

带着问题阅读可能会更好。

2.提出问题:

  1. 下载部分:
  • 1.1 如何从网络下载图片,超时、失败重试如何处理?如何取消?
  • 1.2 支持同时下载几张?默认6张
  • 1.3 同一个url同时下载如何处理?
  • 1.4 同时有多张下载完成,如何处理回调?
  1. 缓存处理
  • 2.1 缓存到内存、磁盘怎么实现?操作是异步的吗?两种缓存是并发的吗?
  • 2.2 缓存大小如何设置?如何清理?
  • 2.3 图片的格式怎么保证,gif/png/jpeg等
  1. 图片处理
  • 3.1 为何要解压缩?如何解压缩?可以异步吗?
  • 3.2 为何要处理scale?怎么处理?可以异步吗?
  • 3.3 gif图如何处理?webp呢?
  1. 控件扩展
  • 4.1 UIButton、UIImageView的扩展都有哪些方法?
  • 4.2 NSData+ImageContentType是如何获取图片类型的?
  • 4.3 UIView (WebCacheOperation)干啥的?
  1. 其他问题
  • 5.1 SDWebImageOperation如何使用自己的?
  • 5.2 还有哪些好的设计?思路?

3.解决问题

  1. 下载部分:
    1.1 如何从网络下载图片,超时、失败重试如何处理?
  • 使用NSOperation+NSUrlConnection处理网络请求,在connection:didReceiveData:方法中,拼接data;在connectionDidFinishLoading方法中将data转为UIImage;具体操作,在图片处理中统一说明
  • 超时利用NSMutableURLRequest设置,默认超时时间15秒
  • 失败重试,当配置SDWebImageOptions参数为SDWebImageRetryFailed时支持,SDWebImageManager中维护一个failedURLs属性,类型为NSMutableSet,存储下已经下载失败的url,若再次下载该url,同时配置了该参数,则 继续下载,否则不再下载。【此处失败重试,并没有在失败后再重复请求,只是一个能否下次下载的标记,相当于一个url黑名单】
  • 取消下载,在自定义operation的cancel方法中,同时调用NSUrlConnection对象的cancel方法即可。实现为利用SDWebImageManager downloadImageWithURL:方法返回的id对象,该对象实现了cancel协议,调用其cancel方法即可【此处为何要用协议这种设计,而不是直接给方法?已经搞清楚了,因为downloaderOperation是可以自定义的,自定义类要自己实现cancel方法,所以采用协议形式设计】
    补充:
    网络下载核心逻辑,文字描述不够清晰,上代码
- (id )downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
// 1.生成SDWebImageCombinedOperation对象,弱引用避免block中引用循环
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    // 2.全局字典保存正在运行的operation
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    // 3.用url构建缓存标记key
    NSString *key = [self cacheKeyForURL:url];

    // 4.由imageCache queryDiskCacheForKey 方法生成NSOperation对象,保存在combinedOperation中,此处相当于包装了一层
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
        
        // 5.如果缓存不存在,先由代理判断是否要下载url(此处是多虑了吧)
        if (image && options & SDWebImageRefreshCached) {
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
                dispatch_main_sync_safe(^{
                    // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
                    // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }

            // 6.配置downloaderOptions,将SDWebImageOptions配置传给SDWebImageDownloaderOptions
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
//            ...
            // 7.使用上方配置参数,调用imageDownloader downloadImageWithURL方法,生成subOperation对象
            id  subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                // 8.处理下载结果
                // 8.1 操作取消,不操作任何事情
                if (!strongOperation || strongOperation.isCancelled) {
   
                }
                // 8.2 下载失败,将url加入到failedUrls集合中
                else if (error) {
                    
                }
                // 8.3 下载成功,缓存到内存和磁盘(可选)中,并在主线程回调
                else {
                    
                    // 8.3.1 判断是否缓存到磁盘
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    // 8.3.2 image如果是NSUrlCache中图片,并且下载的图片为空,不回调
                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                    // 8.3.3 如果是动图,有delegate imageManager:transformDownloadedImage:withURL:方法判断是否要转换成指定的image,在子线程中处理,处理完缓存你transformImage,并回调
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
                            }

                            dispatch_main_sync_safe(^{
                                if (strongOperation && !strongOperation.isCancelled) {
                                    completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                                }
                            });
                        });
                    }
                    // 8.3.4 正常图片,缓存完回调
                    else {
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }

                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }
                // 9. 更新runningOperations值
                if (finished) {
                    @synchronized (self.runningOperations) {
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                }
            }];

}

1.2 支持同时下载几张?
默认6张,利用NSOperationQueue的maxConcurrentOperationCount属性实现
1.3 同一个url多次下载如何处理?
同一个url同时只会下载一次,主要是保证回调问题

  • 在创建任务时将每个url的回调都被保存起来
       // 全局的URLCallbacks字典,每个元素保存url的callback数组
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
       //存放新任务的回调
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        //更新全局的URLCallbacks
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
  • 任务结束时将回调有序执行,此处巧妙结合dispatch_barrier_sync实现(创建任务时也利用了此函数,主要作用是保证提交到全局队列中的同步和异步任务(主要是上一次的创建任务、进度回调、结束回调)都执行完,再创建新的任务,保证创建的任务有序)此处的代码段如下:
operation = [[wself.operationClass alloc] initWithRequest:request
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             __block NSArray *callbacksForURL;
                                                            //获取callback,此处使用sync提交同步任务到barrierQueue中,可以保证获取callbacksForURL肯定是最新的,如果这会儿self.URLCallbacks有更新,即addProgressCallback函数中,dispatch_barrier_sync会保证它前边的任务执行完再更新self.URLCallbacks,简单的说读和写都是同步任务,肯定就有顺序;此处用dispatch_barrier_sync应该也可以,只是sync就够用了
                                                             dispatch_sync(sself.barrierQueue, ^{
                                                                 callbacksForURL = [sself.URLCallbacks[url] copy];
                                                             });
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 //回调在主线程
                                                                 dispatch_async(dispatch_get_main_queue(), ^{
                                                                     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;
                                                            //此处使用栅栏函数,保证移除操作有序
                                                            dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                callbacksForURL = [sself.URLCallbacks[url] copy];
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });
                                                            // 该url的所有回调都会执行,此处回调执行在当前线程
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                                                        cancelled:^{
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            dispatch_barrier_async(sself.barrierQueue, ^{
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            });
                                                        }];

1.4 同时有多张下载完成,如何处理回调?
此处不是问题,各处理各的回调即可;每个url都有自己的operation,所以不会干扰,之前多虑了


1.5 SDWebImageDownloaderOperation解析

  • 上边所述都是对于任务创建及回调等的处理,真正的网络请求是在该类实现的,主要使用NSUrlConnection结合runloop实现,核心逻辑在NSUrlConnection的几个回调方法中
    1> 由于此处NSURLConnection对象是在子线程中创建,所以其回调也是在子线程中返回,需要手动处理子线程的runloop,这里使用了CoreFoundation框架中的几个方法,非常正确。
    2> 同时处理了应用进入后台情况,取消当前的connection任务
    代码块如下:
- (void)start {
    
    @synchronized (self) {
        // 1.判断是否是取消状态
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }
        // 2.处理后台过期事件
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        // 3.创建网络连接
        self.executing = YES;
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        // 保留当前的线程,供cancel方法使用,主要是为了取消当前的runloop
        self.thread = [NSThread currentThread];
    }
    // 4.启动网络连接
    [self.connection start];
    
    if (self.connection) {
        // 5.回调,并发通知
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
        
        // 6.启动runloop,开始接收delegate
        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 {

            CFRunLoopRun();
        }
        //...
}
  • 该类主要实现了自定义NSOperation必须得几个方法:
    start、cancel以及executing、finished的setter自定义

2.缓存处理
2.1 缓存到内存、磁盘怎么实现?操作是异步的吗?两种缓存是并发的吗?

  • 利用NSCache缓存到内存,需要在收到内存警告时,清空内存中的缓存;
  • 磁盘缓存则是保存data到文件中,
    1>.默认存放在 cache/com.hackemist.SDWebImageCache.default目录下
    2>.文件名取url得md5值,
    3>.SDWebImage并没有使用defaultFileManager,而是new了一个,此处考虑很周到
  • 都是异步操作,使用gcd的gloableQueue处理任务,下载完成时开始缓存,当option & SDWebImageCacheMemoryOnly为真时,则不缓存到磁盘

2.2 缓存大小如何设置?如何清理?

  • 内存中缓存的大小利用NSCache的totalCostLimit属性设置,系统会在内存超出最大值时,自动清理最早缓存的对象
  • 磁盘中缓存无默认最大值,有最长生存周期默认为7天(可重新设置),磁盘中清理逻辑为:每次当应用进入后台或者应用即将被杀死时,开始在后台检查文件的生命时长,如果文件已经过期,则从删除之

#if TARGET_OS_IOS
        // 内存警告时清内存
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
       // 应用即将挂起,清理过期文件
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(cleanDisk)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
       // 应用进入后台,在后台清理过期文件,加了后台过期的处理,跟cleanDisk方法的逻辑基本一致
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundCleanDisk)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif

2.3 图片的格式怎么保证,gif/png/jpeg等

if (image && (recalculate || !data)) {
               //如果有alpha,则视其为png图
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;

                // 如果还有原始数据,直接用原始数据头部8位来检查,这个最准
                // png的固定头部为0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
                if ([imageData length] >= [kPNGSignatureData length]) {
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }
                // 转pngdata
                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                   //转jpegdata,最低压缩比
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }

  1. 图片处理
  • 3.1 为何要解压缩?如何解压缩?可以异步吗?
    1>. 无论是png、jpeg等图片都是压缩图片,无法直接显示,需要先转化为bitmap,才能展示在imageView的layer上;
    2>. 利用CGContext提供的方法,将UIImage利用context重新绘制一遍,生成一个新的image,该版本SDWebImage不支持含有alpha通道图片解压缩
+ (UIImage *)decodedImageWithImage:(UIImage *)image {

    if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
        return nil;
    }
    //此处加入autoreleasepool可能并非必须,因为在方法return之后,这堆临时对象就立马释放了,当然外部如果有循环调用的话,还是加上比较好
    @autoreleasepool{
        // do not decode animated images
        if (image.images != nil) {
            return image;
        }
        
        CGImageRef imageRef = image.CGImage;
        
        CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
        BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                         alpha == kCGImageAlphaLast ||
                         alpha == kCGImageAlphaPremultipliedFirst ||
                         alpha == kCGImageAlphaPremultipliedLast);
       // 如果图片有alpha通道,直接return原始图片,不解压缩
        if (anyAlpha) {
            return image;
        }
        
        // 处理颜色空间
        CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
        CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
        
        BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                      imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                      imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                      imageColorSpaceModel == kCGColorSpaceModelIndexed);
       // 未知空间,使用RGB颜色空间
        if (unsupportedColorSpace) {
            colorspaceRef = CGColorSpaceCreateDeviceRGB();
        }
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        NSUInteger bytesPerPixel = 4;
        // 据说此处用0,还可以支持行对齐,此处设置并不是最优
        NSUInteger bytesPerRow = bytesPerPixel * width;
        NSUInteger bitsPerComponent = 8; 
       //创建bitmapContext,参数有:宽、高、每组的位数(就是每个像素8位)、每行的字节数、颜色空间和字节顺序[SDWebImage的设置也不是最优解,应该使用kCGBitmapByteOrder32Host|kCGImageAlphaNoneSkipLast(无alpha通道时),kCGBitmapByteOrder32Host|kCGImageAlphaPremultipliedFirst(有alpha通道时)]
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     bitsPerComponent,
                                                     bytesPerRow,
                                                     colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        //在指定区域绘制图片
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
      // 创建新的image
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];
        // 释放CoreFoundation对象
        if (unsupportedColorSpace) {
            CGColorSpaceRelease(colorspaceRef);
        }
        
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        return imageWithoutAlpha;
    }
}

  • 3.2 为何要处理scale?怎么处理?可以异步吗?
    iPhone是视网膜屏幕,若屏幕scale为2,一个点填充4个像素,scale为3,一个点填充9个像素,如果图片的scale为1,填充到屏幕scale为2的机器上,则因像素不够,会显示模糊,所以要给当前机型合适scale的图片,这也是为啥我们的素材要准备为@2x、@3x这两种(6和6plus使用)
inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image) {
    if (!image) {
        return nil;
    }
    // gif图,每个处理一遍,用了递归
    if ([image.images count] > 0) {
        NSMutableArray *scaledImages = [NSMutableArray array];

        for (UIImage *tempImage in image.images) {
            [scaledImages addObject:SDScaledImageForKey(key, tempImage)];
        }

        return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
    }
    else {
        if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
            CGFloat scale = 1;
            if (key.length >= 8) {  
               //根据图片名称是否包含@2x、@3x字符串来处理,图片的命名一定要规范
                NSRange range = [key rangeOfString:@"@2x."];
                if (range.location != NSNotFound) {
                    scale = 2.0;
                }
                
                range = [key rangeOfString:@"@3x."];
                if (range.location != NSNotFound) {
                    scale = 3.0;
                }
            }

            UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
            image = scaledImage;
        }
        return image;
    }
}
  • 3.3 gif图如何处理?webp呢?
    能够生成正确的anmatedImage就可以,在UIImage (GIF)扩展中
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
    if (!data) {
        return nil;
    }

    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
   // 确定data中有几帧图片
    size_t count = CGImageSourceGetCount(source);

    UIImage *animatedImage;
  // 单图片
    if (count <= 1) {
        animatedImage = [[UIImage alloc] initWithData:data];
    }
    else {
       // 多图片
        NSMutableArray *images = [NSMutableArray array];

        NSTimeInterval duration = 0.0f;

        for (size_t i = 0; i < count; i++) {
            CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
            if (!image) {
                continue;
            }
           //计算每帧图片的时长
            duration += [self sd_frameDurationAtIndex:i source:source];
            // 生成图片数组,注意scale
            [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];

            CGImageRelease(image);
        }

        if (!duration) {
            duration = (1.0f / 10.0f) * count;
        }
      // 用图片数组和时长,创建anmatedImage
        animatedImage = [UIImage animatedImageWithImages:images duration:duration];
    }
    CFRelease(source);
    return animatedImage;
}
  1. 控件扩展
  • 4.1 UIButton、UIImageView的扩展都有哪些方法?
    有两大类
    1>. 根据状态设置中心图、背景图url,可选取placeHolder、competitionHandler参数等
    2>.取消之前根据状态设置的url

  • 4.2 NSData+ImageContentType是如何获取图片类型的?
    根据data的前两位数值来确定类型

+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return @"image/jpeg";
        case 0x89:
            return @"image/png";
        case 0x47:
            return @"image/gif";
        case 0x49:
        case 0x4D:
            return @"image/tiff";
        case 0x52:
            // R as RIFF for WEBP
            if ([data length] < 12) {
                return nil;
            }
           // 此处做特殊处理
            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return @"image/webp";
            }

            return nil;
    }
    return nil;
}
  • 4.3 UIView (WebCacheOperation)干啥的?
    持有一个字典来存储,当前对象所有的operation,当取消其下载时用得着
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    // Cancel in progress downloader from queue
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey:key];
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
          //此处id为SDWebImageDownloaderOperation对象,它实现了SDWebImageOperation协议,为何要这么设计?
            for (id  operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}
  1. 其他问题
  • 5.1 SDWebImageOperation如何使用自己的?
    利用SDWebImageDownloader setOperationClass:方法设置
  • 5.2 还有哪些好的设计?思路?
    1>.在子线程中获取磁盘缓存大小
- (NSUInteger)getSize {
    __block NSUInteger size = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
        for (NSString *fileName in fileEnumerator) {
            NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
            NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
            size += [attrs fileSize];
        }
    });
    return size;
}

2>. 子类化NSCache,内存警告时自动释放内存

@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (id)init
{
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    }
    return self;
}

5.3 图片渐进显示处理,不停得根据当前拿到的data创建一个新的UIImage

if (width + height > 0 && totalSize < self.expectedSize) {
            // 从指定位置创建imageRef
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
            // 处理失真图片?此处不知是为何
            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
            // 用新创建的imageRef,做scale、decode处理,生成正确的UIImage对象
            if (partialImageRef) {
               //初始UIImage对象
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                //scale处理
                UIImage *scaledImage = [self scaledImageForKey:key image:image];
               //decode处理
                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);
    }

你可能感兴趣的:(SDWebImage 源码学习)