上一章節主要講解了圖片的簡單加載、內存/磁盤緩存等內容,但目前該框架還不支持GIF圖片的加載。而GIF圖片在咱們平常開發中是很是常見的。所以,本章節將着手實現對GIF圖片的加載。git
UIImageView
自己是支持對GIF圖片的加載的,將GIF圖片加入到animationImages
屬性中,並經過startAnimating
和stopAnimating
來啓動/中止動畫。github
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];
複製代碼
與本地加載的不一樣之處在於咱們經過網絡獲取到的是
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
來判斷圖片對應的格式。session
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
中的相關方法來提取。要注意的是對於一些對象,使用完以後要及時釋放,不然會形成內存泄漏。oop
- (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
的分類post
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同樣使用了。fetch
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;
}
}
}];
}
複製代碼
和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圖片的解析過程當中,增長這兩個相關屬性。
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時的一個小細節。經過這個細節也能夠體現出本身動手打造框架的好處,由於若是隻是簡單地去閱讀相關源碼,每每很容易忽略不少細節。