這是一個(gè)關(guān)于星空的自定義動(dòng)畫Sample,源碼請(qǐng)戳 https://github.com/wangfuda/nebula
Gif圖:
本文將重點(diǎn)講解在本例自定義動(dòng)畫編程中,如何結(jié)合 Android Studio 的 Memory Monitor,GPU monitor 按步驟做內(nèi)存優(yōu)化,GPU渲染優(yōu)化。
關(guān)于動(dòng)畫實(shí)現(xiàn)部分,源碼已提交至github,請(qǐng)手動(dòng)閱讀理解,注釋很詳盡。
內(nèi)存占用優(yōu)化
內(nèi)存占用優(yōu)化 步驟一:移動(dòng)圖片資源至大分辨率目錄下,比如xxxhdpi.
先來(lái)彪一張直接擼完代碼無(wú)任何優(yōu)化的情況下,內(nèi)存的占用圖:
內(nèi)存占用246M,不能忍。
問:為什么這么大?
答:因?yàn)橘Y源圖都是高清1K分辨率的圖。
問:為什么這么大?
答:。。。
那么我們算一下這246M內(nèi)存占用是怎么來(lái)的吧。
先來(lái)一條圖片內(nèi)存占用計(jì)算公式,公式溯源請(qǐng)自行去看源碼:BitmapFactory.Java & BitmapFactory.cpp
scaledWidth = int( Width * targetDensity / density + 0.5)
scaledHeight = int( Height * targetDensity / density + 0.5)
memory = scaledWidth * scaledHeight * 4
其中參數(shù)定義如下:
Width:圖片寬
Height:圖片高
targetDensity:加載圖片的目標(biāo)手機(jī)的 density,這個(gè)值的來(lái)源是 DisplayMetrics 的 densityDpi,如果是小米note那么這個(gè)數(shù)值就是480,詳見下圖關(guān)于targetDensity的參數(shù)細(xì)節(jié)。
density:decodingBitmap 的 density,這個(gè)值跟這張圖片的放置的目錄有關(guān)(比如 hdpi 是240,xxhdpi 是480)
每像素字節(jié)數(shù):ARGB8888格式的圖片,每像素占用 4 Byte,而 RGB565則是 2 Byte。
對(duì)于一張1080x1920的圖來(lái)說(shuō),放置在hdpi目錄,并在小米note手機(jī)上(分辨率1080x1920,targetDensity為480),而且均默認(rèn)以ARGB8888格式加載。
內(nèi)存占用計(jì)算公式:
scaledWidth = int( 1080* 480/ 240+ 0.5) =2160
scaledHeight = int( 1920* 480/ 240+ 0.5)=3840
memory = scaledWidth * scaledHeight * 4=216038404 = 33177600 = 33.17M
一張背景圖就占用33M,這個(gè)分辨率的圖,我們r(jià)es下一共有7張,還有其他幾張小圖。這回可以回答為什么占用245M的內(nèi)存了。
那么內(nèi)存占用的優(yōu)化方案也就有了,我們盡量把圖片資源放到大分辨率目錄下,比如xxxhdpi(當(dāng)然還有個(gè)前提,你的圖片分辨率也確實(shí)符合大分辨率,否則會(huì)出現(xiàn)在大分辨率設(shè)備上,顯示不全的問題)。
我們來(lái)看看把圖片資源移動(dòng)到xxxhdpi目錄下后,內(nèi)存占用情況:
把圖片資源從hdpi移動(dòng)到xxx-hdpi,從246M降低到56M,減少了190M,Bingo!
內(nèi)存占用優(yōu)化 步驟 二:壓縮png圖片大小(包體大小會(huì)減小,但與內(nèi)存占用情況無(wú)關(guān))
初始單張圖片大小都接近2M,經(jīng)過(guò)tinypng優(yōu)化后,壓縮率達(dá)到70-80%,非常完美,包體大小減小了,不過(guò),經(jīng)過(guò)我們步驟一的科學(xué)計(jì)算,這個(gè)優(yōu)化并不會(huì)影響圖片在內(nèi)存中的占用。
內(nèi)存占用優(yōu)化 步驟 三:動(dòng)畫完成且不再循環(huán)展示的部分,相關(guān)bitmap釋放
public void releaseBitmap {
...
bitmap.recycle();
...
}
來(lái)看下bitmap釋放后的memory monitor圖:
內(nèi)存占用降低到36M,減少了20M
內(nèi)存占用優(yōu)化 步驟 四:無(wú)用對(duì)象釋放,非透明背景圖片采用RGB_565顏色格式,并且將圖片的inSampleSize設(shè)置為2
public void releaseValueAnimator {
...
valueAnimator = null;
...
}
public void initBitmap {
...
localOptions.inSampleSize = 2;
localOptions.inPreferredConfig = Bitmap.Config.RGB_565;
...
}
來(lái)看下變更圖片顏色格式及采樣率后的memory monitor圖:
內(nèi)存占用降低到28M,減少了8M(后來(lái)monitor截圖只對(duì)背景圖片做inPreferredConfig調(diào)整,內(nèi)存占用變?yōu)?0M,相比全部設(shè)置為2,增加了2M)
GPU渲染優(yōu)化
接下來(lái)我們要專注于GPU渲染優(yōu)化了。通過(guò)前面幾張圖,也能看到GPU monitor的狀態(tài),非常不樂觀,完全達(dá)不到幀率刷新的要求:即每幀渲染不超過(guò)16ms,每秒可以渲染60幀。
先看GPU Monitor的各項(xiàng)指標(biāo)含義:
Misc Time:表示在主線程執(zhí)行了太多的任務(wù),導(dǎo)致UI渲染跟不上vSync的信號(hào)而出現(xiàn)掉幀的情況;出現(xiàn)該線條的時(shí)候,可以在Log中看到這樣的日志: Skipped xxx frames! The application may be doing too much work on its main thread
Swap Buffers:表示處理任務(wù)的時(shí)間,也可以說(shuō)是CPU等待GPU完成任務(wù)的時(shí)間,線條越高,表示GPU做的事情越多;
Command Issue:表示執(zhí)行任務(wù)的時(shí)間,這部分主要是Android進(jìn)行2D渲染顯示列表的時(shí)間,為了將內(nèi)容繪制到屏幕上,Android需要使用Open GL ES的API接口來(lái)繪制顯示列表,紅色線條越高表示需要繪制的視圖更多;
Sync:表示的是準(zhǔn)備當(dāng)前界面上有待繪制的圖片所耗費(fèi)的時(shí)間,為了減少該段區(qū)域的執(zhí)行時(shí)間,我們可以減少屏幕上的圖片數(shù)量或者是縮小圖片的大小;
Draw:表示測(cè)量和繪制視圖列表所需要的時(shí)間,藍(lán)色線條越高表示每一幀需要更新很多視圖,或者View的onDraw方法中做了耗時(shí)操作;
Measure/Layout:表示布局的onMeasure與onLayout所花費(fèi)的時(shí)間,一旦時(shí)間過(guò)長(zhǎng),就需要仔細(xì)檢查自己的布局是不是存在嚴(yán)重的性能問題;
Animation:表示計(jì)算執(zhí)行動(dòng)畫所需要花費(fèi)的時(shí)間,包含的動(dòng)畫有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦這里的執(zhí)行時(shí)間過(guò)長(zhǎng),就需要檢查是不是使用了非官方的動(dòng)畫工具或者是檢查動(dòng)畫執(zhí)行的過(guò)程中是不是觸發(fā)了讀寫操作等等;
Input Handling:表示系統(tǒng)處理輸入事件所耗費(fèi)的時(shí)間,粗略等于對(duì)事件處理方法所執(zhí)行的時(shí)間。一旦執(zhí)行時(shí)間過(guò)長(zhǎng),意味著在處理用戶的輸入事件的地方執(zhí)行了復(fù)雜的操作;
Vsync Delay:見Misc Time
GPU渲染優(yōu)化 步驟一:優(yōu)化內(nèi)存占用
可以回過(guò)頭去看看內(nèi)存優(yōu)化過(guò)程演進(jìn)中的Monitor圖,隨著內(nèi)存占用的降低,GPU渲染的性能改善也是隨之漸進(jìn)的,所以GPU渲染性能優(yōu)化,首選就是內(nèi)存優(yōu)化
GPU渲染優(yōu)化 步驟二:能在初始化中做的事,堅(jiān)決不在onDraw中搞。
在sample代碼中重構(gòu)了onDraw中的畫筆的屬性設(shè)置,繪制區(qū)域的創(chuàng)建等代碼,這些代碼都重構(gòu)到初始化中。而在onDraw中僅做參數(shù)值的動(dòng)態(tài)調(diào)整。
Tips.本例Sample最初始未經(jīng)過(guò)任何優(yōu)化的代碼及最終優(yōu)化版本代碼均在github上可以查看到commit記錄,這里不再寫詳細(xì)代碼對(duì)比,請(qǐng)移動(dòng)github閱讀源碼。
我們看下重構(gòu)前后的對(duì)比圖monitor_5_bitmap_RGB_565_inSampleSize2.png
Vs monitor_6_gpu_optimize_object_and_paint_create.png
,對(duì)比發(fā)現(xiàn),GPU渲染耗時(shí)明顯降低。
我們暫時(shí)先對(duì)比看GPU Monitor的 0s ~ 9s 部分的性能改善,目前GPU優(yōu)化主要在這里體現(xiàn),因?yàn)楹蟀氩糠指魑粻斂吹搅耍珿PU渲染耗時(shí)飆升,掉幀嚴(yán)重,那部分的優(yōu)化在后續(xù)步驟會(huì)提到。
重構(gòu)前 GPU Monitor:
重構(gòu)后 GPU Monitor:
GPU渲染優(yōu)化 步驟三:能用硬件加速,就別關(guān)閉它。
我們都看到了,在GPU Monitor中顯示,9s后的GPU渲染,每幀耗時(shí)突然飆升,每幀渲染都是超60ms,
Draw上升至172ms,Vsync上升至148ms.
而且在logcat中也看到日志:
07-09 11:17:57.135 15980-15980/com.osan.nebula I/Choreographer: Skipped 31 frames! The application may be doing too much work on its main thread.
每幀超時(shí),掉幀嚴(yán)重。到底是什么原因呢?經(jīng)過(guò)反復(fù)的排查,終于找到原因,我們來(lái)聊聊這個(gè)飆升的來(lái)龍去脈。
由于在后半部分的動(dòng)畫中,繪制小星星的光暈效果時(shí),使用的畫筆設(shè)置了模糊屬性,為了給小星星加個(gè)光暈效果。
paintCircleStar.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.SOLID));
然后google官方文檔都說(shuō)了,硬件加速不支持的UI特效API之一就有它。也就是你要用硬件加速,這個(gè)模糊效果就失效。所以我一門心思的為了給小星星加光暈... 光暈...暈...,最后我選擇了關(guān)閉硬件加速
private void drawState7(Canvas canvas) {
...
canvas.save();
setLayerType(LAYER_TYPE_SOFTWARE, null);//關(guān)閉硬件加速
canvas.translate(halfWidth, halfHeight);
canvas.scale(scale, scale);
canvas.rotate(30f * mValue7);
...
}
呵呵,為了小星星,闖禍了。因?yàn)楸纠齽?dòng)畫中,各種對(duì)畫布的旋轉(zhuǎn),縮放,變換,透明度動(dòng)態(tài)變化,在非硬件加速情況下,不停的重新繪制,是GPU渲染耗時(shí)飆升的唯一原因。
我們來(lái)看看開啟硬件加速的情況下,GPU Monitor的指標(biāo)監(jiān)控:
恢復(fù)硬件加速后,渲染耗時(shí)立刻恢復(fù)到綠線以下,即每幀渲染不超過(guò)16ms,達(dá)到渲染標(biāo)準(zhǔn)。而且,CPU消耗也明顯下降。
那么為什么硬件加速有如此神奇之功效?
使用硬件加速在對(duì)一些view的屬性改變上有更高的效率,因?yàn)椴恍枰獀iew的invalidate和redrawn。而我們動(dòng)畫中正式大量使用了對(duì)屬性的改變。屬性如:
透明度:alpha
移動(dòng):x, y, translationX, translationY
縮放:scaleX, scaleY
旋轉(zhuǎn):rotation, rotationX, rotationY
坐標(biāo):pivotX, pivotY
注:
1)使用硬件加速,對(duì)于渲染性能的提示是顯著的,API>14后,硬件加速是默認(rèn)開啟的。
2)硬件加速還不支持所有的2D繪圖命令,開啟后可能會(huì)影響自定義View和繪圖操作。
GPU渲染優(yōu)化 步驟四:優(yōu)化算數(shù)運(yùn)算,并盡量從ondraw中移除算數(shù)運(yùn)算
涉及計(jì)算任務(wù),能不在ondraw中執(zhí)行的,就堅(jiān)決移走,即使只是一個(gè)a*b或a/b.
因?yàn)槲覀兝^續(xù)重構(gòu)了onDraw方法,將可能優(yōu)化的運(yùn)算代碼均做了優(yōu)化,能放到初始化做的就移到初始化,能提煉共用的運(yùn)算公式就共用,能不重復(fù)算的就絕對(duì)不算第二遍。
我們來(lái)看看優(yōu)化算數(shù)運(yùn)算后,GPU Monitor的指標(biāo)監(jiān)控:
相比步驟三中的GPU Monitor指標(biāo)有了進(jìn)一步降低,雖然降低幅度很小,但是還是對(duì)GPU渲染性能提升有效果的,而且觀察發(fā)現(xiàn),優(yōu)化后,CPU和GPU的指標(biāo)看起來(lái)更平穩(wěn)。
最后附上一張coding過(guò)程中的草圖以及nebula自定義動(dòng)畫的截圖