原文鏈接:https://www.zybuluo.com/avenwu/note/876161
APP架構(gòu)師整理發(fā)布炉菲,轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)。
1.背景
在做動(dòng)畫(huà)的時(shí)候我們有很多選擇方案荒给。
最常見(jiàn)的是Android原生的幀動(dòng)畫(huà)竞膳,位移動(dòng)畫(huà)浓体,旋轉(zhuǎn)動(dòng)畫(huà),屬性動(dòng)畫(huà)等等,具體根據(jù)動(dòng)畫(huà)效果選擇實(shí)現(xiàn)方案喘批;
針對(duì)那些有規(guī)律朋贬,不太復(fù)雜的矢量動(dòng)畫(huà)我們往往也采取自定義View來(lái)實(shí)現(xiàn),比如各種狂拽炫酷的loading動(dòng)畫(huà)蚪燕;
如果自定義實(shí)現(xiàn)成本比較大娶牌,或者難以達(dá)到Android/iOS多端統(tǒng)一,也經(jīng)常采用幀動(dòng)畫(huà)馆纳,由UE提供幀動(dòng)畫(huà)素材诗良;
如果項(xiàng)目支持,也可以使用Airbnb出品的Lottie[1]鲁驶,這是一個(gè)非常牛x的動(dòng)畫(huà)解析庫(kù)鉴裹,不過(guò)對(duì)sdk版本有要求,具體可以自行查驗(yàn)下.
目前最優(yōu)方案是使用Lottie動(dòng)畫(huà)庫(kù)钥弯,在無(wú)法使用Lottie的情況下径荔,我們就需要手動(dòng)來(lái)具體幀動(dòng)畫(huà)的調(diào)優(yōu)了。
本文主要談?wù)勛鰩瑒?dòng)畫(huà)的一些優(yōu)化策略以避免OutOfMemoryError
問(wèn)題脆霎。
2. 幀動(dòng)畫(huà)有什么問(wèn)題总处?
我們都知道原生的Android幀動(dòng)畫(huà)在加載序列幀時(shí),是一次性將所有序列幀的圖片編碼到內(nèi)存當(dāng)中的睛蛛,所以執(zhí)行幀數(shù)較多的動(dòng)畫(huà)時(shí)很容易發(fā)生內(nèi)存不足鹦马,拋出?OutOfMemoryError
胧谈。
所以呢,你就需要解決內(nèi)存問(wèn)題荸频,基本上可以從以下幾方面入手菱肖;
-
降幀
在保證UI效果和視覺(jué)流暢度的情況下盡可能減少幀數(shù),比如UE輸出的序列幀可能默認(rèn)有有4试溯,5十張圖片蔑滓,減幀后可能只有20張;這里一定要注意的是遇绞,必須經(jīng)過(guò)UE來(lái)減幀键袱,RD不允許私自調(diào)整,避免效果不達(dá)標(biāo)摹闽;
-
壓縮尺寸
根據(jù)在手機(jī)上的展示大小蹄咖,縮放到一致的高寬,避免屏幕上展示100x100,你用一個(gè)500x500的資源付鹿;
-
壓縮體積
采用無(wú)損或者可接受的有損壓縮圖片質(zhì)量澜汤,這個(gè)也無(wú)需多說(shuō),老司機(jī)都懂得舵匾。打個(gè)廣告俊抵,推薦筆者寫(xiě)的IntelliJ插件?http://avenwu.net/biu/;
-
重寫(xiě)幀動(dòng)畫(huà)的編碼邏輯
這里的一個(gè)思路是動(dòng)態(tài)編碼圖片,不再一次性加載所有圖片坐梯,通過(guò)懶加載的方式徽诲,圖片對(duì)內(nèi)存的要求會(huì)大幅降低;
3. 手工實(shí)現(xiàn)幀動(dòng)畫(huà)
3.1 幀動(dòng)畫(huà)分析
下面主要針對(duì)?重寫(xiě)幀動(dòng)畫(huà)的編碼邏輯
?這個(gè)維度來(lái)聊聊具體的實(shí)現(xiàn)策略吵血。?
既然要重寫(xiě)幀動(dòng)畫(huà)谎替,首先就要知道Android原生幀動(dòng)畫(huà)的實(shí)現(xiàn)邏輯;通過(guò)閱讀相關(guān)源碼蹋辅,筆者繪制了如下簡(jiǎn)化示意圖钱贯,基本涵蓋了幀動(dòng)畫(huà)的構(gòu)造和執(zhí)行過(guò)程;
可以看到幀動(dòng)畫(huà)在首次inflate的時(shí)候會(huì)解析xml侦另,并將每一個(gè)item節(jié)點(diǎn)解析為drawable對(duì)象實(shí)例秩命,然后加入到數(shù)組當(dāng)中;后續(xù)動(dòng)畫(huà)過(guò)程就是輪詢(xún)繪制淋肾,在Drawable容器中繪制當(dāng)前drawable即可硫麻,整體代碼簡(jiǎn)潔漂亮。
但是這個(gè)邏輯我們并不能直接復(fù)用到手工實(shí)現(xiàn)的幀動(dòng)畫(huà)中樊卓,為什么拿愧?這里暫不解答,讀者可以自己思考一下:)
現(xiàn)在我們把幀動(dòng)畫(huà)的實(shí)現(xiàn)策略抽象一下碌尔,它就是一個(gè)生產(chǎn)者和消費(fèi)者的關(guān)系浇辜,如下圖所示:
總的來(lái)說(shuō)我們的優(yōu)化策略集中在編碼和緩存上面券敌,渲染不需要改變。根據(jù)編碼的的策略可能代碼差異會(huì)很大柳洋。在實(shí)際開(kāi)發(fā)中待诅,我們也遇到過(guò)一些坑,比如編碼速度跟不上熊镣,導(dǎo)致渲染的時(shí)候出現(xiàn)跳幀卑雁,推測(cè)主要是CPU時(shí)間切片問(wèn)題。?
所以如果做成單線程編碼绪囱,UI線程渲染是要非常謹(jǐn)慎的测蹲,否則很有可能你的編碼業(yè)務(wù)全部被阻塞了,導(dǎo)致UI層面頻繁丟幀鬼吵。
3.2 Bitmap 復(fù)用
在圖片編碼之后扣甲,我們需要考慮Bitmap的編碼復(fù)用,這樣可以避免每次decode一張圖片都要全新申請(qǐng)一塊內(nèi)存齿椅,通過(guò)復(fù)用已有的bitmap實(shí)例對(duì)象我們可以做到編碼的更低開(kāi)銷(xiāo)琉挖。
其核心在于?Options#inBitmap
,這個(gè)API是Android引入的一個(gè)新API涣脚,新API本身沒(méi)什么問(wèn)題示辈,問(wèn)題在于這個(gè)屬性是API 11引入的,但并不可以直接使用遣蚀,這導(dǎo)致做版本判斷的時(shí)候比較坑(你無(wú)法確定這個(gè)接口是否可用,除非你窮舉一下相關(guān)SDK版本)顽耳。根據(jù)源代碼,在API 14的時(shí)候使用該屬性會(huì)拋異常妙同,具體大家可以去develop了解相關(guān)細(xì)節(jié),膝迎。
在引入了Options#inBitmap的時(shí)候一定要注意姿勢(shì)粥帚。否則的話極有可能出現(xiàn)大量日志警告:
Called reconfigure on a bitmap that is in use! This may cause graphical corruption!
當(dāng)然如果出現(xiàn)了這個(gè)警告也不要緊張,肯定是可以解決的限次,這個(gè)警告是在core/jni/android/graphics/Bitmap.cpp
中拋出來(lái)的芒涡,含義其大致就是說(shuō)如果你在修改一個(gè)正在被使用的bitmap那么這會(huì)導(dǎo)致graphical corruption
。這個(gè)單詞沒(méi)有想到合適的中文翻譯卖漫,我猜就是會(huì)破壞圖形的意思吧费尽。
如果你一定要忽略這個(gè)異常的話,請(qǐng)忍受控制臺(tái)的大量警告輸出羊始,反正我是忍不了旱幼。要解決整個(gè)警告也很簡(jiǎn)單,只要保證復(fù)用bitmap作為inBitmap時(shí)不要使用當(dāng)前正在被ImageView展示的bitmap實(shí)例就可以了突委。
3.3 RAM和CPU的平衡處理
前面提到了柏卤,編碼圖片可能會(huì)跟不上渲染冬三,這是因?yàn)榫幋a一張圖片需要CPU去處理圖片,包括IO之類(lèi)的缘缚,這需要編碼線程申請(qǐng)CPU時(shí)間切片勾笆,如果UI線程或者其他線程池有高優(yōu)的任務(wù),那么編碼這一塊就會(huì)很尷尬桥滨。所以要解決這個(gè)問(wèn)題一方面可以提供bitmap內(nèi)存緩存窝爪,減少需要重新編碼的次數(shù),比如我們通過(guò)調(diào)試把圖片復(fù)用的命中率提高到40%,50%之類(lèi)齐媒。另外一個(gè)就是不能給編碼線程設(shè)置過(guò)低的優(yōu)先級(jí)
蒲每,不要覺(jué)得這是后臺(tái)任務(wù)應(yīng)該放一個(gè)BACKGROUND之類(lèi)的低優(yōu)先級(jí),根據(jù)andorid的官方說(shuō)明(具體文檔我忘了)里初,后臺(tái)線程和前臺(tái)(其他默認(rèn)優(yōu)先級(jí)線程)之間獲取CPU的能力差異是非常大的啃勉,好像接近二八開(kāi)的樣子。
3.4 緩存策略
緩存策略也是需要認(rèn)真考慮的地方双妨,前面我們講到了為了平衡編碼壓力淮阐,我們可以引入內(nèi)存緩存,那么以什么策略來(lái)緩存圖片呢刁品?(在心里說(shuō)出一個(gè)答案泣特,看看對(duì)不對(duì))
LRU?
如果你回答LRU的話挑随,恭喜你状您,你的緩存完全失效了。
雖然LRU使用非常廣泛的緩存算法兜挨,但是很遺憾他并不適合幀動(dòng)畫(huà)緩存膏孟,為什么呢?讀者可以自己思考一下LRU的淘汰策略和幀動(dòng)畫(huà)的執(zhí)行策略就明白了拌汇。
所以我們用的是什么裝逼的算法呢柒桑?這個(gè)筆者并不是專(zhuān)研算法的,所以不懂得太多唬人的名字噪舀。不過(guò)回想起多年前求學(xué)時(shí)魁淳,耳(quan)熟(bu)能(wang)詳(ji)的那些算法,你會(huì)想起還有一個(gè)叫LIFO的東西与倡,也就是后進(jìn)先出算法(Last in first out)界逛。
我們根據(jù)需要可以稍加改造找一下,首先實(shí)現(xiàn)一個(gè)LifoCache,這個(gè)可以從LruCache略加改造纺座,然后在取緩存/刪緩存時(shí)息拜,取倒數(shù)第二個(gè)即可。這樣可以最早執(zhí)行的動(dòng)畫(huà)被緩存下來(lái),并且有機(jī)會(huì)再下一輪動(dòng)畫(huà)來(lái)臨時(shí)被復(fù)用该溯。當(dāng)然機(jī)智的你也可以選擇其他更好的算法岛抄。
3.5 優(yōu)化點(diǎn)回顧
以上是我們?cè)趯?shí)踐當(dāng)中總結(jié)的一些比較關(guān)鍵的點(diǎn)。出于尊重原創(chuàng)的職業(yè)素養(yǎng)狈茉,必須聲明夫椭,我們的整體優(yōu)化的啟示靈感來(lái)源于FasterAnimationsContainer[2]
;?
原項(xiàng)目更多的像一個(gè)示意demo氯庆,存在諸多bug蹭秋,需要修復(fù)才能滿足基本使用,因此我們修復(fù)了這些問(wèn)題堤撵,并且發(fā)起了?Pull Request[3]
仁讨。
不過(guò)從維護(hù)記錄來(lái)看原項(xiàng)目已經(jīng)不太活躍,我們將這些改動(dòng)和新增的功能優(yōu)化做了梳理实昨。?
當(dāng)然除了修復(fù)這些問(wèn)題洞豁,我們實(shí)際上進(jìn)行了幾乎95%以上的重構(gòu),所以嚴(yán)格來(lái)說(shuō)荒给,這兩個(gè)項(xiàng)目除去解決內(nèi)存泄漏的思想丈挟,在工程上已經(jīng)沒(méi)有太多相似處了。
我們主要做了如下改動(dòng):
消除bitmap編碼警告志电;
修復(fù)前后臺(tái)切換后動(dòng)畫(huà)僵死的問(wèn)題曙咽;
取消單例模式,支持每個(gè)ImageView控制獨(dú)立的動(dòng)畫(huà)挑辆;
新增圖片內(nèi)存緩存例朱,支持緩存比配置;
動(dòng)畫(huà)不顯示時(shí)緩存自動(dòng)釋放與恢復(fù)鱼蝉;
支持原生xml定義洒嗤,兼容原生寫(xiě)法;
4. 調(diào)優(yōu)效果
這一節(jié)魁亦,我們簡(jiǎn)單對(duì)比下烁竭,手工實(shí)現(xiàn)的幀動(dòng)畫(huà)和原生幀動(dòng)畫(huà)的內(nèi)存數(shù)據(jù);
4.1 原生標(biāo)準(zhǔn)幀動(dòng)畫(huà)
在這幅圖中吉挣,我們執(zhí)行了一個(gè)完整的操作流程
啟動(dòng)程序(不包含目標(biāo)動(dòng)畫(huà)視圖);
跳轉(zhuǎn)到原生幀動(dòng)畫(huà)Activity婉弹,內(nèi)部有一個(gè)ImageView睬魂,設(shè)置了android:src為一個(gè)xml幀動(dòng)畫(huà);
可以看到剛進(jìn)入目標(biāo)Activity內(nèi)存立刻開(kāi)始大幅上升镀赌,CPU出現(xiàn)了波動(dòng)氯哮;而此時(shí)我們實(shí)際上沒(méi)有開(kāi)始執(zhí)行動(dòng)畫(huà);
獲取ImageView的src并start動(dòng)畫(huà)商佛,此時(shí)內(nèi)存沒(méi)有明顯波動(dòng)喉钢,并且CPU也相對(duì)平穩(wěn)姆打;
持續(xù)播放動(dòng)畫(huà),內(nèi)存和CPU基本不再變化肠虽,維持在一個(gè)高位狀態(tài)幔戏;
退出頁(yè)面,GC后內(nèi)存全部釋放税课;
4.2 懶加載幀動(dòng)畫(huà)
前面已經(jīng)提到闲延,我們的幀動(dòng)畫(huà)支持緩存設(shè)置,所以分別看一下緩存40%和100%緩存的兩種情況韩玩。
我們保持一致的操作流程垒玲,來(lái)看一下優(yōu)化后的幀動(dòng)畫(huà)的數(shù)據(jù);
啟動(dòng)程序(不包含目標(biāo)動(dòng)畫(huà)視圖)
跳轉(zhuǎn)到優(yōu)化幀動(dòng)畫(huà)Activity找颓,內(nèi)部有一個(gè)ImageView合愈,設(shè)置了app:src為一個(gè)xml幀動(dòng)畫(huà);
剛進(jìn)入目標(biāo)Activity內(nèi)存小幅增加击狮,CPU出現(xiàn)波動(dòng)佛析;此時(shí)我們實(shí)際上也沒(méi)有開(kāi)始執(zhí)行動(dòng)畫(huà),這個(gè)內(nèi)存開(kāi)銷(xiāo)是預(yù)覽的首幀帘不;
獲取ImageView的src并start動(dòng)畫(huà)说莫,隨著動(dòng)畫(huà)的執(zhí)行內(nèi)存使用量開(kāi)始慢慢上爬,期間伴隨著CPU的波動(dòng)寞焙;
持續(xù)播放動(dòng)畫(huà)储狭,內(nèi)存達(dá)到一個(gè)穩(wěn)定態(tài),基本不再變化捣郊;CPU持續(xù)變化辽狈;
退出頁(yè)面,GC后內(nèi)存全部釋放呛牲;
類(lèi)似的當(dāng)我們把緩存比調(diào)大刮萌,比如調(diào)到100%后,可以得到一個(gè)新的圖娘扩。這個(gè)圖的內(nèi)存高峰和CPU狀態(tài)基本可以看做是上面兩種情況的結(jié)合體着茸。當(dāng)緩存占比高了,那么后續(xù)需要重新編碼的次數(shù)也就少了琐旁,所以CPU的占用也就少了涮阔。
4.3 數(shù)據(jù)對(duì)比
以我們的測(cè)試為例,選用了?16
?張?600x600
?的jpg圖片灰殴,每張大小約為?14~18KB
?之間敬特。?
為了方便,對(duì)比我們選取幾個(gè)關(guān)鍵點(diǎn)的近似內(nèi)存和CPU數(shù)據(jù)。
5. 使用說(shuō)明
介紹完了原理伟阔,就來(lái)看下怎么使用吧辣之,API層面沒(méi)有做太多改變和原生使用比較接近。由于項(xiàng)目是開(kāi)源的皱炉,這里放上傳送門(mén):
http://hub.hacktons.cn/animation/
通過(guò)上述地址可以獲取項(xiàng)目最新的變化和源代碼等信息怀估。 下面我們簡(jiǎn)單看下目前如何使用我們的優(yōu)化方案來(lái)播放一個(gè)幀動(dòng)畫(huà)。
5.1 配置Gradle依賴(lài)庫(kù)
compile 'com.github.avenwu:animation:0.2.0'
5.2 通過(guò)MockFrameImageView使用動(dòng)畫(huà)
先進(jìn)行動(dòng)畫(huà)定義娃承,一般都是用一個(gè)xml搞定奏夫,方便省事。
在layout中進(jìn)行布局定義
最后在Activity或者其他地方就可以啟動(dòng)動(dòng)畫(huà)了
上面的使用方法維持了和原生幀動(dòng)畫(huà)一致的操作和配置历筝,除了layout里面寫(xiě)的控件不是ImageView酗昼,其他一模一樣的。
5.3 通過(guò)代碼使用動(dòng)畫(huà)
除此之外梳猪,我們也可以直接用代碼來(lái)實(shí)現(xiàn)這個(gè)動(dòng)畫(huà)配置麻削,這也是項(xiàng)目最開(kāi)始所支持的方式,這個(gè)就不需要依賴(lài)任何自定義的View春弥,直接在原生ImageView上生效:
5.4 效果對(duì)比圖
最后看一下各種幀動(dòng)畫(huà)的實(shí)現(xiàn)效果呛哟。
Download Video
6. 小結(jié)
綜合來(lái)看,原生幀動(dòng)畫(huà)更適合體量較小匿沛,內(nèi)存壓力不那么大的幀動(dòng)畫(huà)扫责,如此一次性加載所有幀,可以保證后續(xù)幀切換的流程性逃呼;而MockFrameAnimation則為了解決內(nèi)存問(wèn)題鳖孤,采取動(dòng)態(tài)編碼序列幀。
這相當(dāng)于用CPU的編碼/計(jì)算能力換取了內(nèi)存消耗抡笼;同時(shí)為了達(dá)到適合的平衡苏揣,我們?cè)试S開(kāi)發(fā)者設(shè)置圖片緩存的張數(shù),緩存數(shù)越大那么內(nèi)存消耗越多推姻,需要重新編碼的次數(shù)也就相對(duì)更少平匈;
[1]?https://github.com/airbnb/lottie-android??
[2]?https://github.com/tigerjj/FasterAnimationsContainer??
[3]?https://github.com/tigerjj/FasterAnimationsContainer/issues/11??
?
如果你有好的文章想和大家分享歡迎投稿,直接向我投遞文章鏈接即可藏古。