前言
在對手機應用效能分析和定位的過程中Traceview 是使用最多的一個工具
;在遇到啟動時間長介面切換時間長特別卡頓的時候 Traceview 是首選工具;如果檢視介面的幀率問題建議還是先使用 GPU 配置檔案以列表的形式展示在螢幕上這樣可以首先發現這個介面的幀率是否有問題再做後續的排查
TraceView 定義
TraceView 是 Android 平臺特有的資料採集和分析工具,它主要用於分析 Android 中應用程式的 hotspot
;TraceView 本身只是一個數據分析工具,而資料的採集則需要使用 Android SDK 中的 Debug 類或者利用 DDMS 工具
二者的用法如下:
開發者在一些關鍵程式碼段開始前呼叫 Android SDK 中 Debug 類的 startMethodTracing 函式,並在關鍵程式碼段結束前呼叫 stopMethodTracing 函式
;這兩個函式執行過程中將採集執行時間內該應用所有執行緒(注意,只能是 Java 執行緒)的函式執行情況,並將採集資料儲存到 /mnt/sdcard/ 下的一個檔案中。開發者然後需要利用 SDK 中的 TraceView 工具來分析這些資料
藉助 Android SDK 中的 DDMS 工具
;DDMS 可採集系統中某個正在執行的程序的函式呼叫資訊。對開發者而言,此方法適用於沒有目標應用原始碼的情況
TraceView 如何使用
DDMS 中 TraceView 使用示意圖如下
,除錯人員可以透過選擇 Devices 中的應用後點擊 按鈕 Start Method Profiling(開啟方法分析)和點選 Stop Method Profiling(停止方法分析)
開啟方法分析後對應用的目標頁面進行測試操作,測試完畢後停止方法分析,介面會跳轉到 DDMS 的 trace 分析介面
如下圖所示:
TraceView 介面比較複雜,其 UI 劃分為上下兩個面板,即 Timeline Panel(時間線面板)和 Profile Panel(分析面板)
;上圖中的上半部分為 Timeline Panel(時間線面板),Timeline Panel 又可細分為左右兩個 Pane:
左邊 Pane 顯示的是測試資料中所採集的執行緒資訊。由圖可知,本次測試資料採集了 main 執行緒,感測器執行緒和其它系統輔助執行緒的資訊。
右邊 Pane 所示為時間線,時間線上是每個執行緒測試時間段內所涉及的函式呼叫資訊。這些資訊包括函式名、函式執行時間等。由圖可知,Thread-1412 執行緒對應行的的內容非常豐富,而其他執行緒在這段時間內幹得工作則要少得多。
另外,開發者可以在時間線 Pane 中移動時間線縱軸。縱軸上邊將顯示當前時間點中某執行緒正在執行的函式資訊。
上圖中的下半部分為 Profile Panel(分析面板),Profile Panel 是 TraceView 的核心介面,其內涵非常豐富
;它主要展示了某個執行緒(先在 Timeline Panel 中選擇執行緒)中各個函式呼叫的情況,包括 CPU 使用時間、呼叫次數等資訊。而這些資訊正是查詢 hotspot 的關鍵依據
所以,對開發者而言,一定要了解 Profile Panel 中各列的含義
;下表列出了 Profile Panel 中比較重要的列名及其描述
TraceView 實戰
瞭解完 TraceView 的 UI 後,現在介紹如何利用 TraceView 來查詢 hotspot。
一般而言,hotspot 包括兩種型別的函式:
一類是呼叫次數不多,但每次呼叫卻需要花費很長時間的函式。
一類是那些自身佔用時間不長,但呼叫卻非常頻繁的函式。
測試背景
APP 在測試機執行一段時間後出現手機發燙、卡頓、高 CPU 佔有率的現象
。將應用切入後臺進行 CPU 資料的監測,結果顯示,即使應用不進行任何操作,應用的 CPU 佔有率都會持續的增長。
TraceView 結果 UI 顯示後進行資料分析,在 Profile Panel 中
,選擇按 Cpu Time/Call 進行降序排序(從上之下排列,每項的耗費時間由高到低)得到如圖所示結果:
圖中 ImageLoaderTools$2。run() 是應用程式中的函式,它耗時為 1111。124。然後點選 ImageLoaderTools$2。run() 項,得到更為詳盡的呼叫關係圖:
上圖中 Parents 為 ImageLoaderTools$2。run() 方法的呼叫者:Parents (the methods calling this method);Children 為 ImageLoaderTools$2。run() 呼叫的子函式或方法:Children (the methods called by this method)
本例中 ImageLoaderTools$2。run() 方法的呼叫者為 Framework 部分,而 ImageLoaderTools$2。run() 方法呼叫的自方法中我們卻發現有三個方法的 Incl Cpu Time % 佔用均達到了 14% 以上,更離譜的是 Calls+RecurCalls/Total 顯示這三個方法均被呼叫了 35000 次以上,從包名可以識別出這些方法為測試者自身所實現
由此可以判斷 ImageLoaderTools$2。run() 極有可能是手機發燙、卡頓、高 CPU 佔用率的原因所在
程式碼驗證
大致可以判斷是 ImageLoaderTools$2。run() 方法出現了問題,下面找到這個方法進行程式碼上的驗證:
1 package com。sunzn。app。utils; 2 3 import java。io。File; 4 import java。io。IOException; 5 import java。io。InputStream; 6 import java。lang。ref。SoftReference; 7 import java。util。ArrayList; 8 import java。util。HashMap; 9 10 import android。content。Context; 11 import android。graphics。Bitmap; 12 import android。os。Environment; 13 import android。os。Handler; 14 import android。os。Message; 15 16 public class ImageLoaderTools { 17 18 private HttpTools httptool; 19 20 private Context mContext; 21 22 private boolean isLoop = true; 23 24 private HashMap
以上程式碼即是 ImageLoaderTools 圖片工具類的全部程式碼,先不著急去研究這個類的程式碼實現過程,先來看看這個類是怎麼被呼叫的:
1 ImageLoaderTools imageLoaderTools = imageLoaderTools = new ImageLoaderTools(this); 2 3 Bitmap bitmap = imageLoaderTools。imageLoad(picpath, new Callback() { 4 5 @Override 6 public void imageloaded(String picPath, Bitmap bitmap) { 7 if (bitmap == null) { 8 imageView。setImageResource(R。drawable。default); 9 } else {10 imageView。setImageBitmap(bitmap);11 }12 }13 });14 15 if (bitmap == null) {16 imageView。setImageResource(R。drawable。fengmianmoren);17 } else {18 imageView。setImageBitmap(bitmap);19 }
ImageLoaderTools 被呼叫的過程非常簡單:
1。ImageLoaderTools 例項化;
2。執行 imageLoad() 方法載入圖片。
在 ImageLoaderTools 類的建構函式
(90行-96行)進行例項化過程中完成了網路工具 HttpTools 初始化、新建一個圖片快取 Map、新建一個下載佇列、開啟下載執行緒的操作
這時候請注意開啟執行緒的操作
,開啟執行緒後執行 run() 方法(35行-88行),這時 isLoop 的值是預設的 true,maArrayList_taskQueue。size() 是為 0 的,在任務佇列 maArrayList_taskQueue 中還沒有加入下載任務之前這個迴圈會一直迴圈下去
在執行 imageLoad() 方法載入圖片時會首先去快取 mHashMap_caches 中查詢該圖片是否已經被下載過,如果已經下載過則直接返回與之對應的 bitmap 資源,如果沒有查詢到則會往 maArrayList_taskQueue 中新增下載任務並喚醒對應的下載執行緒,之前開啟的執行緒在發現 maArrayList_taskQueue。size() > 0 後就進入下載邏輯,下載完任務完成後將對應的圖片資源加入快取 mHashMap_caches 並更新 UI,下載執行緒執行 wait() 方法被掛起
一個圖片下載的業務邏輯這樣理解起來很順暢,似乎沒有什麼問題。開始我也這樣認為,但後來在仔細的分析程式碼的過程中發現如果同樣一張圖片資源重新被載入就會出現死迴圈
還記得快取 mHashMap_caches 麼?
如果一張圖片之前被下載過,那麼快取中就會有這張圖片的引用存在
重新去載入這張圖片的時候如果重複的去初始化 ImageLoaderTools,執行緒會被開啟,而使用 imageLoad() 方法載入圖片時發現快取中存在這個圖片資源,則會將其直接返回
注意這裡使用的是 return bitmap; 那就意味著 imageLoad() 方法裡新增下載任務到下載佇列的程式碼不會被執行到
這時候 run() 方法中的 isLoop = true 並且 maArrayList_taskQueue。size() = 0,這樣內層 while 裡的邏輯也就是掛起執行緒的關鍵程式碼 wait() 永遠不會被執行到,而外層 while 的判斷條件一直為 true,就這樣程式出現了死迴圈。
死迴圈才是手機發燙、卡頓、高 CPU 佔用率的真正原因所在
解決方案
準確的定位到程式碼問題所在後,提出解決方案就很簡單了,這裡提供的解決方案是將 wait() 方法從內層 while 迴圈提到外層 while 迴圈中,這樣重複載入同一張圖片時,死迴圈一出現執行緒就被掛起,這樣就可以避免死迴圈的出現。程式碼如下:
1 private Thread mThread = new Thread() { 2 3 public void run() { 4 5 while (isLoop) { 6 7 while (maArrayList_taskQueue。size() > 0) { 8 9 try {10 ImageLoadTask task = maArrayList_taskQueue。remove(0);11 12 if (Constant。LOADPICTYPE == 1) {13 byte[] bytes = httptool。getByte(task。path, null, HttpTools。METHOD_GET);14 task。bitmap = BitMapTools。getBitmap(bytes, 40, 40);15 } else if (Constant。LOADPICTYPE == 2) {16 InputStream in = httptool。getStream(task。path, null, HttpTools。METHOD_GET);17 task。bitmap = BitMapTools。getBitmap(in, 1);18 }19 20 if (task。bitmap != null) {21 mHashMap_caches。put(task。path, new SoftReference
最後再附上程式碼修改後程式碼執行的效能圖,和之前的多次被重複執行,效率有了質的提升,手機發燙、卡頓、高 CPU 佔用率的現象也消失了
總結
文章所提的功能只是羅列了
TraceView 工具的部分用法
,有需要文中
完整程式碼
或者
更多 Android 相關學習資料
的小夥伴可以;可以
私信
傳送
"Android 進階"
,即可獲取一份
Android 系列性技術學習手冊
;希望這份手冊能夠給大家學習 Android 帶來一些幫助
好了,以上就是今天要分享的內容,大家覺得有用的話,可以點贊分享一下;如果文章中有什麼問題歡迎大家指正;歡迎在評論區或後臺討論哈~