深入解析:Android卡頓檢測及優化專案實戰經驗總結,任君白嫖

augfun發表於2020-11-17

前言

之前在專案中做過一些Android卡頓以及效能優化的工作,但是一直沒時間總結,趁著這段時間把這部分總結一下。

GitHub系統教程學習地址:https://github.com/Timdk857/Android-Architecture-knowledge-2-

包括【面試專題】、【Jetpack】、【Android framework全套學習筆記】、【flutter全套教程】、【微信小程式全套教程】、【全方位效能優化專題】持續更新中~~

歡迎白嫖、標星、指教~~

作者:Hanking
連結:https://juejin.im/post/5eec6dece51d4573d27cee2a

卡頓

在應用開發中如果留意到log的話有時候可能會發下下面的log資訊:

I/Choreographer(1200): Skipped 60 frames!  The application may be doing too much work on its main thread.

在大部分Android平臺的裝置上,Android系統是16ms重新整理一次,也就是一秒鐘60幀。要達到這種重新整理速度就要求在ui執行緒中處理的任務時間必須要小於16ms,如果ui執行緒中處理時間長,就會導致跳過幀的渲染,也就是導致介面看起來不流暢,卡頓。如果使用者點選事件5s中沒反應就會導致ANR。

幀率

即 Frame Rate,單位 fps,是指 gpu 生成幀的速率,60fps,Android中更幀率相關的類是SurfaceFlinger。

SurfaceFlinger surfaceflinger作用是接受多個來源的圖形顯示資料,將他們合成,然後傳送到顯示裝置。比如開啟應用,常見的有三層顯示,頂部的statusbar底部或者側面的導航欄以及應用的介面,每個層是單獨更新和渲染,這些介面都是有surfaceflinger合成一個重新整理到硬體顯示。

在顯示過程中使用到了bufferqueue,surfaceflinger作為consumer方,比如windowmanager管理的surface作為生產方產生頁面,交由surfaceflinger進行合成。

VSync

Android系統每隔16ms發出VSYNC訊號,觸發對UI進行渲染,VSync是Vertical Synchronization(垂直同步)的縮寫,是一種在PC上很早就廣泛使用的技術,可以簡單的把它認為是一種定時中斷。而在Android 4.1(JB)中已經開始引入VSync機制,用來同步渲染,讓UI和SurfaceFlinger可以按硬體產生的VSync節奏進行工作。

安卓系統中有 2 種 VSync 訊號: 1、螢幕產生的硬體 VSync: 硬體 VSync 是一個脈衝訊號,起到開關或觸發某種操作的作用。 2、由 SurfaceFlinger 將其轉成的軟體 Vsync 訊號:經由 Binder 傳遞給 Choreographer。

除了Vsync的機制,Android還使用了多級緩衝的手段以優化UI流程度,例如雙緩衝(A+B),在顯示buffer A的資料時,CPU/GPU就開始在buffer B中準備下一幀資料:但是不能保證每一幀CPU、GPU都執行狀態良好,可能由於資源搶佔等效能問題導致某一幀GPU掉鏈子,vsync訊號到來時buffer B的資料還沒準備好,而此時Display又在顯示buffer A的資料,導致後面CPU/GPU沒有新的buffer著手準備資料,導致卡頓(jank)。

卡頓原因

從系統層面上看主要以下幾個方面的原因會導致卡頓:

1. SurfaceFlinger 主執行緒耗時

SurfaceFlinger 負責 Surface 的合成 , 一旦 SurfaceFlinger 主執行緒呼叫超時 , 就會產生掉幀 . SurfaceFlinger 主執行緒耗時會也會導致 hwc service 和 crtc 不能及時完成, 也會阻塞應用的 binder 呼叫, 如 dequeueBuffer \ queueBuffer 等.

2. 後臺活動程式太多導致系統繁忙

後臺程式活動太多,會導致系統非常繁忙, cpu \ io \ memory 等資源都會被佔用, 這時候很容易出現卡頓問題 , 這也是系統這邊經常會碰到的問題。 dumpsys cpuinfo 可以檢視一段時間內 cpu 的使用情況:

3.主執行緒排程不到 , 處於 Runnable 狀態

