Android|性能優化

一個好用的App,應該是一個對用戶及時響應的,在用戶打開頁面(或者打開app)時,給出相應結果,用戶點擊按鈕時的按下效果,或者動畫效果,用戶切換界面沒有任何阻塞和卡頓的感覺,當用戶滑動列表時,或者關閉頁面時,能快速的響應用戶的行爲。而這一切的自然發生的前提是我們的應用要保持在60FPS,並且穩定,爲什麼是60FPS?

這是由人眼和大腦協同工作的結果,人眼能感知到刷新上限就是60FPS,超過這個閾值,人眼識別不了更快的刷新頻率,我們平常看到的快速翻書的幀率接近12FPS,看上去是連續的動畫,但是人眼還是能感覺到有視覺停留,達到24FPS時,基本上比較流暢。所以大多數設備的刷新的頻率60FPS。
這裏有興趣的同學可以瞭解下雙緩衝技術和垂直同步的原理:雙緩衝和垂直同步
爲了讓大家瞭解GPU和CPU的協同工作原理,以及屏幕刷新的機制,我貼幾張圖:
這裏寫圖片描述

上半部分可以認爲是CPU和GPU協同工作的部分,省略了CPU,我們只看GPU就好,下半部分是屏幕刷新,在GPU中的雙緩衝中有:
Back Buffer
Frame Buffer
每次屏幕刷新時從Frame Buffer讀取一幀的數據,同時向Gpu發送VSYNC信號(Vertical synchronization),這時候Gpu開始在Back Buffer準備下一幀,如果下一次VSYNC信號到來時,Back Buffer 剛好準備好了,則會將Back Buffer的數據同步到Frame Buffer,就像下面這樣的效果:

這裏寫圖片描述
如果能每次屏幕刷新發出VSYNC信號時,Back Buffer都準備好了,則Frame Buffer能從Back Buffer拿到最新的幀數據,顯示最新的圖像,就像下面這樣:

這裏寫圖片描述

如果屏幕刷新發出VSYNC信號時,Back Buffer還沒有準備好,則不會把Back Buffer的內容同步到Frame Buffer,而直接顯示上一次Frame Buffer的內容,像下面這樣

這裏寫圖片描述

瞭解動屏幕刷新的流程和原理,我們知道每一幀的準備時間只有16ms,16ms並不是一個很長的時間,我們要重視16ms的邊界,渲染布局,動畫,包括在UI Thread裏的操作,在每一次刷新來臨時,都必須準備好。那麼平時我們會遇到哪些耗時的操作和卡頓呢?

性能問題之一:非UI操作發生在UI線程

磁盤IO 文件讀寫,數據庫讀寫,庫文件的加載,SharePreference讀寫
網絡IO 網絡訪問

造成的結果是:導致界面打開延遲,動畫不流暢,甚至ANR

性能問題之二:非UI操作在後臺線程

網絡訪問沒有超時處理,導致線程被佔用,其他任務的執行時間被佔用。
對數據庫或者文件操作之後,對獲取的數據進行預處理,比如過濾,排序,篩選的邏輯過於複雜,算法複雜度高。總而言之,就是後臺線程本身比較耗時,而UI需要等待後臺線程的數據,用戶得到的反饋很慢。解決辦法(業務相關):優化數據結構和算法,將任務拆分到多個線程中並行執行,並指定線程的優先級(重要的任務,可以設置更高優先級)

性能問題之三:UI操作發生在UI線程
事件處理 -- 事件回調的處理在主線程,如果耗時,放在後臺線程

動畫 -- 動畫中的位移,旋轉,縮放,都會佔用時間,動畫,動畫儘量不要太複雜,元素不要太多,如果動畫複雜,並且元素太多,以至於影響性能,可以考慮使用canvas來實現,可以極大的提升性能,

