SDWebImage 源码分析

文章目录
  1. 1. 协调器 Manager
  2. 2. 子模块 SDImageLoader
    1. 2.1. SDImageLoader 协议
    2. 2.2. SDWebImageDownloaderOptions
    3. 2.3. SDWebImageContext
    4. 2.4. 具体下载逻辑 SDWebImageDownloader
  3. 3. 子模块 SDImageCache
    1. 3.1. SDMemoryCache
    2. 3.2. SDDiskCache
  4. 4. 解码
    1. 4.1. 图像渲染优化
    2. 4.2. 绘制到 UIGraphicsImageRenderer 上
  5. 5. 引用

SDWebImage源码

框架架构的主要目的是为了将项目模块化,分层,把代码解耦从而提高代码的复用

SDWebImage 异步下载并支持缓存,图片编码解码的框架

frame

官方提供整体类图

顶层结构

整体类图

协调器 Manager

SDWebImageManagerClassDiagram

  1. Manager 使用中介者模式,用于管理协调各个子模块之间交互
  2. SDWebImageManager 依赖 SDWebImageOptions(配置信息),各个子模块都是聚合关系
    1. 子模块都是协议!使用协议作为隔离层用于解耦 Manager 跟子模块,Manager 不需要知道具体的子模块到底是什么
    2. 聚合的关系,说明子模块可以独立于 Manager 而独立使用
  3. cacheKey(for url: URL?) -> String?
    1. 使用 url 作为cache key,用于读写 image

子模块分析,先分析 imageLoader和 imageCache

子模块 SDImageLoader

SDWebImageLoaderClassDiagram

SDImageLoader 协议

  1. SDImageLoader接口具体实现:
    1. SDImageLoadersManager 组合模式,内部维护 Array<SDImageLoader>
    2. SDWebImageDownloader 具体image 下载逻辑实现
  2. SDImageLoader接口依赖 SDWebImageDownloaderOptions,配置信息跟逻辑拆分,聚合配置信息,避免逻辑方法调用过程中配置参数零散
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public protocol SDImageLoader : NSObjectProtocol {
// 判断 url 的有效性,如果无效会标记 image 加载失败,有效就会调用requestImageWithURL:options:context:progress:completed:
func canRequestImage(for url: URL?) -> Bool

/**
根据 url 下载 image data

参数:
url:image url,有可能是 http url
options: 针对 request 的可选配置信息
context: SDWebImageContextOption,用于处理具体的changes or precess
progressBlock: image 下载的回调,get进度信息。P.S. 后台队列
completedBlock: 下载完成回调
return: request 的 Operation,用户可以自己 cancel
*/
func requestImage(with url: URL?,
options: SDWebImageOptions = [],
context: [SDWebImageContextOption : Any]?,
progress progressBlock: SDImageLoaderProgressBlock?,
completed completedBlock: SDImageLoaderCompletedBlock? = nil)
-> SDWebImageOperation?

/**
来自图像加载器的错误是否应该标记为不可恢的。
如果返回 true,失败的 url放进黑名单 ,不使用`SDWebImageRetryFailed`

error: url 加载的 error,来自于`requestImageWithURL:options:context:progress:completed:` completedBlock's error.
return: 是否把url 放入黑名单
*/
func shouldBlockFailedURL(with url: URL, error: Error) -> Bool
}

Q:在想为什么协议只暴露三个接口??

一些思考

  • 图片请求的并发量
  • 请求超时策略
  • 请求优先级

SDWebImageDownloaderOptions

