iOS开发 之 SDWebImage源码分析

本文的思维导图(这里使用的是MindNode)

iOS开发 之 SDWebImage源码分析_第1张图片
ios-sdwebimage.png

本文基于SDWebImage v3.8.2

目录

  • 概述

  • 结构

    • Base

    • Cache

    • Downloader

    • Categories

    • Utils

  • 问题

    • SDScaledImageForKey的作用?

    • decodedImageWithImage的作用?

    • 如何加载超大图片?

  • 参考

概述

  • 接口基于category

  • 异步下载基于NSOperation

  • 缓存基于Disk + memory二级缓存

  • 异步解压缩等优化

  • 支持png, jpg, gif和webp(默认不支持, 开启请参考How to support webp?)

  • 默认情况下同一URL的网络图片不会被重复下载

通过对URL进行MD5保证同一URL不会重复下载

// SDImageCache.m line 182
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);

如果要让同一URL重新下载, 需要配置如下option

[imageView sd_setImageWithURL:url placeholderImage:nil options:SDWebImageRefreshCached];

结构

Base

  • SDWebImageCompat - 保证不同平台/版本/屏幕等兼容性的宏定义和内联

  • SDWebImageOperation - operation协议, 只定义了cancel operation这一个接口

Cache

  • SDImageCache - 缓存 = Disk + memory二级缓存

其中:

Disk缓存maxAge默认是1 week

// SDImageCache.m line 37
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

// SDImageCache.m line 102
_maxCacheAge = kDefaultCacheMaxCacheAge;

Disk缓存清理策略是当app进入后台或销毁时

// SDImageCache.m line 136
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(cleanDisk)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundCleanDisk)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

memory缓存基于NSCache

// SDImageCache.m line 61
@property (strong, nonatomic) NSCache *memCache;

memory缓存清理策略是当app收到内存警告时

// SDImageCache.m line 131
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(clearMemory)
                                             name:UIApplicationDidReceiveMemoryWarningNotification
                                           object:nil];

Downloader

  • SDWebImageDownloader - 异步图片下载管理: 管理下载队列, 管理operation, 管理网络请求, 处理结果和异常
// SDWebImageDownloader.m line 70
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
  • SDWebImageDownloaderOperation - 实现了异步下载图片的NSOperation, 网络请求基于NSURLSession
// SDWebImageDownloaderOperation.h line 18
@interface SDWebImageDownloaderOperation : NSOperation 

Categories

  • MKAnnotationView+WebCache - 为MKAnnotationView异步加载图片

  • NSData+ImageContentType - 通过image data判断当前图片的格式

// NSData+ImageContentType.m line 12
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";
        }
}
  • UIButton+WebCache - 为UIButton异步加载图片

  • UIImage+GIF / UIImage+MultiFormat / UIImage+WebP - 将image data转换成制定格式图片

// UIImage+MultiFormat.m line 25
UIImage *image;
NSString *imageContentType = [NSData sd_contentTypeForImageData:data];
if ([imageContentType isEqualToString:@"image/gif"]) {
    image = [UIImage sd_animatedGIFWithData:data];
}
#ifdef SD_WEBP
else if ([imageContentType isEqualToString:@"image/webp"])
{
    image = [UIImage sd_imageWithWebPData:data];
}
#endif
else {
    image = [[UIImage alloc] initWithData:data];
    UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
    if (orientation != UIImageOrientationUp) {
        image = [UIImage imageWithCGImage:image.CGImage
                                    scale:image.scale
                              orientation:orientation];
    }
}
  • UIImageView+WebCache / UIImageView+HighlightedWebCache - 为UIImageView异步加载图片

  • UIView+WebCacheOperation - 保存当前MKAnnotationView / UIButton / UIImageView异步下载图片的operations

Utils

  • SDWebImageManager - 核心管理器, 主要就是对缓存管理 + 下载管理进行了封装, 其中最主要的接口就是
// SDWebImageManager.h line 212
- (id )downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
  • SDWebImageDecoder - 图片解压缩, 其中只有一个接口
