Kingfisher源码解析之加载动图

Kingfisher源码解析系列,由于水平有限,哪里有错,肯请不吝赐教

Kingfisher加载GIF的两种使用方式

  1. 使用UIImageView
    let imageView = UIImageView()
    imageView.kf.setImage(with: URL(string: "gif_url")!)
    
  2. 使用AnimatedImageView,AnimatedImageView继承自UIImageView
    let imageView = AnimatedImageView()
    imageView.kf.setImage(with: URL(string: "gif_url")!)
    

Kingfisher内部是如何处理的

看了上面2个显示GIF的方法,我们可能下面2个疑问,如果你对下面2个问题很清楚,本篇文章你可以跳过了

  • 加载GIF图和加载普通图片的使用方式是一样的,它是怎么做到如果是GIF图就显示GIF图,是普通图片就是现实普通图片的
  • 使用UIImageView和AnimatedImageView的调用方式也是一样的,这2中加载方式是否不同
    我们先来看第一个问题,Kingfisher是如何区分GIF图和普通图片的,这个问题分3种情况
  1. 图片通过Resource(通过网络下载的)或者ImageDataProvider提供的
  2. 图片是从缓存中内存缓存中加载的
  3. 图片是从磁盘缓存中加载的

首先来看第一种情况,在这之前,先来看下Kingfisher中配置项的这个配置public var processor: ImageProcessor = DefaultImageProcessor.default,这个配置是提供网络下载完成或者加载完成本地Data之后,会调用processorfunc process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把Data转换成UIImage,而processor的默认值是DefaultImageProcessor,在DefaultImageProcessor该方法的实现会调用下面这个方法

   public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
        var image: KFCrossPlatformImage?
        switch data.kf.imageFormat {
        case .JPEG:
            image = KFCrossPlatformImage(data: data, scale: options.scale)
        case .PNG:
            image = KFCrossPlatformImage(data: data, scale: options.scale)
        case .GIF:
            image = KingfisherWrapper.animatedImage(data: data, options: options)
        case .unknown:
            image = KFCrossPlatformImage(data: data, scale: options.scale)
        }
        return image
    }

在这个方法里会先判断图片的类型,判断的方式是取data的前8个字节,感兴趣的话,可以去源码里看下,这里就不贴了,如果是GIF图的话KingfisherWrapper.animatedImage这个方法

public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
    let info: [String: Any] = [
        kCGImageSourceShouldCache as String: true,
        kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
    ]
    guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
        return nil
    }
    //这里去掉了Macos下的处理
    var image: KFCrossPlatformImage?
    if options.preloadAll || options.onlyFirstFrame {
        guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else {
            return nil
        }
        if options.onlyFirstFrame {
            image = animatedImage.images.first
        } else {
            let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration
            image = .animatedImage(with: animatedImage.images, duration: duration)
        }
        image?.kf.animatedImageData = data
    } else {
        image = KFCrossPlatformImage(data: data, scale: options.scale)
        var kf = image?.kf
        kf?.imageSource = imageSource
        kf?.animatedImageData = data
    }
    return image
}

这个方法时展示GIF的核心逻辑,下面详细介绍下这个方法
首先把data转成CGImageSource,然后判断options.preloadAll || options.onlyFirstFrame 的值,其中onlyFirstFrame默认值为false,若为false则只加载第一帧,preloadAll这个值,在我们使用imageView.kf.setImage时,则取决于imageView的func shouldPreloadAllAnimation()函数的返回值,此函数是Kingfisher给UIImageView扩展的方法,在UIImageVIew中一直返回true

@objc extension KFCrossPlatformImageView {
    func shouldPreloadAllAnimation() -> Bool { return true }
}

也就是说在默认情况下,在上面的方法里会把imageSource转换成GIFAnimatedImage类的实例,而在这个类的实例里,做了获取GIF图的每一帧,并获取每一帧的时间然后加起来,最后通过UIImage.animatedImage(with: [images], duration: duration)生成一个动图的image实例,然后把image赋值给imageView.image

