从零开始打造一个iOS图片加载框架(二)

1、前言

上一章节主要讲解了图片的简单加载、内存/磁盘缓存等内容,但目前该框架还不支持GIF图片的加载。而GIF图片在咱们平常开发中是很是常见的。所以,本章节将着手实现对GIF图片的加载。git

2、加载GIF图片

1. 加载本地GIF图片

UIImageView自己是支持对GIF图片的加载的,将GIF图片加入到animationImages属性中,并经过startAnimatingstopAnimating来启动/中止动画。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];
复制代码

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来判断图片对应的格式。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
复制代码

使用JImageCoderNSData类型的数据解析为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;
            }
        }
    }];
}
复制代码

3.实现效果

YYAnimatedImageFLAnimatedImage分别进行了对比,会发现自定义框架加载的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的animationDurationanimationRepeatCount属性

  • 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中的kCGImagePropertyGIFUnclampedDelayTimekCGImagePropertyGIFDelayTime属性获取。

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;
            }
        }
    }];
}
复制代码

实现效果以下:

设置周期前 设置周期后

发现经过设置动画周期和次数以后,动画加载的更快了!!!为了解决这个问题,从新阅读了YYAnimatedImageFLAnimatedImage的源码,发现它们在获取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;
}
复制代码

如上所示,YYAnimatedImageFLAnimatedImage对于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;
复制代码

为了让动画效果更接近YYAnimatedImageFLAnimatedImage,咱们一样在获取delayTime时增长条件判断。具体效果以下:

动画三

3、总结

本小节主要实现了图片框架对GIF图片的加载功能。重点主要集中在经过ImageIO中的相关方法来获取到GIF中的每张图片,以及图片对应的周期和执行次数等。在最后结尾处也说起到了在获取图片delayTime时的一个小细节。经过这个细节也能够体现出本身动手打造框架的好处,由于若是只是简单地去阅读相关源码,每每很容易忽略不少细节。

参考资料

相关文章
相关标签/搜索