SDWebImage 内部实现了一个图片解码/编码的图片编码器,用于图片的编解码、压缩。其中涉及到编解码管理(SDWebImageCodersManager)、编解码协议(SDWebImageCoder)、PNG/JPEG/TIFF编解码,解压缩,显示大图(SDWebImageImageIOCoder)、GIF编解码(SDWebImageGIFCoder)、WebP编解码(SDWebImageWebPCoder)以及辅助类(SDWebImageCoderHelper

本篇只做 SDWebImageCoderHelper 类的分析。该类作为辅助类,主要实现了两个功能:动图处理 和 图片方向处理。每个功能有两种方法,均为互逆方法。

SDWebImageFrame类对象

在动图处理期间,使用到了 SDWebImageFrame 对象,该对象是使用单独的类实现的,在GIF等动态图使用中作为每一帧的显示作用。该类用于通过SDWebImageCoderHelper中的animatedImageWithFrames创建动画图片。 如果需要指定动画图片循环计数,请在“UIImage + MultiFormat”中使用sd_imageLoopCount属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
当前帧的图片
*/
@property (nonatomic, strong, readonly, nonnull) UIImage *image;
/**
要显示的当前帧的持续时间。 数字是秒而不是毫秒。不应将此值设置为零。
*/
@property (nonatomic, readonly, assign) NSTimeInterval duration;

/**
使用指定图片和持续时间创建框架实例

@param image 当前帧的图片
@param duration 当前帧的持续时间
@return 返回框架实例
*/
+ (instancetype _Nonnull)frameWithImage:(UIImage * _Nonnull)image duration:(NSTimeInterval)duration;

初始化方法实现

1
2
3
4
5
6
7
+ (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
SDWebImageFrame *frame = [[SDWebImageFrame alloc] init];
frame.image = image;
frame.duration = duration;

return frame;
}

动图处理

根据SDWebImageFrame帧数组返回动图

1
2
3
4
5
6
7
8
9
/**
根据SDWebImageFrame帧数组返回动图
对于UIKit,这将应用补丁,然后创建动画UIImage。 补丁是因为`+ [UIImage animatedImageWithImages:duration:]`只使用每个图片的平均持续时间。 因此,如果不同的帧具有不同的持续时间,则不起作用 因此,我们重复指定帧的指定帧以使其工作。
对于AppKit,NSImage不支持GIF以外的动画。 这将尝试将帧编码为GIF格式,然后创建用于渲染的动画NSImage。 注意,如果输入帧包含完整的Alpha通道,动画图片可能会丢失一些细节,因为GIF仅支持1位alpha通道。 (1个像素,透明或不透明)

@param frames SDWebImageFrame帧数组. 如果数组为空或者nil,返回nil
@return 用于在UIImageView(UIKit)或NSImageView(AppKit)上渲染的动画图片
*/
+ (UIImage * _Nullable)animatedImageWithFrames:(NSArray<SDWebImageFrame *> * _Nullable)frames;

方法实现

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
78
79
80
81
82
83
84
85
86
87
+ (UIImage *)animatedImageWithFrames:(NSArray<SDWebImageFrame *> *)frames {
//如果数组元素为空,返回动图为nil
NSUInteger frameCount = frames.count;
if (frameCount == 0) {
return nil;
}
// 生成临时变量保存动图
UIImage *animatedImage;

//如果是iOS 、iWatch 、 Apple TV
#if SD_UIKIT || SD_WATCH
//生成一个元素类型为非负整数,长度为动图帧数的数组个数,保存每一帧的展示时间
NSUInteger durations[frameCount];
for (size_t i = 0; i < frameCount; i++) {
//遍历SDWebImageFrame对象数组,获取每一帧的展示时间
durations[i] = frames[i].duration * 1000;
}
计算所有帧展示时长的最大公约数
NSUInteger const gcd = gcdArray(frameCount, durations);
//临时变量保存总时长
__block NSUInteger totalDuration = 0;
//创建一个可变数组,长度为动图帧数的数组个数
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
//遍历传入的动图帧数的数组
[frames enumerateObjectsUsingBlock:^(SDWebImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
// 获取SDWebImageFrame对象保存的每一帧的图像
UIImage *image = frame.image;
// 获取SDWebImageFrame对象保存的每一帧的展示时间
NSUInteger duration = frame.duration * 1000;
//总时长
totalDuration += duration;
//临时变量,保存重复次数
NSUInteger repeatCount;
// 如果计算出的最大公约数大于零,每一帧的重复次数就是展示时间除以最大公约数
// 否则每一帧只重复一次,也就说不重复
if (gcd) {
repeatCount = duration / gcd;
} else {
repeatCount = 1;
}
// 根据重复次数向动图数组中重复添加同一帧
for (size_t i = 0; i < repeatCount; ++i) {
[animatedImages addObject:image];
}
}];

获得动图
animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];

#else //如果不是(iOS 、iWatch 、 Apple TV )

NSMutableData *imageData = [NSMutableData data];
CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatGIF];
// 创建图像目标。 GIF不支持EXIF图像方向
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frameCount, NULL);
if (!imageDestination) {
// 处理失败。
return nil;
}

