众所周知,Lottie是个非常赞的动画库,不过如果稍不注意,就会导致内存暴增,这里介绍其中一种情况。
最近公司有个需求是要在直播房间内播放一个礼物动画,用的是 Lottie,但是播放动画时,会卡个两秒,这种体验是十分不好的,另外播放期间内存会暴增至900+M,非常危险!必须得解决这个问题。
首先使用 Time Profiler 定位到卡顿的位置是在LayerImageProvider
的reloadImages()
这个函数(这是用来加载动画的资源图片)
因为我们项目的礼物动画是会用到本地图片的,需要使用AnimationView(animation:, imageProvider:)
方式创建动画,指定资源路径:
而reloadImages
内部通过循环调用imageProvider.imageForAsset(asset:)
来加载的,再点进去看看详细的代码:
原来 Lottie 是这么简单粗暴的加载图片(众所周知,UIImage(contentsOfFile:)
方式创建的图片是不会缓存的,好处是使用完就能销毁,不过每次调用都是一个新的UIImage对象,不适合用在复用性高的图片),不过整个礼物动画也就20张小图片而已,怎么就暴增到900+M呢,在imageForAsset
里面打印一下调用情况:
哇靠,好家伙,果然,即便同一张图片都重复创建了200+次,何况20张,一两秒内就加载了差不多4000多张,不卡才怪,难怪内存暴增900+M。
发现问题所在就好解决了,先将图片缓存起来,在动画播放期间内复用,不要重复创建即可。
好在 Lottie 可以让我们自定义imageProvider
,做法很简单,初始化时先将 UIImage 缓存起来,再创建动画:
struct CacheImageProvider: AnimationImageProvider {
let images: [String: CGImage]
func imageForAsset(asset: ImageAsset) -> CGImage? {
images[asset.name] ?? nil
}
}
func startAnimation() {
let anim = Animation.filepath(animJsonPath)
var images: [String: CGImage] = [:]
for fileName in fileNames {
let imagePath = imageDirPath + "/\(fileName)" // 拼接完整路径
guard let image = UIImage(contentsOfFile: imagePath) else { break }
images[fileName] = cgImage
}
let provider = CacheImageProvider(images: images)
let animView = AnimationView(animation: anim, imageProvider: subItem.provider)
self.addSubview(animView)
animView.play()
}
立马试试,不会再卡个两秒了,爽,再看看内存:
最高也就60M,并且动画结束就释放,舒服了~
另外,既然是提前缓存,可以参考YYWebImage
的做法,再加个异步解码吧(系统默认是图片显示的那一刻才会解码,并且解码过程是在主线程),这样主线程就更加顺滑了:
struct CacheImageProvider: AnimationImageProvider {
let images: [String: CGImage]
func imageForAsset(asset: ImageAsset) -> CGImage? {
images[asset.name] ?? nil
}
}
func startAnimation() {
DispatchQueue.global().async {
let anim = Animation.filepath(animJsonPath)
var images: [String: CGImage] = [:]
for fileName in fileNames {
let imagePath = imageDirPath + "/\(fileName)" // 拼接完整路径
guard let image = UIImage(contentsOfFile: imagePath) else { break }
guard let cgImage = image.jp.decode() else { break } // 解码
images[fileName] = cgImage
}
let provider = CacheImageProvider(images: images)
DispatchQueue.main.sync {
let animView = AnimationView(animation: anim, imageProvider: subItem.provider)
self.addSubview(animView)
animView.play()
}
}
}
最终效果:
而且内存进一步减少至49M左右,毕竟使用CGBitmap
方式绘制的图片直接适用于手机的显示,省去系统的自动解码过程:
到此为止最棘手的问题算是解决了~
最新更新
在新版的 Lottie 中,已经内置了CachedImageProvider
,并且是默认使用的(顾名思义就是会对图片进行缓存的图片提供类,所以不会再像以前那样不断地创建、销毁UIImage
对象了)。
- PS:这是个私有类,它是在我们自定义的
Provider
上对其包装了一层来进行缓存。
虽说现在已经有缓存了,不过并没有对其进行异步解码和压缩,这些操作还是需要我们自己去实现。