SDWebImage 4.x & 5.x 对 GIF 类型的处理问题

之前工作中遇到的问题,使用 SDWebImage(v4.4.8)下载并存储了一份 GIF 图片,第一次是可以正常显示的,但之后再从缓存中取出的时候就变成了静态图片。debug 发现是因为从磁盘里取出该图片时已经是 PNG 格式的了。为了解决这个问题所以去仔细看了下 SD 对 GIF 的处理。

测试代码大概是这个样子的:

1
2
3
4
5
6
NSURL *url = [NSURL URLWithString:@"http://img.xxx.gif"];
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:url options:SDWebImageDownloaderLowPriority progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {

[[SDImageCache sharedImageCache] storeImage:image forKey:url.absoluteString completion:nil];
}];

发现问题主要是出在 SDImageCache 这个类的 storeImage:forKey:completion: 方法,在 L192-L197 行是这样的:

1
2
3
4
5
6
7
8
9
10
if (!data && image) {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
SDImageFormat format;
if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}

注释也写的明白:

If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format

判断图片格式主要用的这个方法:NSData+ImageContentType.m #L23-L70

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}

// File signatures table: http://www.garykessler.net/library/file_sigs.html
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
case 0x52: {
...

因为所用的缓存方法只传了 UIImage 进去,没有 NSData,SD 无法判断图片格式,所以就根据 alpha channel 使用了 PNG 或者 JPEG 格式。

当时为了快速解决 bug,项目上线。所以将 storeImage:forKey:completion: 方法替换为了 storeImage:imageData:forKey:toDisk:completion:,即:

1
2
3
4
5
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:url options:SDWebImageDownloaderLowPriority progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
// 替换缓存方法为
[[SDImageCache sharedImageCache] storeImage:image imageData:data forKey:url.absoluteString toDisk:YES completion:nil];
}];

并在代码里加了备注,解释了原因及提醒了相关维护同学。

这个周末有空了,突然想起来这个问题,想着既然图片是由 SD 下载的,那它肯定能拿到图片的 NSData。既然这样,可以在下载的时候根据 NSData 识别图片格式,给 UIImage 打个标签,这样,在存储的时候不就可以识别到该图片的 GIF 格式了嘛。然后就去浏览了下 SD 的源码,发现,其实 SD 已经给所有它经手的图片打过了标签:sd_imageFormat,即 UIImage+Metadata 这个分类的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (SDImageFormat)sd_imageFormat {
SDImageFormat imageFormat = SDImageFormatUndefined;
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_imageFormat));
if ([value isKindOfClass:[NSNumber class]]) {
imageFormat = value.integerValue;
return imageFormat;
}
// Check CGImage's UTType, may return nil for non-Image/IO based image
if (@available(iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0, *)) {
CFStringRef uttype = CGImageGetUTType(self.CGImage);
imageFormat = [NSData sd_imageFormatFromUTType:uttype];
}
return imageFormat;
}

- (void)setSd_imageFormat:(SDImageFormat)sd_imageFormat {
objc_setAssociatedObject(self, @selector(sd_imageFormat), @(sd_imageFormat), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

既然如此的话,那在 SDImageCache 这个类的 storeImage:forKey:completion: 方法 L192-L197 行优先识别下 sd_imageFormat 不就好了,即:

1
2
3
4
5
6
7
8
9
10
11
12
if (!data && image) {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
SDImageFormat format = image.sd_imageFormat;
if (format == SDImageFormatUndefined) {
if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
}
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}

虽然那些未经 SD 处理过的 GIF 格式的 UIImage 使用该方法时仍然会判定为 PNG 或者 JPEG,但经过 SD 下载来的图片做缓存存取的话都会保证格式的。我提了 Issue #2952 ,SD 的维护者已经采纳了意见,提交了 PR #2953,改动了代码到 Master 了,预计 SD 5.6.0 版本会生效。

看到 5.6.0 版本可能会很突兀,文章开头不是说 SDWebImage(v4.4.8)吗?

是这样的,我到 SD 官网查代码的时候发现 SD 在去年就发布了 5.x 版本。经测试,存储 GIF 图片再取出时仍然是动图,似乎已经修复了这个问题,但是通过 debug 发现,仍然是动图的原因是 SD 将图片的编解码方式改了,实际存储到磁盘的是 _UIAnimatedImage 类型,即直接设置 _UIAnimatedImage 类型给 UIImageView.image 也是动图效果,但是从取出图片的 NSData 头发现仍然是 0x89 PNG 类型。这也是为什么我仍然在 5.x 版本提交 Issus 的原因。

顺便提一下,SD v5.x 版本移除了 FLAnimatedImageView+WebCache 分类,新增了 SDAnimatedImageView 类来处理 GIF 等格式。其实,得益于 v5.x 新的编解码方法,直接使用 UIImageView 就可以正常显示 GIF。
你仍然可以使用 FLAnimatedImageView+WebCache 这个分类兼容代码,通过引入 SDWebImageFLPlugin 插件的方式。
可以去了解下 SD 的 README,提供了一堆的 Plugin 和 Coder 来支持各种类型的图片和三方库。这个周末还新增了 SDWebImageLottiePlugin 插件,支持 JSON 动画。
通过无限插件的形式来丰富 SD 的功能,感觉很牛逼。升级升级!

言归正传,现在,我们来看一下关键代码,为什么同样存储为 0x89,v4.x 版本就是静态图片,而 v5.x 就是动图呢?

v4.x

在 v4.x 的代码里,SDWebImageDownloaderOperation 这个类负责下载数据,在 #L418 这一行将 NSData 转换为 UIImage。继续查看 SDWebImageCodersManager 这个类的解码(decodedImageWithData:)方法:

1
2
3
4
5
6
7
8
9
10
11
- (UIImage *)decodedImageWithData:(NSData *)data {
LOCK(self.codersLock);
NSArray<id<SDWebImageCoder>> *coders = self.coders;
UNLOCK(self.codersLock);
for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:data]) {
return [coder decodedImageWithData:data];
}
}
return nil;
}