//遍历传入的动图帧数的数组
for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
// 获取SDWebImageFrame对象保存的每一帧的图像
SDWebImageFrame *frame = frames[i];
// 获取SDWebImageFrame对象保存的每一帧的展示时间
float frameDuration = frame.duration;

CGImageRef frameImageRef = frame.image.CGImage;
NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
}
}
// Finalize the destination.
if (CGImageDestinationFinalize(imageDestination) == NO) {
// 处理失败。
CFRelease(imageDestination);
return nil;
}
CFRelease(imageDestination);
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
animatedImage = [[NSImage alloc] initWithSize:imageRep.size];
[animatedImage addRepresentation:imageRep];
#endif

return animatedImage;
}

根据动图返回SDWebImageFrame帧数组

1
2
3
4
5
6
7
8
9
/**
根据动图返回SDWebImageFrame帧数组
对于UIKit,这将取消应用上述描述的补丁,然后创建frames数组。 这也适用于普通的动画UIImage。
对于AppKit,NSImage不支持GIF以外的动画。 这将尝试解码GIF imageRep,然后创建帧数组。

@param animatedImage 一个动图.如果不是动图,返回nil
@return 返回SDWebImageFrame帧数组
*/
+ (NSArray<SDWebImageFrame *> * _Nullable)framesFromAnimatedImage:(UIImage * _Nullable)animatedImage;

方法实现

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
+ (NSArray<SDWebImageFrame *> *)framesFromAnimatedImage:(UIImage *)animatedImage {
//如果动图不存在,返回nil
if (!animatedImage) {
return nil;
}

//初始化两个临时变量: SDWebImageFrame对象 和帧个数
NSMutableArray<SDWebImageFrame *> *frames = [NSMutableArray array];
NSUInteger frameCount = 0;

#if SD_UIKIT || SD_WATCH
//获取动图的帧图片数组
NSArray<UIImage *> *animatedImages = animatedImage.images;
//获取动图的帧图片数量
frameCount = animatedImages.count;
//如果帧图片数量为0,表示不是动图,返回nil
if (frameCount == 0) {
return nil;
}
//计算每一帧的平均展示时间
NSTimeInterval avgDuration = animatedImage.duration / frameCount;
//如果这个动图没有展示时间就默认每一帧展示100毫秒 ,即0.1秒(这没有像GIF或WebP那样的10ms限制,以允许自定义编码器提供限制)
if (avgDuration == 0) {
avgDuration = 0.1;
}

// 记录不同帧图片的数量
__block NSUInteger index = 0;
//记录一帧图片重复次数
__block NSUInteger repeatCount = 1;
//记录当前遍历到的图片之前的图片
__block UIImage *previousImage = animatedImages.firstObject;

//遍历图片数组
[animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
// 第一张图片不处理,忽略掉
if (idx == 0) {
return;
}
//如果这一帧的图片和之前一帧图片相同就添加重复次数
if ([image isEqual:previousImage]) {
repeatCount++;
} else {// 如果两帧图片不相同,就生成SDWebImageFrame对象
SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
// 数组记录对象
[frames addObject:frame];
// 重复次数设置为1次
repeatCount = 1;
// 记录不同的帧图片的数量加1
index++;
}
//记录当前图片,用于下次遍历使用
previousImage = image;
// 最后一张图片
if (idx == frameCount - 1) {
// 如果是最后一张照片就直接添加
SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
[frames addObject:frame];
}
}];

