SDWebImage学习之GIF加载.md 13 KB

SDWebImage GIF图加载

iOS开发中的GIF图加载

GIF是图片的一种压缩格式,其实就是将多张具有一定存活时间的静态图片压缩组合在了一起,在展示时将这些静态图片按照播放时长连贯播放就形成了动画

  • webVIew加载 wkwebview [webView loadData:gif MIMEType:@"image/gif" characterEncodingName:nilbaseURL:nil]; uiwebview [webView loadData:gif MIMEType:@"image/gif" textEncodingName:@"UTF-8" baseURL:nil];

  • UIImageView加载
    UIImageView控件并不能直接支持GIF,可以使用ImageIO.framework的API可以实现播放 获取gifData的imageSource CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)_gifData, NULL); 获取图片的帧数 size_t count = CGImageSourceGetCount(source);获取image数组 然后通过imageView.animationImages = images,最后调startAnimating即可简单实现动图效果。

这里仅讨论UIImageView加载GIF的情况。 虽然实现了通过UIImageView来播放GIF,但存在问题:

  1. images数组是准备好才开始播放的,一旦图片很多很大会造成内存急剧上升,并且造成卡顿。

    对于内存问题,想到的解决方案是获取图片每一帧,自己控制每一帧的切换,通过NSTimer或者CADisplayLink等方式实现。

    对于卡顿,猜想是图片在屏幕上显示时,因为压缩的图像数据会在渲染时被解码成其未压缩的位图形式,这是一个非常耗费CPU的操作,而且默认在主线程上做;可以考虑在子线程提前将图片解码,不过这个会带来内存的损耗,记得之前instrument查看内存时会出现很多VM: CG Raster data ,这也是之前SD库中经常出现的内存问题,因为默认开启了解压操作,下载的图片默认都解压然后缓存。decodedImageWithImage: 每一个像素点都会分配一个空间来存储相关值,那么分辨率越高的图片,就意味着更多数量的像素点,也就意味着需要分配更多的空间,内存开销极大。

  2. 播放的时间只能大概估计,不能很准确地表达出真正时长下的播放效果

    想获取真实的播放时长,必须获取GIF图中的附加信息,通过CGImageSourceCopyPropertiesAtIndex方法可以获取souceRef中指定帧(index)的属性字典,我们可以知道每一帧的一些属性 (见CGImageProperties.h)kCGImagePropertyGIFDelayTime即为播放时间。

看看浏览器之前关于GIF图片的处理

  • 首页顶部动态图片
  • 看图模式

关于GIF加载的第三方库

这里主要讨论SDWebImage库

SD各个版本对比

SDWebImage4.0 以前版本

UIImageView加载一个GIF图的url,可以显示,但是从缓存读取就是静态图片。

存储图片到磁盘

    NSData *data = imageData;            
     if (!data && image)
            SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
            data = [image sd_imageDataAsFormat:imageFormatFromData];
      }
      [self storeImageDataToDisk:data forKey:key];

由于调用者传入的参数都是SDImageFormatUndefined,所以就简化为有Alpha信息就是PNG格式数据,其他都是JPEG格式数据

SDWebImage4.x

UIImageView加载一个GIF图的url,显示的是静态的图片

加载GIF采用FLAnimatedImage

FLAnimatedImage渲染流程

FLAnimatedImage 会有两个线程同时在运转。 其中一个线程负责渲染GIF的每一帧的图片内容(所谓的渲染,大体上就是加载GIF文件数据,然后抽取出来当前需要哪一帧)。这个加载图片的过程是在异步线程进行的。然后 FLAnimatedImage 会有一个内存区域专门放置这些渲染好的帧。 这时候,在主线程中的UIImageView会根据当前需要,从这个内存区域中读取相应的帧。 这样最大限度的保证主线程不去处理图片渲染的操作,并且那个内存区域也是经过一系列的动态管理,以提升性能。 并且 FLAnimatedImage 的渲染线程,在数据没有准备好的时候,不会加锁等待,而是直接返回 nil。 这样也能避免一些实现中为了图片序列完全显示正确,而造成性能卡顿的问题。

dispatch_main_async_safe(block) 保证 block 在主线程中执行,其中包含 setImageBlock。因此 setImageBlock 在主线程中执行,也就是说 FLAnimatedImage 在主线程中生成,这一步比较耗时,阻塞主线程,造成卡顿。 解决办法是,把FLAnimatedImage的创建放到子线程中。

给FLAnimatedImageView添加扩展方法

func setImage(with url: URL?, placeholderImage: UIImage?) {
    sd_internalSetImage(with: url, placeholderImage: placeholderImage, options: SDWebImageOptions(rawValue: 0), operationKey: nil, setImageBlock: { [weak self] (image, imageData) in
        guard let strongSelf = self else { return }

        let imageFormat = NSData.sd_imageFormat(forImageData: imageData)
        if imageFormat == .GIF {
            // Enter global queue
            DispatchQueue.global(qos: .userInteractive).async { [weak self] in
                // Create FLAnimatedImage in global queue
                let animatedImage = FLAnimatedImage(animatedGIFData: imageData)
                DispatchQueue.main.async { [weak self] in
                    guard let strongSelf = self else { return }
                    // Set image in main queue
                    strongSelf.animatedImage = animatedImage
                    strongSelf.image = nil
                }
            }
        } else {
            // Set image in main queue
            strongSelf.image = image
            strongSelf.animatedImage = nil
        }
        }, progress: nil, completed: nil)
}