遍历 self.coders 寻找能够解码当前 NSData 的 coder,而此时 self.coders 数组里也就只有 SDWebImageImageIOCoder 这个类而已,继续查看这个类的解码方法:

1
2
3
4
5
6
7
8
9
10
- (UIImage *)decodedImageWithData:(NSData *)data {
if (!data) {
return nil;
}

UIImage *image = [[UIImage alloc] initWithData:data];
image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];

return image;
}

直接将 data 转成了 UIImage,并且在这个时候根据 data,拿到了图片格式,设置了 sd_imageFormat

这里的 image 就是最终回调的 image 了,然后使用
1、SDImageCache 类的 storeImage:forKey:completion: 方法将该 image 存到磁盘的时候,还是调用的
2、SDWebImageCodersManager 这个类的编码(encodedDataWithImage:format:options:)方法,遍历 self.coders 数组,使用唯一的类:
3、SDWebImageImageIOCoder 的编码方法(encodedDataWithImage:format:)将 image 按 PNG 格式编码成了 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
37
38
39
40
41
42
43
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
if (!image) {
return nil;
}

if (format == SDImageFormatUndefined) {
BOOL hasAlpha = SDCGImageRefContainsAlpha(image.CGImage);
if (hasAlpha) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
}

NSMutableData *imageData = [NSMutableData data];
CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:format];

// Create an image destination.
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
if (!imageDestination) {
// Handle failure.
return nil;
}

NSMutableDictionary *properties = [NSMutableDictionary dictionary];
#if SD_UIKIT || SD_WATCH
NSInteger exifOrientation = [SDWebImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
[properties setValue:@(exifOrientation) forKey:(__bridge NSString *)kCGImagePropertyOrientation];
#endif

// Add your image to the destination.
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);

// Finalize the destination.
if (CGImageDestinationFinalize(imageDestination) == NO) {
// Handle failure.
imageData = nil;
}

CFRelease(imageDestination);

return [imageData copy];
}

所以这一步存储的时候如果可以通过 sd_imageFormat 拿到正确的 GIF 格式的话,在存储时按照 GIF 格式编码,那么下次取出来动图还是可以生效的。

v5.x

在 v5.x 的代码里,仍然是 SDWebImageDownloaderOperation 这个类负责下载数据,在 #L464 这行处理 NSData 数据。继续查看 SDImageLoader 这个类提供的 SDImageLoaderDecodeImageData 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
if (!decodeFirstFrame) {
// check whether we should use `SDAnimatedImage`
Class animatedImageClass = context[SDWebImageContextAnimatedImageClass];
if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions];
if (image) {
// Preload frames if supported
if (options & SDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
[((id<SDAnimatedImage>)image) preloadAllFrames];
}
} else {
// Check image class matching
if (options & SDWebImageMatchAnimatedImageClass) {
return nil;
}
}
}
}
if (!image) {
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
}
...

在这个方法里判断是否要用 SDAnimatedImage 这个类来解码,主要给 SDAnimatedImageView 或者 FLAnimatedImageView 展示动图时用的。

