博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从零开始打造一个iOS图片加载框架(二)
阅读量:6159 次
发布时间:2019-06-21

本文共 11539 字,大约阅读时间需要 38 分钟。

一、前言

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

二、加载GIF图片

1. 加载本地GIF图片

UIImageView本身是支持对GIF图片的加载的,将GIF图片加入到animationImages属性中,并通过startAnimatingstopAnimating来启动/停止动画。

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
*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复制代码

使用JImageCoderNSData类型的数据解析为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.实现效果

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.mconst 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.mstatic 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时增加条件判断。具体效果如下:

三、总结

本小节主要实现了图片框架对GIF图片的加载功能。重点主要集中在通过ImageIO中的相关方法来获取到GIF中的每张图片,以及图片对应的周期和执行次数等。在最后结尾处也提及到了在获取图片delayTime时的一个小细节。通过这个细节也可以体现出自己动手打造框架的好处,因为如果只是简单地去阅读相关源码,往往很容易忽略很多细节。

参考资料

转载于:https://juejin.im/post/5c9b3e3d518825303c705dd2

你可能感兴趣的文章
Silverlight 如何手动打包xap
查看>>
禁用ViewState
查看>>
Android图片压缩(质量压缩和尺寸压缩)
查看>>
nilfs (a continuent snapshot file system) used with PostgreSQL
查看>>
【SICP练习】150 练习4.6
查看>>
HTTP缓存应用
查看>>
KubeEdge向左,K3S向右
查看>>
DTCC2013:基于网络监听数据库安全审计
查看>>
CCNA考试要点大搜集(二)
查看>>
ajax查询数据库时数据无法更新的问题
查看>>
Kickstart 无人职守安装,终于搞定了。
查看>>
linux开源万岁
查看>>
linux/CentOS6忘记root密码解决办法
查看>>
25个常用的Linux iptables规则
查看>>
集中管理系统--puppet
查看>>
Exchange 2013 PowerShell配置文件
查看>>
JavaAPI详解系列(1):String类(1)
查看>>
HTML条件注释判断IE<!--[if IE]><!--[if lt IE 9]>
查看>>
发布和逸出-构造过程中使this引用逸出
查看>>
使用SanLock建立简单的HA服务
查看>>