當執行緒為 Runnable 狀態的時候 , 排程器如果遲遲不能對齊進行排程 , 那麼就會產生長時間的 Runnable 執行緒狀態 , 導致錯過 Vsync 而產生流暢性問題。

4、System 鎖

system_server 的 AMS 鎖和 WMS 鎖 , 在系統異常的情況下 , 會變得非常嚴重 , 如下圖所示 , 許多系統的關鍵任務都被阻塞 , 等待鎖的釋放 , 這時候如果有 App 發來的 Binder 請求帶鎖 , 那麼也會進入等待狀態 , 這時候 App 就會產生效能問題 ; 如果此時做 Window 動畫 , 那麼 system_server 的這些鎖也會導致視窗動畫卡頓

5、Layer過多導致 SurfaceFlinger Layer Compute 耗時

Android P 修改了 Layer 的計算方法 , 把這部分放到了 SurfaceFlinger 主執行緒去執行, 如果後臺 Layer 過多, 就會導致 SurfaceFlinger 在執行 rebuildLayerStacks 的時候耗時 , 導致 SurfaceFlinger 主執行緒執行時間過長。

從應用層來看以下會導致卡頓:

1、主執行緒執行時間長 主執行緒執行 Input \ Animation \ Measure \ Layout \ Draw \ decodeBitmap 等操作超時都會導致卡頓 。

  • 1、Measure \ Layout 耗時\超時

  • 2、draw耗時

  • 3、Animation回撥耗時

  • 4、View 初始化耗時

  • 5、List Item 初始化耗時

  • 6、主執行緒運算元據庫

2、主執行緒 Binder 耗時

Activity resume 的時候, 與 AMS 通訊要持有 AMS 鎖, 這時候如果碰到後臺比較繁忙的時候, 等鎖操作就會比較耗時, 導致部分場景因為這個卡頓, 比如多工手勢操作。

3、WebView 效能不足

應用裡面涉及到 WebView 的時候, 如果頁面比較複雜, WebView 的效能就會比較差, 從而造成卡頓

4、幀率與重新整理率不匹配

如果螢幕幀率和系統的 fps 不相符 , 那麼有可能會導致畫面不是那麼順暢. 比如使用 90 Hz 的螢幕搭配 60 fps 的動畫。

卡頓檢測

卡頓檢測可以使用以下多種方法同時進行:
1、使用dumpsys gfxinfo
2、使用Systrace獲取相關資訊
3、使用LayoutInspect 檢測佈局層次
4、使用BlockCanary
5、利用Choreographer。
6、使用嚴格模式(StrictMode )。

1、使用dumpsys gfxinfo

在開發過程中發現有卡頓發生時可以使用下面的命令來獲取卡頓相關的資訊:

 

adb shell dumpsys gfxinfo [PACKAGE_NAME]

輸入這個命令後可能會列印下面的資訊:

Applications Graphics Acceleration Info:
Uptime: 102809662 Realtime: 196891968
** Graphics info for pid 31148 [com.android.settings] **
Stats since: 524615985046231ns
Total frames rendered: 8325
Janky frames: 729 (8.76%)
90th percentile: 13ms
95th percentile: 20ms
99th percentile: 73ms
Number Missed Vsync: 294
Number High input latency: 47
Number Slow UI thread: 502
Number Slow bitmap uploads: 44
Number Slow issue draw commands: 135

上面引數說明:

Graphics info for pid 31148 [com.android.settings]: 表明當前dump的為設定介面的幀資訊,pid為31148 Total frames rendered: 8325 本次dump蒐集了8325幀的資訊

Janky frames :729 (8.76%)出現卡頓的幀數有729幀,佔8.76%

Number Missed Vsync: 294 垂直同步失敗的幀

Number Slow UI thread: 502 因UI執行緒上的工作導致超時的幀數

Number Slow bitmap uploads: 44 因bitmap的載入耗時的幀數

Number Slow issue draw commands: 135 因繪製導致耗時的幀數

2、使用systrace

上面使用的dumpsys是能發現問題或者判斷問題的嚴重性,但無法定位真正的原因。如果要定位原因,應當配合systrace工具使用。

systrace使用

Systrace可以幫助分析應用是如何裝置上執行起來的,它將系統和應用程式執行緒集中在一個共同的時間軸上,分析systrace的第一步需要在程式執行的時間段中抓取trace log,在抓取到的trace檔案中,包含了這段時間中想要的關鍵資訊,互動情況。