下面把imageSource转成animatedImage的代码,忽略了较多的异常情况

    let options: [String: Any] = [
        kCGImageSourceShouldCache as String: true,
        kCGImageSourceTypeIdentifierHint as String:kUTTypeGIF
    ]
    //把data转换成imageSource
    let imageSource = CGImageSourceCreateWithData(data as CFData, options as CFDictionary)!
    //获取GIF的总帧数
    let frameCount = CGImageSourceGetCount(imageSource)
    var images = [UIImage]()
    var gifDuration = 0.0
    for i in 0..<frameCount {
        //获取第i帧的图片,并把图片添加到数组里去
        let cgImage = CGImageSourceCreateImageAtIndex(imageSource, i, options as CFDictionary)!
        images.append( UIImage(cgImage: cgImage, scale: 1, orientation: .up))
        //若只有一帧,把动画时间设置成无限大,否则的话获取每一帧的时间
        if frameCount == 1 {
            gifDuration = Double.infinity
        }else {
            //获取每一帧的属性,
            let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) as! [String: Any]
            //获取属性中的GIF信息,以及获取信息中的时间
            let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as! [String: Any]
            let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
            let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
            let duration = unclampedDelayTime ?? delayTime
            gifDuration += duration?.doubleValue ?? 0.1
        }
    }
    imageView.image = UIImage.animatedImage(with: images, duration: gifDuration)

接着看第二种情况,若是从内存缓存中加载的,缓存的就是动图,所以是直接加载的

最后看第三种情况,若是从磁盘中缓存的,Kingfisher又是如何处理的,在这之前,先来看下Kingfisher中配置项的这个配置public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default,这个配置是提供当从磁盘中读取完数据之后,把数据反序列化为UIImage,会调用cacheSerializerpublic func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?方法,把Data反序列化为UIImage,而cacheSerializer的默认值是DefaultCacheSerializer,在DefaultCacheSerializer该方法的实现也会调用public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage?这个方法,下面就是跟第一种情况的逻辑一样了

下面来看AnimatedImageView是如何加载GIF图的,上面说imageView的shouldPreloadAllAnimation一直返回true,而AnimatedImageView重写了此函数,并返回false,因此option.preloadAll等于false,所以会走else里的逻辑,把data转成image,利用关联属性,给image添加了两个属性imageSource:CGImageSourceanimatedImageData:Data,并对其进行赋值

到现在为止,我们还是没有看到AnimatedImageView是如何展示GIF图的。接着往下看
AnimatedImageView重写了image的didSet,而上面的方法返回后,会对imageView.image进行赋值,正好触发了image的didSet,在这里开启了一个CADisplayLink和Animator。

Animator为imageView提供动图的数据,每一帧的图片以及时间,需要注意的是,它并不会一次加载好所有帧的图片,默认情况下,只是先加载前10帧,剩下的等需要的再去加载

CADisplayLink,在每次屏幕刷新的时候,去判断是否需要展示新的一帧图片,若需要,则刷新imageView

这里刷新是调用self.layer.setNeedsDisplay(),而调用此方法,系统会调用layer.delegate里的open func display(_ layer: CALayer),而UIView的layer.delegate是自己本身,所以会调用AnimatedImageView重写的display方法,这是我最开始没有想明白的地方

   override open func display(_ layer: CALayer) {
        if let currentFrame = animator?.currentFrameImage {
            layer.contents = currentFrame.cgImage
        } else {
            layer.contents = image?.cgImage
        }
    }

UIImageView和AnimatedImageView有什么不同

AnimatedImageView支持一下5点特性,而UIImageView都不支持

  1. repeatCount:循环次数
  2. autoPlayAnimatedImage:是否自动开始播放
  3. framePreloadCount:预加载的帧数
  4. backgroundDecode:是否在后台解码
  5. runLoopMode:GIF播放所在的runLoopMode

并且AnimatedImageView由于不用同时解码所有帧的图形数据,所以更节省内存,但是由于多了一些计算所以会比较浪费CPU

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • 1.系统UIImageView 多张图片组成动画 /** * UIImageView 动画 * Memor...
    zhengelababy阅读 8,683评论 3 6
  • 写在前面的 一颗伤心死掉的橘子树 不拘一世之利以为己私分,不以王天下为已处显。显则明。万物一府,死生同状。 扯淡结...
    d4d98020ef88阅读 5,665评论 10 17
  • 1.自定义控件 a.继承某个控件 b.重写initWithFrame方法可以设置一些它的属性 c.在layouts...
    圍繞的城阅读 3,342评论 2 4
  • 众所周知,iOS默认是不支持gif类型图片的显示的,但是我们项目中常常是需要显示gif为动态图片。那肿么办?第三方...
    smalldu阅读 11,628评论 9 53