位枚举,下载器功能自定义配置,用户可更改配置改变 Downloader 中的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public struct SDWebImageDownloaderOptions : OptionSet {
// 下载任务的 priority (Put the download in the low queue priority and task priority.)
public static var lowPriority: SDWebImageDownloaderOptions { get }

// 边下载边显示(This flag enables progressive download, the image is displayed progressively during download as a browser would do.)
public static var progressiveLoad: SDWebImageDownloaderOptions { get }

// 是否使用 urlCache,默认是不使用 (By default, request prevent the use of NSURLCache. With this flag, NSURLCache is used with default policies.)
public static var useNSURLCache: SDWebImageDownloaderOptions { get }

// 如果imageData 从 URLCache 中读取,complete block 会返回 nil,error code 是 `SDWebImageErrorCacheNotModified`, 这个标记应该跟 SDWebImageDownloaderUseNSURLCache 一起使用 (Call completion block with nil image/imageData if the image was read from NSURLCache And the error code is `SDWebImageErrorCacheNotModified` This flag should be combined with `SDWebImageDownloaderUseNSURLCache`.)
public static var ignoreCachedResponse: SDWebImageDownloaderOptions { get }

// 在 iOS 4+ 版本中,app 进入后台继续下载image,需要想系统请求而外的后台时间来让 Request 完成。如果请求时间耗尽,Operation 会被 cancel (In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for extra time in background to let the request finish. If the background task expires the operation will be cancelled.)
public static var continueInBackground: SDWebImageDownloaderOptions { get }

// 处理村粗在 NSHTTPCookieStore 的 cookies (Handles cookies stored in NSHTTPCookieStore by setting NSMutableURLRequest.HTTPShouldHandleCookies = YES;)
public static var handleCookies: SDWebImageDownloaderOptions { get }

// 允许不可信的 SSL 认证,主要用于测试(Enable to allow untrusted SSL certificates. Useful for testing purposes. Use with caution in production.)
public static var allowInvalidSSLCertificates: SDWebImageDownloaderOptions { get }

// 下载优先级,将下载 task 放在 高 priorty 队列中 (Put the download in the high queue priority and task priority.)
public static var highPriority: SDWebImageDownloaderOptions { get }

// 解码下采样,默认情况,image解码应该跟原始尺寸一致,该 flag 在 iOS中根据设备内存来下采样 image size,如果设置 SDWebImageDownloaderAvoidDecodeImage这个 flag 会失效,如果设置了 SDWebImageDownloaderProgressiveLoad,这个 flag会被忽视(By default, images are decoded respecting their original size. On iOS, this flag will scale down the images to a size compatible with the constrained memory of devices. This flag take no effect if `SDWebImageDownloaderAvoidDecodeImage` is set. And it will be ignored if `SDWebImageDownloaderProgressiveLoad` is set.)
public static var scaleDownLargeImages: SDWebImageDownloaderOptions { get }

// 解码,默认情况,在查询 image cache or download from the network的时候,使用后台任务解码 image(在使用UIImage的时候 Core Animation是在 main thread 解码image的)。后台解码会增加内存,该flag 标记解码过程由用户自己处理 (By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation. However, this process may increase the memory usage as well. If you are experiencing a issue due to excessive memory consumption, This flag can prevent decode the image.)
public static var avoidDecodeImage: SDWebImageDownloaderOptions { get }

// gif image 解码,该 flag 标记框架只解码一帧 (By default, we decode the animated image. This flag can force decode the first frame only and produce the static image.)
public static var decodeFirstFrameOnly: SDWebImageDownloaderOptions { get }

//gif image 解码,该flag 标记框架解码所有帧 (By default, for `SDAnimatedImage`, we decode the animated image frame during rendering to reduce memory usage. This flag actually trigger `preloadAllAnimatedImageFrames = YES` after image load from network)
public static var preloadAllFrames: SDWebImageDownloaderOptions { get }

//gif image 解码, 默认情况当你使用 `SDWebImageContextAnimatedImageClass` 的时候,我们依然使用 UIImage,当 cache 命中,或者image 解码器不可用,作为一个 fallback 方案,使用这个 option,可以确保使用你自己提供的 class来生成 image,如果失败会产生 `SDWebImageErrorBadImageData`,注意这个 flag 跟 `SDWebImageDownloaderDecodeFirstFrameOnly` 不兼容 (By default, when you use `SDWebImageContextAnimatedImageClass` context option (like using `SDAnimatedImageView` which designed to use `SDAnimatedImage`), we may still use `UIImage` when the memory cache hit, or image decoder is not available, to behave as a fallback solution. Using this option, can ensure we always produce image with your provided class. If failed, a error with code `SDWebImageErrorBadImageData` will been used. Note this options is not compatible with `SDWebImageDownloaderDecodeFirstFrameOnly`, which always produce a UIImage/NSImage.)
public static var matchAnimatedImageClass: SDWebImageDownloaderOptions { get }
}

SDWebImageContext

根据字典拿到辅助功能对象

1
2
typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM;
typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SDWebImageContextOption const SDWebImageContextSetImageOperationKey = @"setImageOperationKey";
SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager";
SDWebImageContextOption const SDWebImageContextImageCache = @"imageCache";
SDWebImageContextOption const SDWebImageContextImageLoader = @"imageLoader";
SDWebImageContextOption const SDWebImageContextImageCoder = @"imageCoder";
SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransformer";
SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor";
SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio";
SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize";
SDWebImageContextOption const SDWebImageContextQueryCacheType = @"queryCacheType";
SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType";
SDWebImageContextOption const SDWebImageContextOriginalQueryCacheType = @"originalQueryCacheType";
SDWebImageContextOption const SDWebImageContextOriginalStoreCacheType = @"originalStoreCacheType";
SDWebImageContextOption const SDWebImageContextAnimatedImageClass = @"animatedImageClass";
SDWebImageContextOption const SDWebImageContextDownloadRequestModifier = @"downloadRequestModifier";
SDWebImageContextOption const SDWebImageContextDownloadResponseModifier = @"downloadResponseModifier";
SDWebImageContextOption const SDWebImageContextDownloadDecryptor = @"downloadDecryptor";
SDWebImageContextOption const SDWebImageContextCacheKeyFilter = @"cacheKeyFilter";
SDWebImageContextOption const SDWebImageContextCacheSerializer = @"cacheSerializer";