这里也是和 v4.x 设计不同的地方:v4.x 是在 FLAnimatedImageView+WebCache 分类里拿到回调的 image 和 data 之后再将 data 交由 FLAnimatedImage 处理,而 v5.x 由自己处理动图的编解码工作,在 SDAnimatedImageView+WebCache / FLAnimatedImageView+WebCache 分类里下载图片一开始打上 SDWebImageContextAnimatedImageClass 的标记,然后在当前方法里判断标记从而决定是否使用 SDAnimatedImage 解码。

我们这里是直接使用的 SDWebImageDownloader 下载图片,所以最终会由 SDImageCodersManager 这个类的 decodedImageWithData:options:方法来解码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
if (!data) {
return nil;
}
UIImage *image;
NSArray<id<SDImageCoder>> *coders = self.coders;
for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:data]) {
image = [coder decodedImageWithData:data options:options];
break;
}
}

return image;
}

这里就和 v4.x 很相似了,遍历 self.coders 寻找能够解码当前 NSData 的 coder,而此时 self.coders 数组里有 SDImageIOCoderSDImageGIFCoderSDImageAPNGCoder 三种 coder,当然匹配到 SDImageGIFCoder 来解码当前 data 了,其实走的也是它的父类(SDImageIOAnimatedCoder)方法:decodedImageWithData:options:,即为关键代码:

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

- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
if (!data) {
return nil;
}
CGFloat scale = 1;
NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
if (scaleFactor != nil) {
scale = MAX([scaleFactor doubleValue], 1);
}

CGSize thumbnailSize = CGSizeZero;
NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
if (thumbnailSizeValue != nil) {
#if SD_MAC
thumbnailSize = thumbnailSizeValue.sizeValue;
#else
thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
}

BOOL preserveAspectRatio = YES;
NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
if (preserveAspectRatioValue != nil) {
preserveAspectRatio = preserveAspectRatioValue.boolValue;
}

#if SD_MAC
// If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
// Which decode frames in time and reduce memory usage
if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
imageRep.size = size;
NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
[animatedImage addRepresentation:imageRep];
return animatedImage;
}
#endif

CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!source) {
return nil;
}
size_t count = CGImageSourceGetCount(source);
UIImage *animatedImage;

BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
if (decodeFirstFrame || count <= 1) {
animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize];
} else {
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];

for (size_t i = 0; i < count; i++) {
UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize];
if (!image) {
continue;
}

NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];

SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
[frames addObject:frame];
}

NSUInteger loopCount = [self.class imageLoopCountWithSource:source];

animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
animatedImage.sd_imageLoopCount = loopCount;
}
animatedImage.sd_imageFormat = self.class.imageFormat;
CFRelease(source);

return animatedImage;
}

在这个方法拿到 GIF 的每一帧处理成一组 SDImageFrame 类组成的数组,交由 SDImageCoderHelper 这个类处理:

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
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
NSUInteger frameCount = frames.count;
if (frameCount == 0) {
return nil;
}

UIImage *animatedImage;

#if SD_UIKIT || SD_WATCH
NSUInteger durations[frameCount];
for (size_t i = 0; i < frameCount; i++) {
durations[i] = frames[i].duration * 1000;
}
NSUInteger const gcd = gcdArray(frameCount, durations);
__block NSUInteger totalDuration = 0;
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
[frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
UIImage *image = frame.image;
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

NSMutableData *imageData = [NSMutableData data];
CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:SDImageFormatGIF];
// Create an image destination. GIF does not support EXIF image orientation
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frameCount, NULL);
if (!imageDestination) {
// Handle failure.
return nil;
}

for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
SDImageFrame *frame = frames[i];
NSTimeInterval 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) {
// Handle failure.
CFRelease(imageDestination);
return nil;
}
CFRelease(imageDestination);
CGFloat scale = MAX(frames.firstObject.image.scale, 1);

SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
imageRep.size = size;
animatedImage = [[NSImage alloc] initWithSize:size];
[animatedImage addRepresentation:imageRep];
#endif

return animatedImage;
}

