为DanmakuKit支持GIF弹幕

1,795 阅读3分钟

前言

最近DanmakuKit收到了一个issue咨询如何在弹幕上展示GIF图。虽然我认为DanmakuKit提供的接口足以让开发者自己来实现GIF的展示,但是我决定还是要让DanmakuKit直接支持这个能力作为一个不默认引入的subspec。

技术方案

以我的了解,在iOS上实现GIF图展示的方案有两种。第一种是将GIF图取出来解析成每一帧图片并用帧动画进行播放。第二种是使用CADisplayLink,每次取有限数量的图片出来设置layer.content来播放。两种方案的优劣对比如下:

方案优点缺点
帧动画实现方案简单,CPU利用率低内存占用大,图片在主线程解码,存在流畅性问题
CADisplayLink动画内存占用低,基本不存在流畅性问题实现方案复杂,CPU利用率略高

帧动画

第一种方案实现十分简单,只需要将GIF图每一帧拆出来成一张CGImage,并获得每一帧的duration就可以生成GIF图的帧动画,代码如下:

func createGIFAnimation() -> CAKeyframeAnimation? {
        guard let data = gifModel?.resource else { return nil }
        let bytes = [UInt8](data)
        guard let gifData = CFDataCreate(nil, bytes, bytes.count) else { return nil }
        guard let image = CGImageSourceCreateWithData(gifData, nil) else { return nil }

        var time: Float = 0

        var framesArray: [AnyObject] = []
        var tempTimesArray: [NSNumber] = []
        let frameCount = CGImageSourceGetCount(image)

        for i in 0..<frameCount {
            var frameDuration: Float = 0.1
            let properties = CGImageSourceCopyPropertiesAtIndex(image, i, nil)
            guard let framePrpoerties = properties as? [String: AnyObject] else { return nil }
            guard let gifProperties = framePrpoerties[kCGImagePropertyGIFDictionary as String] as? [String: AnyObject] else { return nil }

            if let delayTimeUnclampedProp = gifProperties[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber {
                frameDuration = delayTimeUnclampedProp.floatValue
            } else {
                if let delayTimeProp = gifProperties[kCGImagePropertyGIFDelayTime as String] as? NSNumber {
                    frameDuration = delayTimeProp.floatValue
                }
            }

            if frameDuration < 0.011 {
                frameDuration = 0.1
            }

            if let frame = CGImageSourceCreateImageAtIndex(image, i, nil) {
                tempTimesArray.append(NSNumber(value: frameDuration))
                framesArray.append(frame)
            }

            time += frameDuration
        }

        var timesArray: [NSNumber] = []
        var base: Float = 0
        for duration in tempTimesArray {
            base += duration.floatValue / time
            timesArray.append(NSNumber(value: base))
        }

        timesArray.append(NSNumber(value: 1.0))

        let animation = CAKeyframeAnimation(keyPath: "contents")
        animation.beginTime = CACurrentMediaTime()
        animation.duration = CFTimeInterval(time)
        animation.repeatCount = Float.greatestFiniteMagnitude
        animation.isRemovedOnCompletion = false
        animation.fillMode = .forwards
        animation.values = framesArray
        animation.keyTimes = timesArray
        animation.timingFunction = CAMediaTimingFunction(name: .linear)
        animation.calculationMode = .discrete
        return animation

    }

帧动画的实现方案很简单,但其存在很大的内存性能问题。由于使用时需要获取所有帧的图片,那么将会占用很大的内存。另外,如果创建动画时未在后台线程解码图片,那么在主线程做图片解码也会带来流畅性问题。通过instrument的分析,我们可以很快的确认问题点。

内存Trace如下图,跑了一小段时间的GIF弹幕后可以发现图片占用了相对较大的内存。红框选中的部分展示了图片内存开销占了10.63MB。

截屏2021-09-05 10.19.30.png

流畅性Trace如下图,其中存在非常多的hitches,这些hitch都是由于图片在主线程解码导致的。

截屏2021-09-05 10.24.20.png

选择其中一个hitch详细展开,看红框选中的部分,可以发现调用栈在做图片解码。

截屏2021-09-05 10.26.10.png

内存Trace和流畅性Trace分析均说明了此方案的性能问题,因此帧动画方案不可行。

CADisplayLink动画

第二种方案实现略微复杂,不在本文中罗列所有代码。实现参考了YYWebImage和Kingfisher,详细实现请看GifAnimator。整体方案的流程图如下:

未命名文件.png

内存Trace如下图,图片的内存会被及时释放,占用非常小。此方案由于只取部分即将播放的图片放在内存中并且及时释放,因此解决了内存占用的问题。

截屏2021-09-05 10.53.00.png

流畅性Trace如下,由于图片展示前在后台对图片进行了解码,解决了流畅性问题。红框选中的部分说明了调用栈在后台线程,并且在做图片解码。

截屏2021-09-05 10.56.09.png

总结

大多数业务场景下,我们都是在列表上展示GIF图,若不处理好性能问题,将会给用户带来十分糟糕的流畅性体验。但大部分情况,我们无需自己实现GIF图的展示逻辑。OC上有YYWebImage,Swift上有Kingfisher,他们都对GIF图的展示有很好的支持。

欢迎大家使用DanmakuKit,它是一个高性能的弹幕框架,提供基础的弹幕能力。