圖1顯示的是當一個app在滑動時出現了卡頓的現象,預設的介面下,橫軸是時間,縱向為trace event,trace event 先按程式分組,然後再按執行緒分組.從上到下的資訊分別為Kernel,SurfaceFlinger,應用包名。通過配置trace的分類,可以根據配置情況記錄每個應用程式的所有執行緒資訊以及trace event的層次結構資訊。

Android studio中使用systrace

1、在android裝置的 設定 -- 開發者選項 -- 監控 -- 開啟traces。 2、選擇要追中的類別,並且點選確定。

完成以上配置後,開始抓trace檔案

$ python systrace.py --cpu-freq --cpu-load --time=10 -o mytracefile.html

分析trace檔案 抓到trace.html檔案後,通過web瀏覽器開啟

檢查Frames 每個應用程式都有一排代表渲染幀的圓圈,通常為綠色,如果繪製的時間超過16.6毫秒則顯示黃色或紅色。通過“W”鍵檢視幀。

trace應用程式程式碼 在framework中的trace marker並沒有覆蓋到所有程式碼,因此有些時候需要自己去定義trace marker。在Android4.3之後,可以通過Trace類在程式碼中新增標記,這樣將能夠看到在指定時間內應用的執行緒在做哪些工作,當然,trace 的begin和end操作也會增加一些額外的開銷,但都只有幾微秒左右。 通過下面的例子來說明Trace類的 用法。

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    ...

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Trace.beginSection("MyAdapter.onCreateViewHolder");
        MyViewHolder myViewHolder;
        try {
            myViewHolder = MyViewHolder.newInstance(parent);
        } finally {
            Trace.endSection();
        }
        return myViewHolder;
    }

   @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Trace.beginSection("MyAdapter.onBindViewHolder");
        try {
            try {
                Trace.beginSection("MyAdapter.queryDatabase");
                RowItem rowItem = queryDatabase(position);
                mDataset.add(rowItem);
            } finally {
                Trace.endSection();
            }
            holder.bind(mDataset.get(position));
        } finally {
            Trace.endSection();
        }
    }

…

}

3 、使用BlockCanary

BlockCanary是國內開發者MarkZhai開發的一套效能監控元件,它對主執行緒操作進行了完全透明的監控,並能輸出有效的資訊,幫助開發分析、定位到問題所在,迅速優化應用。 其特點有:
1、非侵入式,簡單的兩行就開啟監控,不需要到處打點,破壞程式碼優雅性。
2、精準,輸出的資訊可以幫助定位到問題所在(精確到行),不需要像Logcat一樣,慢慢去找。
3、目前包括了核心監控輸出檔案,以及UI顯示卡頓資訊功能

BlockCanary基本原理

android應用程式只有一個主執行緒ActivityThread,這個主執行緒會建立一個Looper(Looper.prepare),而Looper又會關聯一個MessageQueue,主執行緒Looper會在應用的生命週期內不斷輪詢(Looper.loop),從MessageQueue取出Message 更新UI。

public static void loop() {
    ...
    for (;;) {
        ...
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        msg.target.dispatchMessage(msg);
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
        ...
    }
}
複製程式碼

BlockCanary主要是檢測msg.target.dispatchMessage(msg);之前的>>>>> Dispatching to 和之後的<<<<< Finished to的間隔時間。 應用發生卡頓,一定是在dispatchMessage中執行了耗時操作。通過給主執行緒的Looper設定一個Printer,打點統計dispatchMessage方法執行的時間,如果超出閥值,表示發生卡頓,則dump出各種資訊,提供開發者分析效能瓶頸。

4、使用Choreographer

Android 主執行緒執行的本質,其實就是 Message 的處理過程,我們的各種操作,包括每一幀的渲染操作 ,都是通過 Message 的形式發給主執行緒的 MessageQueue ,MessageQueue 處理完訊息繼續等下一個訊息。

