使用Lottie动画导致内存暴增的原因之一

众所周知,Lottie是个非常赞的动画库,不过如果稍不注意,就会导致内存暴增,这里介绍其中一种情况。

最近公司有个需求是要在直播房间内播放一个礼物动画,用的是 Lottie,但是播放动画时,会卡个两秒,这种体验是十分不好的,另外播放期间内存会暴增至900+M,非常危险!必须得解决这个问题。

内存暴增

首先使用 Time Profiler 定位到卡顿的位置是在LayerImageProviderreloadImages()这个函数(这是用来加载动画的资源图片)

因为我们项目的礼物动画是会用到本地图片的,需要使用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上对其包装了一层来进行缓存。
扩展方法:将自定义的Provider包装成CachedImageProvider
在主要的构造方法中将Provider包装成CachedImageProvider来使用
替换Provider时包装成CachedImageProvider来使用

虽说现在已经有缓存了,不过并没有对其进行异步解码和压缩,这些操作还是需要我们自己去实现。

你可能感兴趣的:(使用Lottie动画导致内存暴增的原因之一)