一、前言
上一章节主要讲解了图片的简单加载、内存/磁盘缓存等内容,但目前该框架还不支持GIF图片的加载。而GIF图片在我们日常开发中是非常常见的。因此,本章节将着手实现对GIF图片的加载。
二、加载GIF图片
1. 加载本地GIF图片
UIImageView
本身是支持对GIF图片的加载的,将GIF图片加入到animationImages
属性中,并通过startAnimating
和stopAnimating
来启动/停止动画。
UIImageView* animatedImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
animatedImageView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:@"image1.gif"],
[UIImage imageNamed:@"image2.gif"],
[UIImage imageNamed:@"image3.gif"],
[UIImage imageNamed:@"image4.gif"], nil];
animatedImageView.animationDuration = 1.0f;
animatedImageView.animationRepeatCount = 0;
[animatedImageView startAnimating];
[self.view addSubview: animatedImageView];
复制代码
2.加载网络GIF图片
与本地加载的不同之处在于我们通过网络获取到的是
NSData
类型,如果只是简单地通过initImageWithData:
方法转化为image,那么往往只能获取到GIF中的第一张图片。我们知道GIF图片其实就是由于多张图片组合而成。因此,我们这里最重要是如何从NSData
中解析转化为images
。
JImageCoder
:我们定义一个类转化用于图像的解析
@interface JImageCoder : NSObject
+ (instancetype)shareCoder;
- (UIImage *)decodeImageWithData:(NSData *)data;
@end
复制代码
我们知道通过网络请求下载之后返回的是NSData
数据
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:URL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//do something: data->image
}];
复制代码
对于PNG和JPEG格式我们可以直接使用initImageWithData
方法来转化为image
,但对于GIF图片,我们则需要特殊处理。那么处理之前,我们就需要根据NSData
来判断图片对应的格式。
- 根据
NSData
数据判断图片格式:这里参考了SDWebImage
的实现,根据数据的第一个字节来判断。
- (JImageFormat)imageFormatWithData:(NSData *)data {
if (!data) {
return JImageFormatUndefined;
}
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return JImageFormatJPEG;
case 0x89:
return JImageFormatPNG;
case 0x47:
return JImageFormatGIF;
default:
return JImageFormatUndefined;
}
}
复制代码
获取到图片的格式之后,我们就可以根据不同的格式来分别进行处理
- (UIImage *)decodeImageWithData:(NSData *)data {
JImageFormat format = [self imageFormatWithData:data];
switch (format) {
case JImageFormatJPEG:
case JImageFormatPNG:{
UIImage *image = [[UIImage alloc] initWithData:data];
image.imageFormat = format;
return image;
}
case JImageFormatGIF:
return [self decodeGIFWithData:data];
default:
return nil;
}
}
复制代码
针对GIF图片中的每张图片的获取,我们可以使用ImageIO
中的相关方法来提取。要注意的是对于一些对象,使用完之后要及时释放,否则会造成内存泄漏。
- (UIImage *)decodeGIFWithData:(NSData *)data {
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!source) {
return nil;
}
size_t count = CGImageSourceGetCount(source);
UIImage *animatedImage;
if (count <= 1) {
animatedImage = [[UIImage alloc] initWithData:data];
animatedImage.imageFormat = JImageFormatGIF;
} else {
NSMutableArray<UIImage *> *imageArray = [NSMutableArray array];
for (size_t i = 0; i < count; i ++) {
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
if (!imageRef) {
continue;
}
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
[imageArray addObject:image];
CGImageRelease(imageRef);
}
animatedImage = [[UIImage alloc] init];
animatedImage.imageFormat = JImageFormatGIF;
animatedImage.images = [imageArray copy];
}
CFRelease(source);
return animatedImage;
}
复制代码
为了使得UIImage
对象可以存储图片的格式和GIF中的images
,这里实现了一个UIImage
的分类
typedef NS_ENUM(NSInteger, JImageFormat) {
JImageFormatUndefined = -1,
JImageFormatJPEG = 0,
JImageFormatPNG = 1,
JImageFormatGIF = 2
};
@interface UIImage (JImageFormat)
@property (nonatomic, assign) JImageFormat imageFormat;
@property (nonatomic, copy) NSArray *images;
@end
@implementation UIImage (JImageFormat)
- (void)setImages:(NSArray *)images {
objc_setAssociatedObject(self, @selector(images), images, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSArray *)images {
NSArray *images = objc_getAssociatedObject(self, @selector(images));
if ([images isKindOfClass:[NSArray class]]) {
return images;
}
return nil;
}
- (void)setImageFormat:(JImageFormat)imageFormat {
objc_setAssociatedObject(self, @selector(imageFormat), @(imageFormat), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (JImageFormat)imageFormat {
JImageFormat imageFormat = JImageFormatUndefined;
NSNumber *value = objc_getAssociatedObject(self, @selector(imageFormat));
if ([value isKindOfClass:[NSNumber class]]) {
imageFormat = value.integerValue;
return imageFormat;
}
return imageFormat;
}
@end
复制代码
使用JImageCoder
将NSData
类型的数据解析为images
之后,便可以像本地加载GIF一样使用了。
static NSString *gifUrl = @"https://user-gold-cdn.xitu.io/2019/3/27/169bce612ee4dc21";
- (void)downloadImage {
__weak typeof(self) weakSelf = self;
[[JImageDownloader shareInstance] fetchImageWithURL:gifUrl completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
__strong typeof (weakSelf) strongSelf = weakSelf;
if (strongSelf && image) {
if (image.imageFormat == JImageFormatGIF) {
strongSelf.imageView.animationImages = image.images;
[strongSelf.imageView startAnimating];
} else {
strongSelf.imageView.image = image;
}
}
}];
}
复制代码
3.实现效果
和YYAnimatedImage
和FLAnimatedImage
分别进行了对比,会发现自定义框架加载的GIF播放会更快些。我们回到UIImageView
的GIF本地加载中,会发现遗漏了两个重要的属性:
@property (nonatomic) NSTimeInterval animationDuration; // for one cycle of images. default is number of images * 1/30th of a second (i.e. 30 fps)
@property (nonatomic) NSInteger animationRepeatCount; // 0 means infinite (default is 0)
复制代码
animatedDuration
定义了动画的周期,由于我们没有给它设置GIF的周期,所以这里使用的默认周期。接下来我们将回到GIF图片的解析过程中,增加这两个相关属性。
4.GIF的animationDuration
和animationRepeatCount
属性
animationRepeatCount
:动画执行的次数
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
....
NSInteger loopCount = 0;
CFDictionaryRef properties = CGImageSourceCopyProperties(source, NULL);
if (properties) {
CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
if (gif) {
CFTypeRef loop = CFDictionaryGetValue(gif, kCGImagePropertyGIFLoopCount);
if (loop) {
CFNumberGetValue(loop, kCFNumberNSIntegerType, &loopCount);
}
}
CFRelease(properties); //注意使用完需要释放
}
复制代码
animationDuration
:动画执行周期
我们分别获取到GIF中每张图片对应的delayTime(显示时间),最后求它们的和,便可以作为GIF动画的一个完整周期。而图片的delayTime可以通过
ImageSource
中的kCGImagePropertyGIFUnclampedDelayTime
或kCGImagePropertyGIFDelayTime
属性获取。
NSTimeInterval duration = 0;
for (size_t i = 0; i < count; i ++) {
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
if (!imageRef) {
continue;
}
....
float delayTime = kJAnimatedImageDefaultDelayTimeInterval;
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(source, i, NULL);
if (properties) {
CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
if (gif) {
CFTypeRef value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
if (!value) {
value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
}
if (value) {
CFNumberGetValue(value, kCFNumberFloatType, &delayTime);
}
}
CFRelease(properties);
}
duration += delayTime;
}
复制代码
获取之后加入到UIImage属性中:
animatedImage = [[UIImage alloc] init];
animatedImage.imageFormat = JImageFormatGIF;
animatedImage.images = [imageArray copy];
animatedImage.loopCount = loopCount;
animatedImage.totalTimes = duration;
- (void)downloadImage {
__weak typeof(self) weakSelf = self;
[[JImageDownloader shareInstance] fetchImageWithURL:gifUrl completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
__strong typeof (weakSelf) strongSelf = weakSelf;
if (strongSelf && image) {
if (image.imageFormat == JImageFormatGIF) {
strongSelf.imageView.animationImages = image.images;
strongSelf.imageView.animationDuration = image.totalTimes;
strongSelf.imageView.animationRepeatCount = image.loopCount;
[strongSelf.imageView startAnimating];
} else {
strongSelf.imageView.image = image;
}
}
}];
}
复制代码
实现效果如下:
发现通过设置动画周期和次数之后,动画加载的更快了!!!为了解决这个问题,重新阅读了YYAnimatedImage
和FLAnimatedImage
的源码,发现它们在获取GIF图片的delayTime时,都会有一个小小的细节。
FLAnimatedImage.m
const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum = 0.02;
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
// Support frame delays as low as `kFLAnimatedImageDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility.
// To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO.
if ([delayTime floatValue] < ((float)kFLAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) {
FLLog(FLLogLevelInfo, @"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kFLAnimatedImageDelayTimeIntervalMinimum);
delayTime = @(kDelayTimeIntervalDefault);
}
UIImage+YYWebImage.m
static NSTimeInterval _yy_CGImageSourceGetGIFFrameDelayAtIndex(CGImageSourceRef source, size_t index) {
NSTimeInterval delay = 0;
CFDictionaryRef dic = CGImageSourceCopyPropertiesAtIndex(source, index, NULL);
if (dic) {
CFDictionaryRef dicGIF = CFDictionaryGetValue(dic, kCGImagePropertyGIFDictionary);
if (dicGIF) {
NSNumber *num = CFDictionaryGetValue(dicGIF, kCGImagePropertyGIFUnclampedDelayTime);
if (num.doubleValue <= __FLT_EPSILON__) {
num = CFDictionaryGetValue(dicGIF, kCGImagePropertyGIFDelayTime);
}
delay = num.doubleValue;
}
CFRelease(dic);
}
// http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility
if (delay < 0.02) delay = 0.1;
return delay;
}
复制代码
如上所示,YYAnimatedImage
和FLAnimatedImage
对于delayTime小于0.02的情况下,都会设置为默认值0.1。这么处理的主要目的是为了更好兼容更低级的设备,具体可以查看这里。
static const NSTimeInterval kJAnimatedImageDelayTimeIntervalMinimum = 0.02;
static const NSTimeInterval kJAnimatedImageDefaultDelayTimeInterval = 0.1;
float delayTime = kJAnimatedImageDefaultDelayTimeInterval;
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(source, i, NULL);
if (properties) {
CFDictionaryRef gif = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
if (gif) {
CFTypeRef value = CFDictionaryGetValue(gif, kCGImagePropertyGIFUnclampedDelayTime);
if (!value) {
value = CFDictionaryGetValue(gif, kCGImagePropertyGIFDelayTime);
}
if (value) {
CFNumberGetValue(value, kCFNumberFloatType, &delayTime);
if (delayTime < ((float)kJAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) {
delayTime = kJAnimatedImageDefaultDelayTimeInterval;
}
}
}
CFRelease(properties);
}
duration += delayTime;
复制代码
为了让动画效果更接近YYAnimatedImage
和FLAnimatedImage
,我们同样在获取delayTime时增加条件判断。具体效果如下:
三、总结
本小节主要实现了图片框架对GIF图片的加载功能。重点主要集中在通过
ImageIO
中的相关方法来获取到GIF中的每张图片,以及图片对应的周期和执行次数等。在最后结尾处也提及到了在获取图片delayTime时的一个小细节。通过这个细节也可以体现出自己动手打造框架的好处,因为如果只是简单地去阅读相关源码,往往很容易忽略很多细节。