SDWebImage 源码解读之 UIImage+GIF

3,329 阅读3分钟
原文链接: valie.space

iOS不支持播放GIF格式的动图,但是我们可以通过ImageIO框架来实现它。
先介绍一下GIF的几个概念:

  • frame(帧):一个GIF可以简单认为是多张图片组成的动画,一帧就是其中一张图片;
  • frameCount(帧数):表示GIF有多少帧;
  • loopCount(播放次数):有些GIF播放到一定次数就停止了,如果为0就代表GIF一直循环播放;
  • delayTime(延迟时间):每一帧播放的时间。

实现方法
通过ImageIO的CGImageSourceRef读取GIF图片数据,将每一帧图片及对应的播放时间解析出来,最后通过以下方法生成一张UIImage,从而实现GIF动态图的播放。

 + (UIImage *)animatedImageWithImages:(NSArray<UIImage *> *)images duration:(NSTimeInterval)duration 

1.源码解析之获取每一帧的播放时间(delayTime)
参数:

  • index:每一帧所对应的下标
  • source:通过CGImageSourceRef获取图片数据
+ (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
    float frameDuration = 0.1f;

    ①通过CGImageSourceCopyPropertiesAtIndex方法获取到该帧图片的属性字典;
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);

    ②通过__bridge将Core Foundation中(以下简称为CF)的CFDictionaryRef类型转为Foundation中对应的NSDictionary类型;
    NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;

    ③获取该帧图片中的GIF相关的属性字典(key=kCGImagePropertyGIFDictionary)
    NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];

    ④获取该帧图片的播放时间(key=kCGImagePropertyGIFUnclampedDelayTime);
    NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
    if (delayTimeUnclampedProp) {
        frameDuration = [delayTimeUnclampedProp floatValue];
    }
    else {

        ⑤如果通过kCGImagePropertyGIFUnclampedDelayTime没有获取到播放时长,就通过kCGImagePropertyGIFDelayTime来获取,两者的含义是相同的;
        NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
        if (delayTimeProp) {
            frameDuration = [delayTimeProp floatValue];
        }
    }

    ⑥对于播放时间低于0.011s的,重新指定时长为0.100s;
    if (frameDuration < 0.011f) {
        frameDuration = 0.100f;
    }

    ⑦CF对象和OC对象进行相互转化,在ARC环境下,编译器不会自动管理CF对象的内存,因此,创建一个CF对象以后,我们需要使用CFRelease将其手动释放。
    CFRelease(cfFrameProperties);
    return frameDuration;
}

2.源码解析之由图片数据(data)生成UIImage动图
参数:

  • data:图片数据
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
    if (!data) {
        return nil;
    }

    ①使用__bridge将Foundation对象NSData转为对应的Core Foundation对象CFDataRef,再通过CGImageSourceCreateWithData生成CGImageSourceRef用以获取图片数据;
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

    ②从source中获取帧数(使用size_t是考虑到可移植性和程序效率,可参考http://jeremybai.github.io/blog/2014/09/10/size-t);
    size_t count = CGImageSourceGetCount(source);

    UIImage *animatedImage;

    ③处理帧数<=1的情况
    if (count <= 1) {
        animatedImage = [[UIImage alloc] initWithData:data];
    }

    ④处理帧数大于1的情况
    else {
        NSMutableArray *images = [NSMutableArray array];

        NSTimeInterval duration = 0.0f;
        for (size_t i = 0; i < count; i++) {

            ④ a.取出索引对应的图片
            CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
            if (!image) {
                continue;
            }

            ④ b.将该帧的播放时间累加到总时间duration中;
            duration += [self sd_frameDurationAtIndex:i source:source];

            ④ c.由CGImageRef生成UIImage,并存入图片数组中;
            [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];

            ④ d.手动释放CF的CGImageRef对象
            CGImageRelease(image);
        }

        if (!duration) {
            duration = (1.0f / 10.0f) * count;
        }

        ④ e.由图片数组生成可播放的UIImage动图
        animatedImage = [UIImage animatedImageWithImages:images duration:duration];
    }

    ⑤手动释放CF的CGImageSourceRef对象
    CFRelease(source);

    return animatedImage;
}

3.源码解析之绘制指定size的图片(重绘图片的宽高比例保持不变)

参数:

  • size:要求的图片大小
- (UIImage *)sd_animatedImageByScalingAndCroppingToSize:(CGSize)size {

    ①如果size和原图大小相等或者size为零,则返回原图;
    if (CGSizeEqualToSize(self.size, size) || CGSizeEqualToSize(size, CGSizeZero)) {
        return self;
    }

    ②设置缩放图片的size初始值为原图大小;
    CGSize scaledSize = size;

    ③设置绘制的起点为(0,0);
    CGPoint thumbnailPoint = CGPointZero;

    ④计算宽高各自的缩放比例,选择比例较大的一个作为缩放比例;
    CGFloat widthFactor = size.width / self.size.width;
    CGFloat heightFactor = size.height / self.size.height;
    CGFloat scaleFactor = (widthFactor > heightFactor) ? widthFactor : heightFactor;

    ⑤根据缩放比重新计算缩放图片的size
    scaledSize.width = self.size.width * scaleFactor;
    scaledSize.height = self.size.height * scaleFactor;

    ⑥重新计算绘制的起点,以缩放图片的中心为中心进行裁剪
    if (widthFactor > heightFactor) {
        thumbnailPoint.y = (size.height - scaledSize.height) * 0.5;
    }
    else if (widthFactor < heightFactor) {
        thumbnailPoint.x = (size.width - scaledSize.width) * 0.5;
    }

    NSMutableArray *scaledImages = [NSMutableArray array];

    ⑦使用UIGraphics对image中的每张图片进行绘制
    for (UIImage *image in self.images) {
        UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);

        [image drawInRect:CGRectMake(thumbnailPoint.x, thumbnailPoint.y, scaledSize.width, scaledSize.height)];
        UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

        [scaledImages addObject:newImage];

        UIGraphicsEndImageContext();
    }

    ⑧根据图片数组生成UIImage图片
    return [UIImage animatedImageWithImages:scaledImages duration:self.duration];
}