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<SDWebImageOperation>对象,该对象实现了cancel协议,调用其cancel方法即可【此处为何要用协议这种设计,而不是直接给方法?已经搞清楚了,因为downloaderOperation是可以自定义的,自定义类要自己实现cancel方法,所以采用协议形式设计】
    补充:
    网络下载核心逻辑,文字描述不够清晰,上代码
- (id <SDWebImageOperation>)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 <SDWebImageOperation> 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 <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) 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);
    }

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,649评论 18 139
  • 前不久做了一个生成快照的需求,其中用到 SDWebImage 来下载图片,在使用该框架的过程中也遇到了一些问题,索...
    ShannonChenCHN阅读 14,065评论 12 241
  • 图片下载的这些回调信息存储在SDWebImageDownloader类的URLOperations属性中,该属性是...
    怎样m阅读 2,378评论 0 1
  • 下载 下载管理器 SDWebImageDownLoader作为一个单例来管理图片的下载操作。图片的下载是放在一个N...
    wind_dy阅读 1,471评论 0 1
  • 莫说单身的生活更自由更享受,生活在现实生活中,自由的同时也必须面对来自各个方面的压力。每个人都要独立地面对生活,都...
    遗弃小屋阅读 637评论 0 1