Measure/Layout -- 從根視圖開始向下傳遞Measure/Layout,佈局層次越深,元素越多,時間越長,儘量減少佈局,可以使用Hierarchy Viewer 查看不合理的佈局和渲染時間,使用ViewStub來延遲加載
繪製 --  onDraw,dispatchDraw
幀資源同步  -- 例如圖片,一次顯示數量少,但是單個文件非常大,或者一次顯示大量圖片,但是每個文件大小適中
繪製命令的合併和排序  -- 不同的調用順序會導致不同的消耗

這裏寫圖片描述

水平方向的綠線是16ms基準線,大部分的幀渲染都在16ms之內,右上角顯示的是每一幀中各個階段消耗的時間
可能看不清楚 ,我放一張局部放大的:
這裏寫圖片描述

從下到上分別是:
VSYNC 就是上面說的垂直同步信號的發送時間 (0ms)
Input Handing 處理輸入事件的時間 (0.41ms)
Animation 動畫處理的時間 (0.00ms)
Measure/Layout 測量和定位的時間 (0.07ms)
Draw Draw()方法執行的時間,也就是生成DisplayList的時間(0.28ms)
Sync/Upload 是將要繪製的資源送到GPU的時間,比如Bitmap上傳到Gpu(0.06ms)
Command Issue 對DisplayList進行Order 和Merge的時間,也就是說Draw()方法生成的DisplayList並不是全部執行,必須進行合併,裁剪,排序優化後再送入GPu繪製 (1.45ms)
Swap Buffers 可以理解成Gpu的實際繪製了,這是最後一步 (1.74ms)

而最終要的Misc Time (2.74ms)全稱是:Miscellaneous 這是發生在UI Thread,並且跟UI渲染無關的操作,應儘量減少在UI Thread 跟UI渲染無關的操作!
當前截圖的這一幀的總耗時爲:6.75ms,是非常快的,尤其2.74ms其實是可以避免的,是我在UI Thread進行了Bitmap相關的處理,所以比較耗時

說完了繪製的原理,大家想必對16ms有了深刻的認識了,16ms時間真的不多。

上面說了列舉了可能導致程序卡頓的操作和造成卡頓的原因,以及16ms,這一切的基礎是你對圖像渲染的工作原理有一定的瞭解,纔會重視這16ms,那我們如果追蹤和定位程序中卡頓和耗時操作呢?

StrickMode

這是StrickMode能提供的檢測在UI Thread的違例操作

磁盤讀取操作 detectDiskReads()
磁盤寫入操作 detectDiskWrites()
網絡操作 使用 detectNetwork()
自定義的耗時調用 detectCustomSlowCalls()

但是我要說的,SharePreference (9.0之上 apply異步提交 )的讀寫有時候確實要在UI Thread,還有SO文件的同步加載,Resources.getDrawable(),這些我們平常都是在UI Thread操作的,而且有時候也是必須的,在我們的工程裏,如果開始了StrickMode模式,會打印出一大堆的違例操作,比如 SharePreference,和View初始化時的getDrawable()操作,而且一些耗時 1ms,2ms的操作也會被追蹤出來,但是我們的其他的磁盤耗時操作,比如我從Sdcard上都文件的操作,他沒有檢查出來,反而是一大堆無關痛癢的信息,而且巨多,給大家看看官方的說法:
StrictMode is not a security mechanism and is not guaranteed to find all disk or network accesses
我也是醉了,StrickMode不是安全的,而且不保證能檢測到所有的磁盤讀寫和網絡操作。
反正我使用StrickMode沒有得到任何有用的信息。
另外值得一提的是,StrickMode可以檢查泄露,和那些沒有關閉的流,以及檢測實例個數,有興趣的同步可以自己研究下

detectActivityLeaks()
detectLeakedClosableObjects() 未關閉的Closable對象 
detectLeakedSqlLiteObjects()泄露的Sqlite對象 
setClassInstanceLimit()檢測實例數量

BlockCanary

你們可以用用試試看,最肯爹的是耗時操作的定義是我們自己決定的,定義成多少呢?不能完全以來於工具,他的原理就是每次Message的執行前和執行後,計算時間差,如果大於我們定義的時間閾值,就會可以以通知的信息告知開發者,但是最肯爹的是,我們到底定義成多少纔算Block呢?作者留給我們一個難題,其實這個真不好定義,我不建議用

