SDWebImage加载gif动态图原理

前面有篇文章分析了不同的框架加载gif的性能https://blog.csdn.net/u014600626/article/details/113767876。SD的性能是不错的,CPU占用很低, 而且这个框架很常见, 就想研究下SDWebImage是怎么加载gif图片的。

先看调用的地方, 就是很简单的使用 [imageView sd_setImageWithURL:url](之前的版本可能不支持直接传gif的地址,但是在5.x.x之后肯定是可以了,本文基于5.10.4分析);  或者使用工程中/沙盒里 获取到的二进制数据即可

#pragma mark SD加载image
- (void)testGifImage5 {

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.frame];

    // 网络图
    NSURL *url = [NSURL URLWithString:@"http://res.hongrenshuo.com.cn/66532a15-c726-4edf-bb4f-0b8897753f31.gif?t=1606124887942"];
    [imageView sd_setImageWithURL:url];


    // 本地图
//    NSString * path = [[NSBundle mainBundle] pathForResource:@"直播间测试gif" ofType:@"gif"];
//    NSData * data = [NSData dataWithContentsOfFile:path];
//    UIImage *image = [UIImage sd_imageWithGIFData:data];
//    imageView.image = image;
    [self.view addSubview:imageView];
    NSLog(@"SD加载image");

}

现在开始。

1,首先我们看下SDWebImage是怎么加载gif的。

对外暴露的方法只有一个,传入gif的二进制数据, 返回生成的UIImage对象,

由于传入的二进制数据, 这个gif可以是在工程中的, 也可以是网络上的图片下载到沙盒,然后读取出来传入给此方法.

+ (nullable UIImage *)sd_imageWithGIFData:(nullable NSData *)data {
    if (!data) {
        return nil;
    }
    return [[SDImageGIFCoder sharedCoder] decodedImageWithData:data options:0];
}

2.此方法调用到了SDImageGIFCoder, 但是SDImageGIFCoder中没有实现此方法, 只好到父类中寻找, SDImageGIFCoder的父类是SDImageIOAnimatedCoder, 看下父类中的实现.

看下面方法之前先看下这个东西, 有助于下面的理解: 

CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); // 解码二进制数据,获取到图片的原始信息

CGImageSourceRef定义如下,typedef struct CGImageSource *CGImageSourceRef;

可以看到它是一个CGImageSource 指针。
CGImageSource又是什么呢?
CGImageSource是对图像数据读取任务的抽象,通过它可以获得图像对象、缩略图、图像的属性(包括Exif信息)。

  • 解码图片原始数据,判断静态图还是gif,走进不同的处理方法中
  • 动态图添加到数组中, 然后统一返回一个UIImage
// 解码二进制数据,生成UIImage对象
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
    if (!data) {
        return nil;
    }
    CGFloat scale = 1;
    NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
    if (scaleFactor != nil) {
        scale = MAX([scaleFactor doubleValue], 1);
    }
    
    CGSize thumbnailSize = CGSizeZero;
    NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
    if (thumbnailSizeValue != nil) {
        thumbnailSize = thumbnailSizeValue.CGSizeValue;
    }
    
    BOOL preserveAspectRatio = YES;
    NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
    if (preserveAspectRatioValue != nil) {
        preserveAspectRatio = preserveAspectRatioValue.boolValue;
    }
    // 调用系统方法生成ImageSource,这里面有图片的原始信息
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    if (!source) {
        return nil;
    }
    // 读取CGImageSourceRef有几个图片对象。判断是不是gif
    size_t count = CGImageSourceGetCount(source);
    UIImage *animatedImage;

    BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
    // 只需解析第一张图 || 静态图
    if (decodeFirstFrame || count <= 1) {
        animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
    } else {
        // 动态图,用一个数组保存每帧的信息,SDImageFrame只有2个属性,一个是图片,一个是图片的时间
        NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
        
        for (size_t i = 0; i < count; i++) {
            // 遍历生成图片
            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
            if (!image) {
                continue;
            }
            // 获取每个图片的时间
            NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
            // 生成SDImageFrame进行保存
            SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
            [frames addObject:frame];
        }
        // 循环次数
        NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
        // 有一个比较重要的地方,把数组组成的SDImageFrame生成一个UIImage
        animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
        animatedImage.sd_imageLoopCount = loopCount;
    }
    animatedImage.sd_imageFormat = self.class.imageFormat;
    CFRelease(source);
    
    return animatedImage;
}

