品读优秀的第三方框架是最快的技术提升方式吧,在工作中,很多时候在思考如何封装一个功能组件时总会各种犹豫,为打破这种顾此失彼的状态,果断看一些源码,学习别人编程思想,并从中借鉴模仿其手法。 花了一周看这个框架顺便写个总结吧!

SDWebImage 源代码结构

SDWebImage 主要功能就是图片的异步下载+缓存,其次也包含了图片的解码和清除等;源码下载地址 SDWebImage Github 上源码(下载最新的 3.7.0 版本),下载后打开工程代码文件结构还是很清晰的,从文件命名初步分析整体如下图:

SDWebImage 代码文件结构图
再从我们使用 SDWebImage 调用 API 入口入手一步步看调用到的方法和每个文件每个类,主要解析分为以下几类:

  1. WebCache Categories 面向开发者接口层,开发者直接调用 API 接口异步加载图片,也就是说这是我们读代码的入口,入口方法中会调用管理类(协调调用下载和缓存等),例如 UIImageView+WebCache 类中的:

    1
    2
    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;

  2. SDWebImageManager 是 Utils 管理工具类文件中,其是整个 SDWebImage 的核心负责发起下载和缓存,执行上步骤的方法后,上步骤方法会调用这个类的如下方法,在方法中调用下载和缓存的类;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 其头文件引用了下载和缓存的类
    #import "SDWebImageOperation.h"
    #import "SDWebImageDownloader.h"
    #import "SDImageCache.h"

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
    options:(SDWebImageOptions)options
    progress:(SDWebImageDownloaderProgressBlock)progressBlock
    completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

  3. SDImageCache 缓存对象,负责内存缓存和沙盒缓存的类,核心方法:

    1
    2
    // 根据key从磁盘缓存中获取图片:异步操作
    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
  4. SDWebImageDownloader 下载器也就是下载管理器,下载网络请求的前期设置 NSURLRequest对象请求头的封装、缓存、cookie的设置加载选项的处理及管理 Operation 之间的关系;SDWebImageDownloaderOperation 负责图片下载网络请求的具体操作;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // SDWebImageDownloader 核心方法
    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
    options:(SDWebImageDownloaderOptions)options
    progress:(SDWebImageDownloaderProgressBlock)progressBlock
    completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

    // SDWebImageDownloaderOperation 核心是遵守 NSURLConnectionDataDelegate 代理,实现其代理方法:
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
    - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;

  5. 运用到的其它工具类主要是:SDWebImageCompat 、SDWebImageDecoder、NSData+ImageContentType、UIImage+MultiFormat、UIImage+GIF、UIImage+WebP;

内部方法逻辑流程分析

SDWebImage 实现图片的加载、缓存处理、数据处理、图片下载等工作内部方法时序图:

SDWebImage 逻辑方法流程图

  1. 创建 UIImageView 对象调用其分类 UIImageView+WebCache 的接口方法例如:- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder (常用方法还有其它接口方法);
  2. UIImageView+WebCache 内部最终都会调用方法 (private method) sd_setImageWithURL: placeholderImage: options: progress: completed:;
  3. 上步方法内部实现首先判断需不需要取消当前图片的下载操作(cell 复用一个 UIImageView 多个下载会出现闪图),其调用的核心方法是 UIView+WebCacheOperation - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key,这里思考为什么设计为 UIView 的分类,很好理解因为 UIButton 也要调用, 他们都是继承 UIView,其次调用者都是 self 对象本身;
  4. 第二步中内部方法中执行完第三步逻辑(判断完是否取消当前图片下载操作)后,继续执行,调用负责发起下载和缓存 SDWebImageManager 中的方法 downloadImageWithURL: options: progress: completed: ;
  5. SDWebImageManager 对象的上述方法执行过程中通过调用 SDImageCache 对象的 queryDiskCacheForKey: done: 方法去查询缓存相关操作,SDImageCache 对象活动完成后将结果返回;
  6. SDWebImageManager 对象的方法执行完判断缓存相关操作,继续执行调用 SDWebImageDownloader (下载器)对象的 downloadImageWithURL: options: progress: completed: 发起下载操作;具体的图片网络下载是由 SDWebImageDownloader 对象调用 SDWebImageDownloaderOperation 完成;
  7. 图片下载完成后图片结果 block 回调到 SDWebImageManager 对象中,SDWebImageManager 继续调用 SDImageCache 对象执行图片存储操作;

核心工具类分析

1. SDWebImageCompat 适配类