为什么使用字典而不是在创建一个类呢?
可能这些类or配置信息都不重要,使用字典管理

具体下载逻辑 SDWebImageDownloader

downloader

  1. 依赖 SDWebImageDownloadToken (关联具体的 downloadOperation)
  2. 依赖 SDWebImageDownloadOptions
  3. 依赖 SDWebImageDownloaderOperation (内部封装 downloadTask,coderQueue)
  4. 依赖 SDWebImageDownloaderConfig (下载配置信息)
  5. 聚合 SDWebImageDownloaderRequestModifier (在请求前修改 request)
  6. 聚合 SDWebImageDownloaderResponsetModifier (在响应后修改 response)
  7. 聚合 SDWebImageDownloaderDecryptor
  8. 聚合 NSURLSessionConfiguration (下载operation 配置的 session 环境)

私有属性,维护下载任务

  1. URLSession
  2. OperationQueue,URLOperations

downloader_class

大致过程

  1. SDWebImageDownloader.init
    1. 配置 downloader config
    2. downloadQueue 并发 default 6 thread
    3. 配置 httpHeader
    4. 配置 urlSession
  2. url -> SDWebImageDownloadToken -> SDWebImageDownloaderOperation
    1. 配置 request
    2. 根据 context 配置 SDWebImageDownloaderOperation
      1. operation.start 中封装 dataTask
      2. 配置 coderQueue(serial) SDWebImageDownloaderOperation taskDelegate receive data 回调中边下载data 边解码
      3. dataTask 结束后调用 operation 的 completeBlock 将 imageData 交给用户

子模块 SDImageCache

内存:当前程序运行空间,空间小,数据易丢失,读取快
程序运行后,内存分为 5 个区:栈区,堆区,全局区,常量区,代码区
内存缓存设计一般使用:栈区,堆区

磁盘:空间大、可持久、读取速度慢

iOS 主要提供4种磁盘存储方式:

  • NSKeyedArchiver:归档的形式来保存数据,只能一次性归档保存以及一次性解压。所以只能针对小量数据,如果想改动数据的某一小部分,需要解压整个数据或者归档整个数据。
  • NSUserDefaults:用来保存应用程序设置和属性、用户保存的数据。存储的数据类型少,包括:NSData、NSString、NSNumber、NSDate、NSArray、 NSDictionary
  • plist:userDefault就是一个 plist
  • FileManager 相关 write 写入方式:永久保存在磁盘中
  • 数据库

App 的沙盒根目录结构:

  1. Documents
  2. Library
  3. temp

大体结构,在网络层的 operation 中 coderQueue(serial) 负责解码 image

cache

cache 读取方法:使用url的单向 hash值来读写
cache

  1. 依赖 SDImageCacheOptions
  2. 聚合 SDMemoryCache
  3. 聚合 SDDiskCache
  4. 聚合 SDImageCacheConfig
  5. 使用组合模式 SDImageCachesManager 做分支节点,SDImageCache 做叶子节点
  6. SDImageCacheManager 依赖 SDImageCachesManagerOperationPolicy(用于处理多个 cache)

SDWebImageCacheClassDiagram

计算机组成原理:层级存储器设计思想

SDMemoryCache

内存管理设计
内存空间(因为是内存,使用什么数据结构?)
3个队列:
50 * 10 kb
20 * 100 kb
10 * 100+kb

队列淘汰策略
FIFO 算法
LRU 算法(检查时机)

内部使用NSCache 实现缓存
NSMapTable 作为弱引用缓存

SDDiskCache

磁盘管理设计
存储方式
空间大小限制
淘汰策略

根据文件属性删减文件

  • (void)removeExpiredData

解码

  • 使用策略模式针对不同图片解码
  • 解码时机:
    • 图片数据下载完成
    • 磁盘读取后

图像渲染优化

优化的方式有很多,架构层次跨度上从底层的 Core Graphics、vImage、Image I/O 到上层的 Core Image 和 UIKit 都有。

  1. 绘制到 UIGraphicsImageRenderer 上
  2. 绘制到 Core Graphics Context 上
  3. 使用 Image I/O 创建缩略图像
  4. 使用 Core Image 进行 Lanczos 重采样
  5. 使用 vImage 优化图片渲染

绘制到 UIGraphicsImageRenderer 上

为了统一调用方式,以下的每种技术共用一个公共接口方法:

1
2
func resizedImage(at url: URL, for size: CGSize) -> UIImage? { <#...#> }
imageView.image = resizedImage(at: url, for: size)

这里,size 的计量单位不是用 pixel,而是用 point。想要计算出你调整大小后图像的等效尺寸,用主 UIScreen 的 scale,等比例放大你 UIImageView 的 size 大小:

let scaleFactor = UIScreen.main.scale
let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
let size = imageView.bounds.size.applying(scale)

引用

Image Resizing Techniques