上面处理gif中,  有3个方法比较重要, 1:解析gif生成单帧的UIImage对象;2:获取此图片的持续时间; 3:把单帧图+持续时间,结合起来生成一个新的UIImage对象

            // 遍历生成图片
            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
            // 获取每个图片的时间
            NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];

        // 生成UIImage
        animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];

依次来看下第一个,createFrameAtIndex: 这个方法很虚,删删减减后, 核心方法只有一个: CGImageSourceCreateImageAtIndex获取到CGImageRef.

CGImageRef抽象了图像的基本数据和元数据,创建的时候会通过CGImageSourceRef去读取图像的基础数据和元数据,但没有读取图像的其他数据,没有做图片解码的动作。

+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options {
    CGImageRef imageRef;
    if (createFullImage) {
        // 本方法的核心,CGImageRef抽象了图像的基本数据和元数据,
        // 创建的时候会通过CGImageSourceRef去读取图像的基础数据和元数据,但没有读取图像的其他数据,没有做图片解码的动作。
        imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
    } 
    // 处理了图片的方向
    UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
    UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation];
    CGImageRelease(imageRef);
    return image;
}

第二个方法很简单, 就是通过CGImageSourceCopyPropertiesAtIndex获取到第i张图片的属性, 然后取出图片的时长, 同时做了一个额外的处理, 每张图片的最低显示时长为0.1s

+ (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
    NSTimeInterval frameDuration = 0.1;
    // 获取图片原始的数据
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
    if (!cfFrameProperties) {
        return frameDuration;
    }
    NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
    NSDictionary *containerProperties = frameProperties[self.dictionaryProperty];
    // 获取对应图片的时长
    NSNumber *delayTimeUnclampedProp = containerProperties[self.unclampedDelayTimeProperty];
    if (delayTimeUnclampedProp != nil) {
        frameDuration = [delayTimeUnclampedProp doubleValue];
    } 
    if (frameDuration < 0.011) {
        frameDuration = 0.1;
    }    
    CFRelease(cfFrameProperties);
    return frameDuration;
}

好吧,到第三个方法了,这个方法就比较有意思了. 

  • 使用辗转相除法获取数组的最大公约数
  • 根据最大公约数添加对应张数的图片到数组中, 计算整个gif总时长
  • 为什么要求最大公约数? 因为系统的方法是按照图片的张数平分展示时间
  • 比如这个gif有3张图,分别显示0.1s,0.2s,0.5s,那么安装上述方法乘1000后,最大公约数就是100,这就意味着第一张图加入到animatedImages中1次,第二张图加入2次,第三张图加入5次.这样整个gif的时长还是0.8s,被这8张图平分,每种图的时间刚好就是gif的原始时长.
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
    NSUInteger frameCount = frames.count;
    if (frameCount == 0) {
        return nil;
    }
    
    UIImage *animatedImage;
    // 把时间换成毫秒放到数组中,准备求数组的最大公约数,
    // 原始时间是0.1s,0.2s这样的,不方便求最大公约数
    NSUInteger durations[frameCount];
    for (size_t i = 0; i < frameCount; i++) {
        durations[i] = frames[i].duration * 1000;
    }
    // 求出了数组的最大公约数gcd
    NSUInteger const gcd = gcdArray(frameCount, durations);
    __block NSUInteger totalDuration = 0;
    NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
    // 遍历frame
    [frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
        // 获取到每帧图片
        UIImage *image = frame.image;
        // 计算gif的总时长
        NSUInteger duration = frame.duration * 1000;
        totalDuration += duration;
        // 计算每个图片的重复次数
        // 比如这个gif有3张图,每张显示0.1s,0.2s,0.5s,那么安装上述方法乘1000后,最大公约数就是100,
        // 这就意味着第一张图加入到animatedImages中1次,第二张图加入2次,第三张图加入5次.
        // 为什么这么做? 因为系统的方法是按照图片的张数平分展示时间
        // + (nullable UIImage *)animatedImageWithImages:(NSArray<UIImage *> *)images duration:(NSTimeInterval)duration
        NSUInteger repeatCount;
        if (gcd) {
            repeatCount = duration / gcd;
        } else {
            repeatCount = 1;
        }
        for (size_t i = 0; i < repeatCount; ++i) {
            [animatedImages addObject:image];
        }
    }];
    
    animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];
    return animatedImage;
}