最好用和最實用,並且能真正定性和定位問題的,我強烈推薦CPU Monitor(Method Trace)和Systrace

看看Ststrace的截圖吧:
這裏寫圖片描述

同樣的我上兩張局部放大圖:
這裏寫圖片描述這裏寫圖片描述

這個就是定量分析了,這是Systrace在5s內的操作追蹤,上面兩幅圖已經說明了,在UI Thread有耗時操作,但是渲染被延遲,84次的延遲,以及一次較耗時的 measure/layout操作,以及在OnDraw裏面的操作耗時較多,看看系統給的優化建議:
不要阻塞UI Thread,耗時的操作移到別的線程執行,比如網絡訪問,bitmap的加載和處理,並且應該把這些任務執行的線程的優先級及降低

用Systrace定性分析後,就可以用CPU Monitor來定位分析了:
CPU Monitor 可以追蹤應用的所有線程的所有方法調用的時間,在工具欄上可以選擇要呈現的線程追蹤

這裏寫圖片描述

這裏寫圖片描述

WallClock Time 表示方法調用和方法返回的時間(包括阻塞等待,睡眠的時間)
CPU Time 實際運行的時間(使用CPU)

Inclusive Time 表示方法執行的時間包括子方法的調用執行時間
Exclusive Time 表示方法執行的時間,不包括子方法的執行

這幅圖有兩個方向:水平方向,垂直方向
水平方向表示方法的順序調用

fanctionA();
fanctionB();

垂直方向表示方法的嵌套調用

fanctionA(){
    factionC();
    factionB();
}

通過GPU Monitor可以抓出到底是哪個方法耗時,把耗時的操作簡化,優化,如果不是UI渲染相關的,應該放在工作線程來做,還可以檢查做佈局渲染的耗時,佈局的優化可能要深入業務去分析,比如可以延遲加載(使用ViewStub)

通過CPU Monitor就可以把很多非UI渲染的操作揪出來,剩下的純UI方面的優化了,這個要深入到代碼和業務中,但是並不多說耗時操作放在後臺就行了,就像上面一開始說的,有些後臺操作的任務本身就可以優化,比如算法,數據結構的不合理,都是可以優化,如果我們不是一開始重視性能優化,那麼後期要做的優化非常多。其實最難的就是UI渲染方面的優化。

比如官方是不推薦用LinearLayout和RelativeLayout,gridLayout,因爲效率比較低,會導致整個使用LinearLayout和RelativeLayout,gridLayout的佈局重新測量多次,而測量和佈局多次,消耗是很大的。
推薦使用ConstraintLayout,可以替代LinearLayout和ReltiveLayout,但是性能卻好很多。

再有就是在ListView或者RecyclerView或者列表滑動的時候,最好不好出發Measure/layout操作,比如列表中的TextView.setText()頻繁調用會導致 Measure/Layout出發,導致列表滑動不流暢。

還有比如有一張動態圖是由5張不同圖疊加到一起,其中有4張是靜止不懂的,還有一張圖是變化的,如果自定義這個效果,我們肯定是用canvas來實現了,draw 不同的bitmap,但是我們是要繪製5次嗎?不是,如果是繪製5次,就會有重繪了,其實我們只需要繪製2次就夠了,因爲有4張圖是不變的,我們可以先把這4張圖一次性合成一張,這樣每次繪製的時候,其實只用畫兩次就夠了,而且還節省內存

還有比如有下面的操作

canvas.rotate()
canvas.clip()
canvas.drawBitmap()
canvas.clip()
canvas.rotate()
canvas.drawBitmap()

最終繪製出來的效果是一樣的,但是性能是不一樣的呢,這裏面設計到 Stencil Buffer,不講深了,其實我也不懂。。。

關於佈局的一些技巧我就不說了,HierarchyViewer對我來說可有可無

平常開發中還有很多優化技巧,如果你做UI比較多,要細細的琢磨纔行

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章