TraceView 是 Android 平臺特有的數據採集和分析工具,它主要用於分析 Android 中應用程序的 hotspot。TraceView 自己只是一個數據分析工具,而數據的採集則須要使用 Android SDK 中的 Debug 類或者利用 DDMS 工具。兩者的用法以下:java
1)開發者在一些關鍵代碼段開始前調用 Android SDK 中 Debug 類的 startMethodTracing 函數,並在關鍵代碼段結束前調用 stopMethodTracing 函數。這兩個函數運行過程當中將採集運行時間內該應用全部線程(注意,只能是 Java 線程)的函數執行狀況,並將採集數據保存到 /mnt/sdcard/ 下的一個文件中。開發者而後須要利用 SDK 中的 TraceView 工具來分析這些數據。android
2)藉助 Android SDK 中的 DDMS 工具。DDMS 可採集系統中某個正在運行的進程的函數調用信息。對開發者而言,此方法適用於沒有目標應用源代碼的狀況。shell
DDMS 中 TraceView 使用示意圖以下,調試人員能夠經過選擇 Devices 中的應用後點擊 按鈕 Start Method Profiling(開啓方法分析)和點擊 Stop Method Profiling(中止方法分析)緩存
開啓方法分析後對應用的目標頁面進行測試操做,測試完畢後中止方法分析,界面會跳轉到 DDMS 的 trace 分析界面,以下圖所示:網絡
TraceView 界面比較複雜,其 UI 劃分爲上下兩個面板,即 Timeline Panel(時間線面板)和 Profile Panel(分析面板)。上圖中的上半部分爲 Timeline Panel(時間線面板),Timeline Panel 又可細分爲左右兩個 Pane:app
1)左邊 Pane 顯示的是測試數據中所採集的線程信息。由圖可知,本次測試數據採集了 main 線程,傳感器線程和其它系統輔助線程的信息。ide
2)右邊 Pane 所示爲時間線,時間線上是每一個線程測試時間段內所涉及的函數調用信息。這些信息包括函數名、函數執行時間等。由圖可知,Thread-1412 線程對應行的的內容很是豐富,而其餘線程在這段時間內幹得工做則要少得多。函數
3)另外,開發者能夠在時間線 Pane 中移動時間線縱軸。縱軸上邊將顯示當前時間點中某線程正在執行的函數信息。工具
上圖中的下半部分爲 Profile Panel(分析面板),Profile Panel 是 TraceView 的核心界面,其內涵很是豐富。它主要展現了某個線程(先在 Timeline Panel 中選擇線程)中各個函數調用的狀況,包括 CPU 使用時間、調用次數等信息。而這些信息正是查找 hotspot 的關鍵依據。因此,對開發者而言,必定要了解 Profile Panel 中各列的含義。下表列出了 Profile Panel 中比較重要的列名及其描述。oop
注意:
對於Android 1.5及如下的版本:不支持。
對於Android 1.5以上2.1下(含2.1)的版本:受限支持。trace文件只能生成到SD卡,且必須在程序中加入代碼。
對於Android 2.2上(含2.2)的版本:全支持。能夠不用SD卡,不用在程序中加代碼,直接本身用DDMS就能夠進程Traceview。
有兩方面用途:
1 查看跟蹤代碼的執行時間,分析哪些是耗時操做
2 能夠用於跟蹤方法的調用,尤爲是Android Framework層的方法調用關係
獲取方法的調用順序
1. 在traceview中搜索響應的方法名不能使用大寫字母
2. 搜索出的方法會自動展開,其中包含Parents 和 Children 兩組信息
3. 點擊Parents下的方法名,直接跳轉到調用當前的方法處。Children相反
Traceview 面板分上下兩部分:
1)上面是時間軸面板 (Timeline Panel)
左側顯示的是線程信息
右側黑色部分是顯示執行時間段、白色是線程暫停時間段,
右側鼠標放在上面會出現時間線縱軸,在頂部會顯示當前時間線所執行的具體函數信息
2)下面是分析面板(Profile Panel) - 每一列內容
Inclusive time - 函數自己運行花費時間 + 函數調用其餘函數時間
Exclusive time - 函數自己運行花費時間。
Calls + RecurCall/Total 調用 + 重複調用次數 / 函數總調用次數
Cpu Time/Call 總的Cpu時間與總的調用次數之比
瞭解完 TraceView 的 UI 後,如今介紹如何利用 TraceView 來查找 hotspot。通常而言,hotspot 包括兩種類型的函數:
1)一類是調用次數很少,但每次調用卻須要花費很長時間的函數。
2)一類是那些自身佔用時間不長,但調用卻很是頻繁的函數。
測試背景:APP 在測試機運行一段時間後出現手機發燙、卡頓、高 CPU 佔有率的現象。將應用切入後臺進行 CPU 數據的監測,結果顯示,即便應用不進行任何操做,應用的 CPU 佔有率都會持續的增加。
按照 TraceView 簡介中的方法進行測試,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() 方法出現了問題,下面找到這個方法進行代碼上的驗證:
package com.sunzn.app.utils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.HashMap; import android.content.Context; import android.graphics.Bitmap; import android.os.Environment; import android.os.Handler; import android.os.Message; public class ImageLoaderTools { private HttpTools httptool; private Context mContext; private boolean isLoop = true; private HashMap<String, SoftReference<Bitmap>> mHashMap_caches; private ArrayList<ImageLoadTask> maArrayList_taskQueue; private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { ImageLoadTask loadTask = (ImageLoadTask) msg.obj; loadTask.callback.imageloaded(loadTask.path, loadTask.bitmap); }; }; private Thread mThread = new Thread() { public void run() { while (isLoop) { while (maArrayList_taskQueue.size() > 0) { try { ImageLoadTask task = maArrayList_taskQueue.remove(0); if (Constant.LOADPICTYPE == 1) { byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET); task.bitmap = BitMapTools.getBitmap(bytes, 40, 40); } else if (Constant.LOADPICTYPE == 2) { InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET); task.bitmap = BitMapTools.getBitmap(in, 1); } if (task.bitmap != null) { mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap)); File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); if (!dir.exists()) { dir.mkdirs(); } String[] path = task.path.split("/"); String filename = path[path.length - 1]; File file = new File(dir, filename); BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap); Message msg = Message.obtain(); msg.obj = task; mHandler.sendMessage(msg); } } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } synchronized (this) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }; }; public ImageLoaderTools(Context context) { this.mContext = context; httptool = new HttpTools(context); mHashMap_caches = new HashMap<String, SoftReference<Bitmap>>(); maArrayList_taskQueue = new ArrayList<ImageLoaderTools.ImageLoadTask>(); mThread.start(); } private class ImageLoadTask { String path; Bitmap bitmap; Callback callback; } public interface Callback { void imageloaded(String path, Bitmap bitmap); } public void quit() { isLoop = false; } public Bitmap imageLoad(String path, Callback callback) { Bitmap bitmap = null; String[] path1 = path.split("/"); String filename = path1[path1.length - 1]; if (mHashMap_caches.containsKey(path)) { bitmap = mHashMap_caches.get(path).get(); if (bitmap == null) { mHashMap_caches.remove(path); } else { return bitmap; } } File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); File file = new File(dir, filename); bitmap = BitMapTools.getBitMap(file.getAbsolutePath()); if (bitmap != null) { return bitmap; } ImageLoadTask task = new ImageLoadTask(); task.path = path; task.callback = callback; maArrayList_taskQueue.add(task); synchronized (mThread) { mThread.notify(); } return null; } }
以上代碼便是 ImageLoaderTools 圖片工具類的所有代碼,先不着急去研究這個類的代碼實現過程,先來看看這個類是怎麼被調用的:
ImageLoaderTools imageLoaderTools = imageLoaderTools = new ImageLoaderTools(this); Bitmap bitmap = imageLoaderTools.imageLoad(picpath, new Callback() { @Override public void imageloaded(String picPath, Bitmap bitmap) { if (bitmap == null) { imageView.setImageResource(R.drawable.default); } else { imageView.setImageBitmap(bitmap); } } }); if (bitmap == null) { imageView.setImageResource(R.drawable.fengmianmoren); } else { imageView.setImageBitmap(bitmap); }
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 循環中,這樣重複加載同一張圖片時,死循環一出現線程就被掛起,這樣就能夠避免死循環的出現。代碼以下:
private Thread mThread = new Thread() { public void run() { while (isLoop) { while (maArrayList_taskQueue.size() > 0) { try { ImageLoadTask task = maArrayList_taskQueue.remove(0); if (Constant.LOADPICTYPE == 1) { byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET); task.bitmap = BitMapTools.getBitmap(bytes, 40, 40); } else if (Constant.LOADPICTYPE == 2) { InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET); task.bitmap = BitMapTools.getBitmap(in, 1); } if (task.bitmap != null) { mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap)); File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); if (!dir.exists()) { dir.mkdirs(); } String[] path = task.path.split("/"); String filename = path[path.length - 1]; File file = new File(dir, filename); BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap); Message msg = Message.obtain(); msg.obj = task; mHandler.sendMessage(msg); } } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } synchronized (this) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }; };
最後再附上代碼修改後代碼運行的性能圖,和以前的屢次被重複執行,效率有了質的提高,手機發燙、卡頓、高 CPU 佔用率的現象也消失了。
附加:
若是想更精確到方法可使用這種方法,首先,必須在程序當中加入代碼
Debug.startMethodTracing("love_world_"); Debug.stopMethodTracing();
以便生成trace文件,有了這個trace文件咱們才能夠將其轉化爲圖形。
1)啓動追蹤
使用Debug的如下靜態方法方法來啓動:
static void startMethodTracing(String traceName)
使用指定trace文件的名字和默認最大容量(8M)的方式開始方法的追蹤
static void startMethodTracing()
使用默認trace文件的名字(dmtrace.trace)和默認最大容量(8M)的方式開始方法的追蹤
static void startMethodTracing(String traceName, int bufferSize, int flags)
使用指定trace文件的名字和最大容量的方式開始方法的追蹤。並可指定flags.
注:int flags好像沒意義。通常都用0.
static void startMethodTracing(String traceName, int bufferSize)
使用指定trace文件的名字和最大容量的方式開始方法的追蹤。
注1:以上的方法的文件都會建立於SD卡下,即"/sdcard/"下,對默認文件名的就是"/sdcard/dmtrace.trace"
若是沒SD卡,以上方法會拋異常導致程序crash.
注2:若是文件名沒有指定類型,系統爲其加上類型.trace
1)中止追蹤
使用Debug的靜態方法方法來中止:
public static void stopMethodTracing ()
例如,onCreate與onStart方法之間方法跟蹤
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Debug.startMethodTracing("Love_World_"); } @Override protected void onStart() { super.onStart(); Debug.stopMethodTracing(); } }
添加SD卡訪問權限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
若是不添加,執行項目會出現如下異常:
java.lang.RuntimeException:Unable to open trace file '/mnt/sdcard/Love_World_.trace': Permission denied
若是手機沒有SD卡也會出現一樣的問題。
導出traceview文件
1)首先執行項目,查看trace文件是否生成
進入shell模式。
adb shell
查看是否已經生成這個文件。
ls sdcard/Love_World_.trace
Ctrl + C 退出adb shell模式。
2)導出trace文件
adb pull sdcard/Love_World_.trace
打開trace文件:
打開trace文件須要Android提供的traceview.bat工具,工具所在目錄:sdk\tools\traceview.bat, 有兩種方式執行:
1) 在命令行中切換到此目錄。
2) 將此目錄添加到系統環境變量中。
// cmd在calc.trace所在目錄執行 traceview C:\Users\YourName\Desktop\Love_World_.trace
其中「C:\Users\YourName\Desktop\」 表示trace所在你係統中的目錄,此工具須要輸入trace文件的絕對路徑才行。
在新版本的SDK 會有如下提示:
The standalone version of traceview is deprecated.
Please use Android Device Monitor (tools/monitor) instead.
因此建議使用tools/monitor 啓動後跟Eclipse DDMS界面差很少,而後File -> Open File -> 選擇trace文件。
異常處理:
'C:\Windows\system32\java.exe' 不是內部或外部命令,也不是可運行的程序 或批處理文件。 SWT folder '' does not exist. Please set ANDROID_SWT to point to the folder containing swt.jar for your platfo rm.
配置Java環境變量,把java bin 添加到系統環境變量PATH中。
異常信息:
The standalone version of traceview is deprecated. Please use Android Device Monitor (tools/monitor) instead. Failed to read the trace filejava.io.IOException: Key section does not have an * end marker at com.android.traceview.DmTraceReader.parseKeys(DmTraceReader.java:420) at com.android.traceview.DmTraceReader.generateTrees(DmTraceReader.java:91) at com.android.traceview.DmTraceReader.<init>(DmTraceReader.java:87) at com.android.traceview.MainWindow.main(MainWindow.java:286)
一般是trace文件有異常,再從新生成並導出試試。
沒有SD卡會出現異常:
Unable to open trace file '/sdcard/Love_World_.trace': Permission denied Caused by: java.lang.RuntimeException: Unable to open trace file '/sdcard/Love_World_.trace': Permission denied
生成的trace系統自動放在SDCARD上,沒有sd卡因此會出現這種異常。