眾所周知眶拉,Lottie是個(gè)非常贊的動(dòng)畫庫(kù),不過如果稍不注意错沃,就會(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è)問題咆贬。
首先使用 Time Profiler 定位到卡頓的位置是在LayerImageProvider
的reloadImages()
這個(gè)函數(shù)(這是用來(lái)加載動(dòng)畫的資源圖片)
因?yàn)槲覀冺?xiàng)目的禮物動(dòng)畫是會(huì)用到本地圖片的,需要使用AnimationView(animation:, imageProvider:)
方式創(chuàng)建動(dòng)畫帚呼,指定資源路徑:
而reloadImages
內(nèi)部通過循環(huán)調(diào)用imageProvider.imageForAsset(asset:)
來(lái)加載的掏缎,再點(diǎn)進(jìn)去看看詳細(xì)的代碼:
原來(lái) Lottie 是這么簡(jiǎn)單粗暴的加載圖片(眾所周知,UIImage(contentsOfFile:)
方式創(chuàng)建的圖片是不會(huì)緩存的煤杀,好處是使用完就能銷毀眷蜈,不過每次調(diào)用都是一個(gè)新的UIImage對(duì)象,不適合用在復(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)問題所在就好解決了勋功,先將圖片緩存起來(lái),在動(dòng)畫播放期間內(nèi)復(fù)用辅甥,不要重復(fù)創(chuàng)建即可酝润。
好在 Lottie 可以讓我們自定義imageProvider
,做法很簡(jiǎn)單璃弄,初始化時(shí)先將 UIImage 緩存起來(lái)要销,再創(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ì)對(duì)圖片進(jìn)行緩存的圖片提供類糊昙,所以不會(huì)再像以前那樣不斷地創(chuàng)建、銷毀UIImage
對(duì)象了)谢谦。
- PS:這是個(gè)私有類释牺,它是在我們自定義的
Provider
上對(duì)其包裝了一層來(lái)進(jìn)行緩存。
雖說(shuō)現(xiàn)在已經(jīng)有緩存了回挽,不過并沒有對(duì)其進(jìn)行異步解碼和壓縮没咙,這些操作還是需要我們自己去實(shí)現(xiàn)。