前面有篇文章分析了不同的框架加载gif的性能https://blog.csdn.net/u014600626/article/details/113767876。SD的性能是不错的,CPU占用很低, 而且这个框架很常见, 就想研究下SDWebImage是怎么加载gif图片的。
先看调用的地方, 就是很简单的使用 [imageView sd_setImageWithURL:url](之前的版本可能不支持直接传gif的地址,但是在5.x.x之后肯定是可以了,本文基于5.10.4分析); 或者使用工程中/沙盒里 获取到的二进制数据即可
#pragma mark SD加载image
- (void)testGifImage5 {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.frame];
// 网络图
NSURL *url = [NSURL URLWithString:@"http://res.hongrenshuo.com.cn/66532a15-c726-4edf-bb4f-0b8897753f31.gif?t=1606124887942"];
[imageView sd_setImageWithURL:url];
// 本地图
// NSString * path = [[NSBundle mainBundle] pathForResource:@"直播间测试gif" ofType:@"gif"];
// NSData * data = [NSData dataWithContentsOfFile:path];
// UIImage *image = [UIImage sd_imageWithGIFData:data];
// imageView.image = image;
[self.view addSubview:imageView];
NSLog(@"SD加载image");
}
现在开始。
1,首先我们看下SDWebImage是怎么加载gif的。
对外暴露的方法只有一个,传入gif的二进制数据, 返回生成的UIImage对象,
由于传入的二进制数据, 这个gif可以是在工程中的, 也可以是网络上的图片下载到沙盒,然后读取出来传入给此方法.
+ (nullable UIImage *)sd_imageWithGIFData:(nullable NSData *)data {
if (!data) {
return nil;
}
return [[SDImageGIFCoder sharedCoder] decodedImageWithData:data options:0];
}
2.此方法调用到了SDImageGIFCoder, 但是SDImageGIFCoder中没有实现此方法, 只好到父类中寻找, SDImageGIFCoder的父类是SDImageIOAnimatedCoder, 看下父类中的实现.
看下面方法之前先看下这个东西, 有助于下面的理解:
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); // 解码二进制数据,获取到图片的原始信息
CGImageSourceRef定义如下,typedef struct CGImageSource *CGImageSourceRef;
可以看到它是一个CGImageSource 指针。
CGImageSource又是什么呢?
CGImageSource是对图像数据读取任务的抽象,通过它可以获得图像对象、缩略图、图像的属性(包括Exif信息)。
// 解码二进制数据,生成UIImage对象
- (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) {
thumbnailSize = thumbnailSizeValue.CGSizeValue;
}
BOOL preserveAspectRatio = YES;
NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
if (preserveAspectRatioValue != nil) {
preserveAspectRatio = preserveAspectRatioValue.boolValue;
}
// 调用系统方法生成ImageSource,这里面有图片的原始信息
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!source) {
return nil;
}
// 读取CGImageSourceRef有几个图片对象。判断是不是gif
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 options:nil];
} else {
// 动态图,用一个数组保存每帧的信息,SDImageFrame只有2个属性,一个是图片,一个是图片的时间
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 options:nil];
if (!image) {
continue;
}
// 获取每个图片的时间
NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
// 生成SDImageFrame进行保存
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
[frames addObject:frame];
}
// 循环次数
NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
// 有一个比较重要的地方,把数组组成的SDImageFrame生成一个UIImage
animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
animatedImage.sd_imageLoopCount = loopCount;
}
animatedImage.sd_imageFormat = self.class.imageFormat;
CFRelease(source);
return animatedImage;
}
上面处理gif中, 有3个方法比较重要, 1:解析gif生成单帧的UIImage对象;2:获取此图片的持续时间; 3:把单帧图+持续时间,结合起来生成一个新的UIImage对象
// 遍历生成图片
UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
// 获取每个图片的时间
NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
// 生成UIImage
animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
依次来看下第一个,createFrameAtIndex: 这个方法很虚,删删减减后, 核心方法只有一个: CGImageSourceCreateImageAtIndex获取到CGImageRef.
CGImageRef抽象了图像的基本数据和元数据,创建的时候会通过CGImageSourceRef去读取图像的基础数据和元数据,但没有读取图像的其他数据,没有做图片解码的动作。
+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options {
CGImageRef imageRef;
if (createFullImage) {
// 本方法的核心,CGImageRef抽象了图像的基本数据和元数据,
// 创建的时候会通过CGImageSourceRef去读取图像的基础数据和元数据,但没有读取图像的其他数据,没有做图片解码的动作。
imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
}
// 处理了图片的方向
UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation];
CGImageRelease(imageRef);
return image;
}
第二个方法很简单, 就是通过CGImageSourceCopyPropertiesAtIndex获取到第i张图片的属性, 然后取出图片的时长, 同时做了一个额外的处理, 每张图片的最低显示时长为0.1s
+ (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
NSTimeInterval frameDuration = 0.1;
// 获取图片原始的数据
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
if (!cfFrameProperties) {
return frameDuration;
}
NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
NSDictionary *containerProperties = frameProperties[self.dictionaryProperty];
// 获取对应图片的时长
NSNumber *delayTimeUnclampedProp = containerProperties[self.unclampedDelayTimeProperty];
if (delayTimeUnclampedProp != nil) {
frameDuration = [delayTimeUnclampedProp doubleValue];
}
if (frameDuration < 0.011) {
frameDuration = 0.1;
}
CFRelease(cfFrameProperties);
return frameDuration;
}
好吧,到第三个方法了,这个方法就比较有意思了.
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
NSUInteger frameCount = frames.count;
if (frameCount == 0) {
return nil;
}
UIImage *animatedImage;
// 把时间换成毫秒放到数组中,准备求数组的最大公约数,
// 原始时间是0.1s,0.2s这样的,不方便求最大公约数
NSUInteger durations[frameCount];
for (size_t i = 0; i < frameCount; i++) {
durations[i] = frames[i].duration * 1000;
}
// 求出了数组的最大公约数gcd
NSUInteger const gcd = gcdArray(frameCount, durations);
__block NSUInteger totalDuration = 0;
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
// 遍历frame
[frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
// 获取到每帧图片
UIImage *image = frame.image;
// 计算gif的总时长
NSUInteger duration = frame.duration * 1000;
totalDuration += duration;
// 计算每个图片的重复次数
// 比如这个gif有3张图,每张显示0.1s,0.2s,0.5s,那么安装上述方法乘1000后,最大公约数就是100,
// 这就意味着第一张图加入到animatedImages中1次,第二张图加入2次,第三张图加入5次.
// 为什么这么做? 因为系统的方法是按照图片的张数平分展示时间
// + (nullable UIImage *)animatedImageWithImages:(NSArray<UIImage *> *)images duration:(NSTimeInterval)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];
return animatedImage;
}
最大公约数的计算, 用了数学上的辗转相除法求了这个数组中的最大公约数.
// 求数组中的所有项的最大公约数
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;
}
// 求2个单独数字的最大公约数, 辗转相除法
static NSUInteger gcd(NSUInteger a, NSUInteger b) {
NSUInteger c;
while (a != 0) {
c = a;
a = b % a;
b = c;
}
return b;
}
最后调到了系统的方法 animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f]; 官方文档上很简略, UIImageView本身就支持可动画的image对象, 而animatedImageWithImages:duration:会根据数组生成这个可动画的对象,这样显示就完全交给了系统,至于解码,保存数据,我们就不需要操心那么多了.
animatedImageWithImages:duration:
Creates and returns an animated image from an existing set of images.
创建并返回一个可动画的image对象, 从一个image的集合中
UIImageView
An object that displays a single image or a sequence of animated images in your interface.
一个对象,可以展示单个image或者一系列可动画的images在用户界面上.
到此,SD加载gif就完成了.
我们也可以使用系统的方法加载gif, 只不过gif的时间是我随便填的,只是看看效果而已.
#pragma mark UIImage加载image
- (void)testGifImage6 {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.frame];
[self.view addSubview:imageView];
// 只能处理单个图片,而且单图1.2M,整个下载下来需要1.2*50=60M, 而生成的gif才2.1M
// 所以这种写法也就看看就行了
// CPU:0%, 内存:60M
NSMutableArray *array = [NSMutableArray array];
for (int i = 1; i<=50; i++) {
NSString *imageName = [NSString stringWithFormat:@"直播间测试gif-%@.tiff",@(i)];
UIImage *image = [UIImage imageNamed:imageName];
[array addObject:image];
}
// 方式1
// imageView.animationImages = array;
// imageView.animationDuration = 3;
// [imageView startAnimating];
// 方式2
imageView.image = [UIImage animatedImageWithImages:array duration:3];
NSLog(@"UIImage animatedImageWithImages动画加载");
}