Choreographer 的引入,主要是配合 Vsync ,給上層 App 的渲染提供一個穩定的 Message 處理的時機,也就是 Vsync 到來的時候 ,系統通過對 Vsync 訊號週期的調整,來控制每一幀繪製操作的時機. 目前大部分手機都是 60Hz 的重新整理率,也就是 16.6ms 重新整理一次,系統為了配合螢幕的重新整理頻率,將 Vsync 的週期也設定為 16.6 ms,每個 16.6 ms , Vsync 訊號喚醒 Choreographer 來做 App 的繪製操作 ,這就是引入 Choreographer 的主要作用。

Choreographer 兩個主要作用

1、承上:負責接收和處理 App 的各種更新訊息和回撥,等到 Vsync 到來的時候統一處理。比如集中處理 Input(主要是 Input 事件的處理) 、Animation(動畫相關)、Traversal(包括 measure、layout、draw 等操作) ,判斷卡頓掉幀情況,記錄 CallBack 耗時等。

2、啟下:負責請求和接收 Vsync 訊號。接收 Vsync 事件回撥(通過 FrameDisplayEventReceiver.onVsync );請求 Vsync(FrameDisplayEventReceiver.scheduleVsync) .

使用Choreographer 計算幀率

Choreographer 處理繪製的邏輯核心在 Choreographer.doFrame 函式中,從下圖可以看到,FrameDisplayEventReceiver.onVsync post 了自己,其 run 方法直接呼叫了 doFrame 開始一幀的邏輯處理:

Choreographer週期性的在UI重繪時候觸發,在程式碼中記錄上一次和下一次繪製的時間間隔,如果超過16ms,就意味著一次UI執行緒重繪的“丟幀”。丟幀的數量為間隔時間除以16,如果超過3,就開始有卡頓的感知。 使用Choreographer檢測幀的程式碼如下:

public class MyFrameCallback implements Choreographer.FrameCallback {
        private String TAG = "效能檢測";
        private long lastTime = 0;

        @Override
        public void doFrame(long frameTimeNanos) {
            if (lastTime == 0) {
                //程式碼第一次初始化。不做檢測統計。
                lastTime = frameTimeNanos;
            } else {
                long times = (frameTimeNanos - lastTime) / 1000000;
                int frames = (int) (times / 16);

                if (times > 16) {
                    Log.w(TAG, "UI執行緒超時(超過16ms):" + times + "ms" + " , 丟幀:" + frames);
                }

                lastTime = frameTimeNanos;
            }

            Choreographer.getInstance().postFrameCallback(mFrameCallback);
        }
    }

卡頓優化

由上面的分析可知物件分配垃圾回收(GC)、執行緒排程以及Binder呼叫 是Android系統中常見的卡頓原因,因此卡頓優化主要以下幾種方法,更多的要結合具體的應用來進行:

1、佈局優化

  • 通過減少冗餘或者巢狀佈局來降低檢視層次結構。比如使用約束佈局代替線性佈局和相對佈局。
  • 用 ViewStub 替代在啟動過程中不需要顯示的 UI 控制元件。
  • 使用自定義 View 替代複雜的 View 疊加。

2、減少主執行緒耗時操作

  • 主執行緒中不要直接運算元據庫,資料庫的操作應該放在資料庫執行緒中完成。
  • sharepreference儘量使用apply,少使用commit,可以使用MMKV框架來代替sharepreference。
  • 網路請求回來的資料解析儘量放在子執行緒中,不要在主執行緒中進行復制的資料解析操作。
  • 不要在activity的onResume和onCreate中進行耗時操作,比如大量的計算等。

3、減少過度繪製 過度繪製是同一個畫素點上被多次繪製,減少過度繪製一般減少佈局背景疊加等方式,如下圖所示右邊是過度繪製的圖片。

4、列表優化

  • RecyclerView使用優化,使用DiffUtil和notifyItemDataSetChanged進行區域性更新等。

5、物件分配和回收優化

自從Android引入 ART 並且在Android 5.0上成為預設的執行時之後,物件分配和垃圾回收(GC)造成的卡頓已經顯著降低了,但是由於物件分配和GC有額外的開銷,它依然又可能使執行緒負載過重。 在一個呼叫不頻繁的地方(比如按鈕點選)分配物件是沒有問題的,但如果在在一個被頻繁呼叫的緊密的迴圈裡,就需要避免物件分配來降低GC的壓力。

  • 減少小物件的頻繁分配和回收操作。

相關文章