最终调用的关键代码就是:```
animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];

1
2
3
4

option 键看一下这个方法的解释:![animatedImageWithImages:](http://image.gonghonglou.com/sdwebimage-gif/animatedImage.png)

控制台打印下被上述方法处理过后的 animatedImage,看看它具体变成了什么:

(lldb) po animatedImage
<_UIAnimatedImage:0x600002040aa0 anonymous {128, 128}>

(lldb)

1
2
3
4
5
6
7
这个 `_UIAnimatedImage` 类就是为什么直接将该类设置给 UIImageView.image 也是能够展示动图效果的原因了(注意这个类苹果并没有暴露给开发者使用)。

这里的 image 就是最终回调的 image 了,然后使用
1、`SDImageCache` 类的 `storeImage:forKey:completion:` 方法将该 image 存到磁盘的时候,还是调用的
2、`SDWebImageCodersManager` 这个类的编码(`encodedDataWithImage:format:options:`)方法,遍历 self.coders 数组(`SDImageIOCoder`、`SDImageGIFCoder`、`SDImageAPNGCoder`),当前版本v5.5.2使用 `SDImageAPNGCoder`,后期版本,预计v5.6.0改正之后会使用 `SDImageGIFCoder` 来编码,其实都是调用它们的父类:
3、`SDImageIOAnimatedCoder` 的编码方法(`encodedDataWithImage:format:options:`)将 _UIAnimatedImage 由:
4、`SDImageCoderHelper` 根据 image 取出一组 `SDImageFrame` 类(对 UIImage 的封装)组成的数组,并将这一组 UIImage 编码成合理的 NSData,后续存到了磁盘里。**编码方法即为关键代码**:
  • (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options {
    if (!image) {
    return nil;
    }

    if (format != self.class.imageFormat) {
    return nil;
    }

    NSMutableData *imageData = [NSMutableData data];
    CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
    NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];

    // Create an image destination. Animated Image does not support EXIF image orientation TODO
    // The CGImageDestinationCreateWithData will log a warning when count is 0, use 1 instead.
    CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frames.count ?: 1, NULL);
    if (!imageDestination) {
    // Handle failure.
    return nil;
    }
    NSMutableDictionary *properties = [NSMutableDictionary dictionary];
    double compressionQuality = 1;
    if (options[SDImageCoderEncodeCompressionQuality]) {
    compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
    }
    properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);

    BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
    if (encodeFirstFrame || frames.count == 0) {
    // for static single images
    CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
    } else {
    // for animated images
    NSUInteger loopCount = image.sd_imageLoopCount;
    NSDictionary *containerProperties = @{self.class.loopCountProperty : @(loopCount)};
    properties[self.class.dictionaryProperty] = containerProperties;
    CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)properties);

    for (size_t i = 0; i < frames.count; i++) {
    SDImageFrame *frame = frames[i];
    NSTimeInterval frameDuration = frame.duration;
    CGImageRef frameImageRef = frame.image.CGImage;
    NSDictionary *frameProperties = @{self.class.dictionaryProperty : @{self.class.delayTimeProperty : @(frameDuration)}};
    CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
    }
    }
    // Finalize the destination.
    if (CGImageDestinationFinalize(imageDestination) == NO) {
    // Handle failure.
    imageData = nil;
    }

    CFRelease(imageDestination);

    return [imageData copy];

}


以上,就是为什么同样存储为 0x89,v4.x 版本是静态图片,而 v5.x 是动图的原因。并且分析了 SD 的处理逻辑:v4.x & v5.x 都是由下载模块下载资源拿到 data 交由编解码模块解码成 image 回调使用;存储过程又是将 image 交由编解码模块编码成 data 存储到磁盘。

需要注意的版本差异是:
使用 `[[SDImageCache sharedImageCache] storeImage:image forKey:key completion:nil];` 方法存储 GIF 时,从磁盘再次取出该图片:
v5.x 得益于新的编解码方法会保留动图效果,其中 v5.6.0 之前虽然有动图效果,但取出的 NSData 头信息仍然是 0x89 即 PNG 格式;v5.6.0 之后会修正为 0x47 GIF 格式(根据 SD 维护者提供的预计版本,见 [Issue #2952](https://github.com/SDWebImage/SDWebImage/issues/2952) 和 [PR #2953](https://github.com/SDWebImage/SDWebImage/pull/2953))
v4.x 则会失去动图效果

再次需要注意的是:

使用以上方法所存储的 image 如果是 SD 经手过的,由 NSData 类型转换来的 UIImage,SD 会给它打上 `sd_imageFormat` 标签,即能够正确识别图片类型。如果并非 SD 经手的话,即没有 `sd_imageFormat` 标签的情况,该方法还是会将图片编码为 PNG 或者 JPEG,除非使用 `[[SDImageCache sharedImageCache] storeImage:image imageData:imageData forKey:key toDisk:YES completion:nil];` 方法,同时传入 image 和 imageData。

当然,使用该方法传入 imageData 的话,存储过程中就节省了 image 转 data 的操作,这就是 API 易用性和高性能的选择了。