同样调用 sd_internalSetImageWithURL: 方法,只是修改 setImageBlock 参数,在子线程中创建 FLAnimatedImage,然后在主线程中设置图片。

注意 SD+FLAnimatedImageView加载GIF时并没有对图片缓存,每次都需要重新去请求图片

SDWebImage5.x

新推出 SDAnimatedImageView类 整体流程

1、SDAnimatedImageView + WebCache.m [SDAnimatedImageView sd_setImageWithURL:]

[SDAnimatedImageView sd_internalSetImageWithURL:]

2、 [UIView sd_internalSetImageWithURL:]

3、接着交给SDWebImageManager分发

[SDWebImageManager loadImageWithURL:]

3.1 首先从缓存获取 [SDWebImageManager callCacheProcessForOperation:]

4、缓存查询流程 [SDImageCache queryImageForKey:] 依次从内存缓存和磁盘缓存去查询

4.1 如果内存缓存存在,直接返回image

4.2 如果内存缓存没有,但磁盘缓存存在

     NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        UIImage *diskImage;
        SDImageCacheType cacheType = SDImageCacheTypeNone;
        if (image) {
            // the image is from in-memory cache, but need image data
            diskImage = image;
            cacheType = SDImageCacheTypeMemory;
        } else if (diskData) {
            cacheType = SDImageCacheTypeDisk;
            // decode image data only if in-memory cache missed
            diskImage = [self diskImageForKey:key data:diskData options:options context:context];
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = diskImage.sd_memoryCost;
                [self.memoryCache setObject:diskImage forKey:key cost:cost];
            }
        }

只有当内存缓存没命中时才会对磁盘缓存的data解码

调用 SDImageCacheDefine.m里的SDImageCacheDecodeImageData进行解码

    UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context);

这里有判断是否只需要解码第一帧,如果不仅仅解码首帧,则从 context中取出AnimatedImageClass,默认使用的是 SDAnimatedImage,然后进行解码,根据需要还可以预解码所有帧。

如果是一个动图,并且不只解码第一帧,那么就会调用 SDAnimatedImage 的初始化方法把所有帧都解码出来。 [SDAnimatedImage initWithData] 传入data

通过SDImageCodersManager找到能对当前data解码的coder类(遵循SDAnimatedImageCoder协议),创建coder实例 ,最后调用[self initWithAnimatedCoder:animatedCoder scale:scale];生成SDAnimatedImage对象,这个对象保存_animatedCoder和_animatedImageFormat。

  - (instancetype)initWithAnimatedCoder:(id<SDAnimatedImageCoder>)animatedCoder scale:(CGFloat)scale {
    if (!animatedCoder) {
        return nil;
    }
    UIImage *image = [animatedCoder animatedImageFrameAtIndex:0];
    if (!image) {
        return nil;
    }
    self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation];
    if (self) {
        // Only keep the animated coder if frame count > 1, save RAM usage for non-animated image format (APNG/WebP)
        if (animatedCoder.animatedImageFrameCount > 1) {
            _animatedCoder = animatedCoder;
        }
        NSData *data = [animatedCoder animatedImageData];
        SDImageFormat format = [NSData sd_imageFormatForImageData:data];
        _animatedImageFormat = format;
    }
    return self;
}

SDAnimatedImage本身也是UIImage,实际上是第一帧图。

磁盘缓存返回后同步内存缓存,memoryCache

     diskImage = [self diskImageForKey:key data:diskData options:options context:context];
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = diskImage.sd_memoryCost;
                [self.memoryCache setObject:diskImage forKey:key cost:cost];
            }

所以内存缓存存储的是SDAnimatedImage对象

5、缓存如果命中返回,如果没有则进行下载操作 [SDWebImageManager callDownloadProcessForOperation:]

6、下载结束同样地调用 SDImageLoaderDecodeImageData 进行解码,在self.coderQueue 队列里进行

        [self.coderQueue cancelAllOperations];
    [self.coderQueue addOperationWithBlock:^{
        UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
        CGSize imageSize = image.size;
        if (imageSize.width == 0 || imageSize.height == 0) {
            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
        } else {
            [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
        }
        [self done];
    }];

7、下载结束缓存处理 回到SDWebImageManager [imageCache storeImage:image imageData:data forKey:] 依次按需存入内存缓存和磁盘缓存

存储时有时候(只有image没有data时)会先对image指定图片格式信息,类似打标签

   SDImageFormat format = image.sd_imageFormat;
    if (format == SDImageFormatUndefined) {
        // If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
        if (image.sd_isAnimated) {
            format = SDImageFormatGIF;
        } else {
            // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
            if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
                format = SDImageFormatPNG;
            } else {
                format = SDImageFormatJPEG;
            }
        }
    } 

再调用

data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];

根据不同的format将image编码,根据对应的格式,交给指定的encode类去进行压缩处理。最后调用存储到磁盘

[self.diskCache setData:imageData forKey:key];

这种情况下,下次从磁盘读取时可以知道图片的格式,之前版本的SD并没有判断GIF,导致存到磁盘的可能是PNG,自然下次读取时也不会播放。

8、展示GIF

UIView+WebCore finalSetImageBlock

UIImageView *imageView = (UIImageView *)view;
    finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
        imageView.image = setImage;
    };

[SDAnimatedImageView setImage:] 调 startAnimating 开始播放

几个关键的类和方法

SDAnimatedImagePlayer

SDDisplayLink

iOS上使用CADisplayLink,来自定义循环播放动画

calculateMaxBufferCount