#else //以下非iOS实现,一些东西不同,不做分析

NSBitmapImageRep *bitmapRep;
for (NSImageRep *imageRep in animatedImage.representations) {
if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
bitmapRep = (NSBitmapImageRep *)imageRep;
break;
}
}
if (bitmapRep) {
frameCount = [[bitmapRep valueForProperty:NSImageFrameCount] unsignedIntegerValue];
}

if (frameCount == 0) {
return nil;
}

for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
//NSBitmapImageRep需要手动更改帧。 “Good taste”API
[bitmapRep setProperty:NSImageCurrentFrame withValue:@(i)];
float frameDuration = [[bitmapRep valueForProperty:NSImageCurrentFrameDuration] floatValue];
NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapRep.CGImage size:CGSizeZero];
SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:frameImage duration:frameDuration];
[frames addObject:frame];
}
}
#endif

return frames;
}

图片方向处理

将EXIF图片方向转换为iOS图片方向

1
2
3
4
5
6
7
/**
将EXIF图片方向转换为iOS图片方向。

@param exifOrientation EXIF图片方向
@return iOS图片方向
*/
+ (UIImageOrientation)imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation;

方法实现

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
// 将EXIF图片方向转换为iOS图片方向。根据EXIF图片方向做判断,返回iOS图片方向
+ (UIImageOrientation)imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation {
// CGImagePropertyOrientation在上面的iOS 8上可用。 目前保持兼容性
UIImageOrientation imageOrientation = UIImageOrientationUp;
switch (exifOrientation) {
case 1:
imageOrientation = UIImageOrientationUp;
break;
case 3:
imageOrientation = UIImageOrientationDown;
break;
case 8:
imageOrientation = UIImageOrientationLeft;
break;
case 6:
imageOrientation = UIImageOrientationRight;
break;
case 2:
imageOrientation = UIImageOrientationUpMirrored;
break;
case 4:
imageOrientation = UIImageOrientationDownMirrored;
break;
case 5:
imageOrientation = UIImageOrientationLeftMirrored;
break;
case 7:
imageOrientation = UIImageOrientationRightMirrored;
break;
default:
break;
}
return imageOrientation;
}

将iOS方向转换为EXIF图片方向

1
2
3
4
5
6
7
/**
将iOS方向转换为EXIF图片方向

@param imageOrientation iOS方向
@return EXIF图片方向
*/
+ (NSInteger)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation;

方法实现

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
// 将iOS方向转换为EXIF图片方向,根据iOS方向做判断,返回EXIF图片方向
+ (NSInteger)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation {
// CGImagePropertyOrientation 在上面的iOS 8上可用。 目前保持兼容性
NSInteger exifOrientation = 1;
switch (imageOrientation) {
case UIImageOrientationUp:
exifOrientation = 1;
break;
case UIImageOrientationDown:
exifOrientation = 3;
break;
case UIImageOrientationLeft:
exifOrientation = 8;
break;
case UIImageOrientationRight:
exifOrientation = 6;
break;
case UIImageOrientationUpMirrored:
exifOrientation = 2;
break;
case UIImageOrientationDownMirrored:
exifOrientation = 4;
break;
case UIImageOrientationLeftMirrored:
exifOrientation = 5;
break;
case UIImageOrientationRightMirrored:
exifOrientation = 7;
break;
default:
break;
}
return exifOrientation;
}

两个私有方法: 计算最大公约数

计算两个整数a和b的最大公约数

1
2
3
4
5
6
7
8
9
static NSUInteger gcd(NSUInteger a, NSUInteger b) {
NSUInteger c;
while (a != 0) {
c = a;
a = b % a;
b = c;
}
return b;
}

计算一个整数数组的最大公约数

1
2
3
4
5
6
7
8
9
10
static NSUInteger gcdArray(size_t const count, NSUInteger const * const values) {
if (count == 0) {
return 0;
}
NSUInteger result = values[0];
for (size_t i = 1; i < count; ++i) {
result = gcd(values[i], result);
}
return result;
}