使用Lottie動(dòng)畫導(dǎo)致內(nèi)存暴增的原因之一

眾所周知,Lottie是個(gè)非常贊的動(dòng)畫庫,不過如果稍不注意,就會(huì)導(dǎo)致內(nèi)存暴增,這里介紹其中一種情況。

最近公司有個(gè)需求是要在直播房間內(nèi)播放一個(gè)禮物動(dòng)畫,用的是 Lottie,但是播放動(dòng)畫時(shí),會(huì)卡個(gè)兩秒,這種體驗(yàn)是十分不好的,另外播放期間內(nèi)存會(huì)暴增至900+M,非常危險(xiǎn)!必須得解決這個(gè)問題。

內(nèi)存暴增

首先使用 Time Profiler 定位到卡頓的位置是在LayerImageProviderreloadImages()這個(gè)函數(shù)(這是用來加載動(dòng)畫的資源圖片)

因?yàn)槲覀冺?xiàng)目的禮物動(dòng)畫是會(huì)用到本地圖片的,需要使用AnimationView(animation:, imageProvider:)方式創(chuàng)建動(dòng)畫,指定資源路徑:

動(dòng)畫文件結(jié)構(gòu)

reloadImages內(nèi)部通過循環(huán)調(diào)用imageProvider.imageForAsset(asset:)來加載的,再點(diǎn)進(jìn)去看看詳細(xì)的代碼:

原來 Lottie 是這么簡單粗暴的加載圖片(眾所周知,UIImage(contentsOfFile:)方式創(chuàng)建的圖片是不會(huì)緩存的,好處是使用完就能銷毀,不過每次調(diào)用都是一個(gè)新的UIImage對象,不適合用在復(fù)用性高的圖片),不過整個(gè)禮物動(dòng)畫也就20張小圖片而已,怎么就暴增到900+M呢,在imageForAsset里面打印一下調(diào)用情況:

哇靠,好家伙,果然,即便同一張圖片都重復(fù)創(chuàng)建了200+次,何況20張,一兩秒內(nèi)就加載了差不多4000多張,不卡才怪,難怪內(nèi)存暴增900+M。

發(fā)現(xiàn)問題所在就好解決了,先將圖片緩存起來,在動(dòng)畫播放期間內(nèi)復(fù)用,不要重復(fù)創(chuàng)建即可。

好在 Lottie 可以讓我們自定義imageProvider,做法很簡單,初始化時(shí)先將 UIImage 緩存起來,再創(chuàng)建動(dòng)畫:

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()
}

立馬試試,不會(huì)再卡個(gè)兩秒了,爽,再看看內(nèi)存:

最高也就60M,并且動(dòng)畫結(jié)束就釋放,舒服了~

另外,既然是提前緩存,可以參考YYWebImage的做法,再加個(gè)異步解碼吧(系統(tǒng)默認(rèn)是圖片顯示的那一刻才會(huì)解碼,并且解碼過程是在主線程),這樣主線程就更加順滑了:

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()
        }
    }
}

最終效果:

而且內(nèi)存進(jìn)一步減少至49M左右,畢竟使用CGBitmap方式繪制的圖片直接適用于手機(jī)的顯示,省去系統(tǒng)的自動(dòng)解碼過程:

到此為止最棘手的問題算是解決了~

最新更新

在新版的 Lottie 中,已經(jīng)內(nèi)置了CachedImageProvider,并且是默認(rèn)使用的(顧名思義就是會(huì)對圖片進(jìn)行緩存的圖片提供類,所以不會(huì)再像以前那樣不斷地創(chuàng)建、銷毀UIImage對象了)。

  • PS:這是個(gè)私有類,它是在我們自定義的Provider上對其包裝了一層來進(jìn)行緩存。
擴(kuò)展方法:將自定義的Provider包裝成CachedImageProvider
在主要的構(gòu)造方法中將Provider包裝成CachedImageProvider來使用
替換Provider時(shí)包裝成CachedImageProvider來使用

雖說現(xiàn)在已經(jīng)有緩存了,不過并沒有對其進(jìn)行異步解碼和壓縮,這些操作還是需要我們自己去實(shí)現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容