只有一个内联函数判断 @2x 和 @3x 图片判断处理操作, 核心代码如下:

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
/**
给定一张图片,通过 scale 放大因子返回一个放大的图片

@param key 图片名称
@param image 资源图片
@return 处理以后的图片
*/
inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image) {
// 判断是否为 retina 屏, 即 retina 屏绘图时有放大因子
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
CGFloat scale = 1;
// "@2x.png" 只有是2倍图3倍图图片名称长度一定大于7
if (key.length >= 8) {
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;
}

2. SDWebImageDecoder 解码

UIImage 的分类扩展一个类方法用于图片的解码操作,这里疑问是本来就是图片为何还要解码到 UIImage?图片的加载常使用 [UIImage imageNamed:@"xxx.png"],这种方式加载系统会把图像 Cache 到内存并且系统默认会在主线程对图片进行解码(系统自行完成 可看看 Core Graphics 框架),图像资源大或者图片多,这种方式会消耗很大的内存;解决这种问题可以使用 imageWithContentsOfFile: 图片加载方式,也可以将图片解码过程我们来把控提前在子线程中完成;

UIImage 是显示图片的高级方式,是不可变的,因而可安全的供线程使用;CGImage 是图片位图,是可变的,可调用 Core Graphics 层 API 改变 bitmaps 数据;CIImage 也包含图片相关数据的图片对象Core Image 层的不可变的;

3. NSData+ImageContentType 图片格式判断

NSData 的分类扩展一个类方法用于判断图片格式

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
/**
根据图片 NSData 判断图片类型

@param data 图片的NSData
@return NSString 图片类型
*/
+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
// 1字节 char 类型的别名
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.UIImage+GIF 生成 GIF UIImage 对象

UIImage 分类扩展实现对GIF图片的 NSData 的处理,生成 GIF 格式的UIImage 对象,核心代码:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
根据 GIF 图片的 data 生成对应的 GIF 的 UIImage 对象

@param data GIF 图片的 NSData
@return GIF 图片
*/
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
if (!data) {
return nil;
}

CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

// 获取 GIF 图片包含的 CGImage 帧数
size_t count = CGImageSourceGetCount(source);

UIImage *animatedImage;

// 如果只有 1 帧,单张图片
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];

[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];

CGImageRelease(image);
}

if (!duration) {
duration = (1.0f / 10.0f) * count;
}

animatedImage = [UIImage animatedImageWithImages:images duration:duration];
}

CFRelease(source);

return animatedImage;
}

+ (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
float frameDuration = 0.1f;
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];

NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
if (delayTimeUnclampedProp) {
frameDuration = [delayTimeUnclampedProp floatValue];
}
else {

NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
if (delayTimeProp) {
frameDuration = [delayTimeProp floatValue];
}
}

if (frameDuration < 0.011f) {
frameDuration = 0.100f;
}

CFRelease(cfFrameProperties);
return frameDuration;
}

5.UIImage+MultiFormat

实现 NSData 转 UIImage 对象

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
data 转 UIImage

@param data 图片 data
@return UIImage 对象
*/
+ (UIImage *)sd_imageWithData:(NSData *)data {
if (!data) {
return nil;
}

UIImage *image;
// 根据 data 的第一个字节判断获取图片格式
NSString *imageContentType = [NSData sd_contentTypeForImageData:data];
if ([imageContentType isEqualToString:@"image/gif"]) {
// 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];
}
}

return image;
}

/**
有图片 data 获取图片方向

@param imageData 图片数据
@return UIImageOrientation 图片方向枚举值
*/
+(UIImageOrientation)sd_imageOrientationFromImageData:(NSData *)imageData {
// 默认向上
UIImageOrientation result = UIImageOrientationUp;
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (imageSource) {
// 获取图片的属性列表
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
CFTypeRef val;
int exifOrientation;
// 获取图片方向
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) {
CFNumberGetValue(val, kCFNumberIntType, &exifOrientation);
result = [self sd_exifOrientationToiOSOrientation:exifOrientation];
} // else - if it's not set it remains at up
CFRelease((CFTypeRef) properties);
} else {
//NSLog(@"NO PROPERTIES, FAIL");
}
CFRelease(imageSource);
}
return result;
}

tmp

  1. webp 图片格式

依赖 libwebp 库是 Google 出的图片库,他的好处就是比 jpg 图片格式压缩更厉害小省流量减轻服务器压力等;

  1. app 执行过程

app 执行过程堆栈分析最先调用的是 start 函数(系统内核准备工作动态库加载).dylid 这个库自身也是一个动态库但他的作用是动态链接器 dyld 内部会调用 _dyld_start()函数 再调用 dyldbootstrap::start() 函数,执行了函数之后再走 main 函数,再开启 runloop 走完了之后再开启队列,然后再走 UIApplication 内部方法,再走 didfinsh 方法。