前言
最近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。
流畅性Trace如下图,其中存在非常多的hitches,这些hitch都是由于图片在主线程解码导致的。
选择其中一个hitch详细展开,看红框选中的部分,可以发现调用栈在做图片解码。
内存Trace和流畅性Trace分析均说明了此方案的性能问题,因此帧动画方案不可行。
CADisplayLink动画
第二种方案实现略微复杂,不在本文中罗列所有代码。实现参考了YYWebImage和Kingfisher,详细实现请看GifAnimator。整体方案的流程图如下:
内存Trace如下图,图片的内存会被及时释放,占用非常小。此方案由于只取部分即将播放的图片放在内存中并且及时释放,因此解决了内存占用的问题。
流畅性Trace如下,由于图片展示前在后台对图片进行了解码,解决了流畅性问题。红框选中的部分说明了调用栈在后台线程,并且在做图片解码。
总结
大多数业务场景下,我们都是在列表上展示GIF图,若不处理好性能问题,将会给用户带来十分糟糕的流畅性体验。但大部分情况,我们无需自己实现GIF图的展示逻辑。OC上有YYWebImage,Swift上有Kingfisher,他们都对GIF图的展示有很好的支持。
欢迎大家使用DanmakuKit,它是一个高性能的弹幕框架,提供基础的弹幕能力。