// SDWebImageDecoder.h line 16
+ (UIImage *)decodedImageWithImage:(UIImage *)image;
  • SDWebImagePrefetcher - 低优先级情况下预先下载图片, 对SDWebImageManager进行简单的封装
// SDWebImagePrefetcher.m line 39
- (id)initWithImageManager:(SDWebImageManager *)manager {
    if ((self = [super init])) {
        _manager = manager;
        _options = SDWebImageLowPriority;
        _prefetcherQueue = dispatch_get_main_queue();
        self.maxConcurrentDownloads = 3;
    }
    return self;
}

问题

SDScaledImageForKey的作用?

首先, 我们来看一下UIImage的size和scale是什么(定义引自Apple API Reference - UIImage)

size - The logical dimensions of the image, measured in points
scale - The scale factor of the image

当使用imageNamed加载图片时, iOS会根据当前设备scale以及图片后缀计算出size和scale

例如对于120*120pixel的图片来说, 在设备phone6(scale=2)上加载图片时

  • 图片名没有@2x, @3x后缀, size = {60, 60}, scale = 2.0

  • 图片名的后缀为@2x, size = {60, 60}, scale = 2.0

  • 图片名的后缀为@3x, size = {40, 40}, scale = 3.0

而从网络下载的图片, size和pixel是一一对应的, 即1 size = 1 pixel

例如对于上述120*120pixel的图片来说

  • size = {120, 120}, scale = 1.0

对于@2x, @3x的图片来说, 此时的size和scale是不正确的

所以SDScaledImageForKey就是将size和scale更新成与图片名后缀一致

decodedImageWithImage的作用?

decodedImageWithImage主要是对图片进行解压缩

当从Disk读取或从网络下载完图片后, 都会对图片进行解压缩

由于Disk读取或从网络下载都是在子线程中进行的, 所以解压缩操作不会占用主线程资源

这样当加载UIImage时, 主线程就省去了加压缩的操作

这里需要留意的是, 在decodedImageWithImage中使用了@autoreleasepool以降低内存瞬时峰值, 更多可以参考Understanding SDWebImage - Decompression

如何加载超大图片?

PS: 对于这个问题, 其实本人也没还遇到过, 但是在了解上述SDWebImage的机制后, 完全是有可能发生内存占用过高的情况(虽然SDWebImage告诉我们不要担心内存, 它会处理UIApplicationDidReceiveMemoryWarningNotification)

我们可以从以下三个方面进行优化

  • Lazy Loading

这也是iOS中一个非常重要的原则, 小到通过getter实现成员变量的懒加载, 大到对象的创建

对于图片这种大量占用内存的资源来说, 我们更需要按需进行Lazy Loading

例如当在列表显示时, 我们可以加载缩略图, 在查看详情时, 才加载原来的大图, 当然这点优化可能需要服务器的配合, 即为缩略图和原图创建不同的URL

  • 等比压缩

在对图片质量没有严格要求的情况下, 我们可以修改SDWebImage的源码, 在图片下载完成后, 对图片进行等比压缩

详细参考解决MWPhotoBrowser中的SDWebImage加载大图导致的内存警告问题

当然这种方法其实有一个值得商榷的地方, 如果客户端确定不显示原图, 那么服务器何不直接压缩和处理呢?

  • 分块加载

android开发中加载大图, 也有分块加载的优化方式: BitmapRegionDecoder(详细参考android开发 之 优化篇)

iOS中也有分块加载图片的方法

CGImageRef CGImageCreateWithImageInRect(CGImageRef image, CGRect rect);

更多代码示例, 可以参考RSKImageCropper

参考

  • SDWebImage

  • How is SDWebImage better than X?

  • How to support webp?

  • Understanding SDWebImage - Decompression

  • SDWebImage实现分析

  • Mobile design 101: pixels, points and resolutions

更多文章, 请支持我的个人博客

你可能感兴趣的:(iOS开发 之 SDWebImage源码分析)