最大公约数的计算, 用了数学上的辗转相除法求了这个数组中的最大公约数.

// 求数组中的所有项的最大公约数
static NSUInteger gcdArray(size_t const count, NSUInteger const * const values) {
    if (count == 0) {
        return 0;
    }
    NSUInteger result = values[0];
    for (size_t i = 1; i < count; ++i) {
        // 依次求最大公约数
        result = gcd(values[i], result);
    }
    return result;
}
// 求2个单独数字的最大公约数, 辗转相除法
static NSUInteger gcd(NSUInteger a, NSUInteger b) {
    NSUInteger c;
    while (a != 0) {
        c = a;
        a = b % a;
        b = c;
    }
    return b;
}

最后调到了系统的方法  animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f]; 官方文档上很简略, UIImageView本身就支持可动画的image对象, 而animatedImageWithImages:duration:会根据数组生成这个可动画的对象,这样显示就完全交给了系统,至于解码,保存数据,我们就不需要操心那么多了.

animatedImageWithImages:duration:

Creates and returns an animated image from an existing set of images.

创建并返回一个可动画的image对象, 从一个image的集合中

UIImageView

An object that displays a single image or a sequence of animated images in your interface.

一个对象,可以展示单个image或者一系列可动画的images在用户界面上.

到此,SD加载gif就完成了.

我们也可以使用系统的方法加载gif, 只不过gif的时间是我随便填的,只是看看效果而已.

#pragma mark UIImage加载image
- (void)testGifImage6 {

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.frame];
    [self.view addSubview:imageView];

    // 只能处理单个图片,而且单图1.2M,整个下载下来需要1.2*50=60M, 而生成的gif才2.1M
    // 所以这种写法也就看看就行了
    // CPU:0%, 内存:60M
    NSMutableArray *array = [NSMutableArray array];
    for (int i = 1; i<=50; i++) {
        NSString *imageName = [NSString stringWithFormat:@"直播间测试gif-%@.tiff",@(i)];
        UIImage *image = [UIImage imageNamed:imageName];
        [array addObject:image];
    }
    // 方式1
//    imageView.animationImages = array;
//    imageView.animationDuration = 3;
//    [imageView startAnimating];

    // 方式2
    imageView.image = [UIImage animatedImageWithImages:array duration:3];
    NSLog(@"UIImage animatedImageWithImages动画加载");

}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Cesium是一个用于构建三维地球、地图和应用程序的开源JavaScript库。它为开发者提供了丰富的功能和工具,使他们能够创建高度交互性和可视化效果的地球应用程序。 要在Cesium中加载和显示GIF图像,可以使用Cesium的ImageryLayer对象。ImageryLayer对象用于在地球上显示各种类型的图片,包括GIF图像。 要加载GIF图像,首先需要创建一个ImageryLayer对象。可以使用GIF图像的URL作为ImageryLayer对象的一个属性来指定要加载的图像。例如,假设我们有一个名为gifImageUrl的变量,其值为GIF图像的URL,可以使用以下代码创建一个ImageryLayer对象: ```javascript var imageryLayer = new Cesium.ImageryLayer(new Cesium.SingleTileImageryProvider({ url: gifImageUrl })); ``` 接下来,将ImageryLayer对象添加到Cesium的Viewer中,以便在地球上显示图像。假设我们有一个名为viewer的Cesium Viewer对象,可以使用以下代码将ImageryLayer对象添加到Viewer对象中: ```javascript viewer.scene.imageryLayers.add(imageryLayer); ``` 通过执行以上代码,Cesium将加载并显示指定的GIF图像。可以在三维地球上看到该图像。 需要注意的是,加载和显示GIF图像可能需要一些时间,具体取决于图像大小和网络连接速度。如果图像加载时间较长,可以通过调整Cesium的加载策略和优化网络连接来改善加载性能。 总之,使用Cesium的ImageryLayer对象和相应的URL,我们可以加载和显示GIF图像,并在Cesium的三维地球上进行可视化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值