Android 編程下的 TraceView 簡介及其案例實(shí)戰(zhàn)
TraceView 是 Android 平臺配備一個(gè)很好的性能分析的工具。它可以通過圖形化的方式讓我們了解我們要跟蹤的程序的性能,并且能具體到 method。詳細(xì)內(nèi)容參考:Profiling with Traceview and dmtracedump
TraceView 簡介
TraceView 是 Android 平臺特有的數(shù)據(jù)采集和分析工具,它主要用于分析 Android 中應(yīng)用程序的 hotspot。TraceView 本身只是一個(gè)數(shù)據(jù)分析工具,而數(shù)據(jù)的采集則需要使用 Android SDK 中的 Debug 類或者利用 DDMS 工具。二者的用法如下:
- 開發(fā)者在一些關(guān)鍵代碼段開始前調(diào)用 Android SDK 中 Debug 類的 startMethodTracing 函數(shù),并在關(guān)鍵代碼段結(jié)束前調(diào)用 stopMethodTracing 函數(shù)。這兩個(gè)函數(shù)運(yùn)行過程中將采集運(yùn)行時(shí)間內(nèi)該應(yīng)用所有線程(注意,只能是 Java 線程)的函數(shù)執(zhí)行情況,并將采集數(shù)據(jù)保存到 /mnt/sdcard/ 下的一個(gè)文件中。開發(fā)者然后需要利用 SDK 中的 TraceView 工具來分析這些數(shù)據(jù)。
- 借助 Android SDK 中的 DDMS 工具。DDMS 可采集系統(tǒng)中某個(gè)正在運(yùn)行的進(jìn)程的函數(shù)調(diào)用信息。對開發(fā)者而言,此方法適用于沒有目標(biāo)應(yīng)用源代碼的情況。
DDMS 中 TraceView 使用示意圖如下,調(diào)試人員可以通過選擇 Devices 中的應(yīng)用后點(diǎn)擊
按鈕 Start Method Profiling(開啟方法分析)和點(diǎn)擊
Stop Method Profiling(停止方法分析)

開啟方法分析后對應(yīng)用的目標(biāo)頁面進(jìn)行測試操作,測試完畢后停止方法分析,界面會(huì)跳轉(zhuǎn)到 DDMS 的 trace 分析界面,如下圖所示:

TraceView 界面比較復(fù)雜,其 UI 劃分為上下兩個(gè)面板,即 Timeline Panel(時(shí)間線面板)和 Profile Panel(分析面板)。上圖中的上半部分為 Timeline Panel(時(shí)間線面板),Timeline Panel 又可細(xì)分為左右兩個(gè) Pane:
- 左邊 Pane 顯示的是測試數(shù)據(jù)中所采集的線程信息。由圖可知,本次測試數(shù)據(jù)采集了 main 線程,傳感器線程和其它系統(tǒng)輔助線程的信息。
- 右邊 Pane 所示為時(shí)間線,時(shí)間線上是每個(gè)線程測試時(shí)間段內(nèi)所涉及的函數(shù)調(diào)用信息。這些信息包括函數(shù)名、函數(shù)執(zhí)行時(shí)間等。由圖可知,Thread-1412 線程對應(yīng)行的的內(nèi)容非常豐富,而其他線程在這段時(shí)間內(nèi)干得工作則要少得多。
- 另外,開發(fā)者可以在時(shí)間線 Pane 中移動(dòng)時(shí)間線縱軸。縱軸上邊將顯示當(dāng)前時(shí)間點(diǎn)中某線程正在執(zhí)行的函數(shù)信息。
上圖中的下半部分為 Profile Panel(分析面板),Profile Panel 是 TraceView 的核心界面,其內(nèi)涵非常豐富。它主要展示了某個(gè)線程(先在 Timeline Panel 中選擇線程)中各個(gè)函數(shù)調(diào)用的情況,包括 CPU 使用時(shí)間、調(diào)用次數(shù)等信息。而這些信息正是查找 hotspot 的關(guān)鍵依據(jù)。所以,對開發(fā)者而言,一定要了解 Profile Panel 中各列的含義。下表列出了 Profile Panel 中比較重要的列名及其描述。

TraceView 實(shí)戰(zhàn)
了解完 TraceView 的 UI 后,現(xiàn)在介紹如何利用 TraceView 來查找 hotspot。一般而言,hotspot 包括兩種類型的函數(shù):
- 一類是調(diào)用次數(shù)不多,但每次調(diào)用卻需要花費(fèi)很長時(shí)間的函數(shù)。
- 一類是那些自身占用時(shí)間不長,但調(diào)用卻非常頻繁的函數(shù)。
測試背景:APP 在測試機(jī)運(yùn)行一段時(shí)間后出現(xiàn)手機(jī)發(fā)燙、卡頓、高 CPU 占有率的現(xiàn)象。將應(yīng)用切入后臺進(jìn)行 CPU 數(shù)據(jù)的監(jiān)測,結(jié)果顯示,即使應(yīng)用不進(jìn)行任何操作,應(yīng)用的 CPU 占有率都會(huì)持續(xù)的增長。
按照 TraceView 簡介中的方法進(jìn)行測試,TraceView 結(jié)果 UI 顯示后進(jìn)行數(shù)據(jù)分析,在 Profile Panel 中,選擇按 Cpu Time/Call 進(jìn)行降序排序(從上之下排列,每項(xiàng)的耗費(fèi)時(shí)間由高到低)得到如圖所示結(jié)果:

圖中 ImageLoaderTools$2.run() 是應(yīng)用程序中的函數(shù),它耗時(shí)為 1111.124。然后點(diǎn)擊 ImageLoaderTools$2.run() 項(xiàng),得到更為詳盡的調(diào)用關(guān)系圖:

上圖中 Parents 為 ImageLoaderTools$2.run() 方法的調(diào)用者:Parents (the methods calling this method);Children 為 ImageLoaderTools$2.run() 調(diào)用的子函數(shù)或方法:Children (the methods called by this method)。本例中 ImageLoaderTools$2.run() 方法的調(diào)用者為 Framework 部分,而 ImageLoaderTools$2.run() 方法調(diào)用的自方法中我們卻發(fā)現(xiàn)有三個(gè)方法的 Incl Cpu Time % 占用均達(dá)到了 14% 以上,更離譜的是 Calls+RecurCalls/Total 顯示這三個(gè)方法均被調(diào)用了 35000 次以上,從包名可以識別出這些方法為測試者自身所實(shí)現(xiàn),由此可以判斷 ImageLoaderTools$2.run() 極有可能是手機(jī)發(fā)燙、卡頓、高 CPU 占用率的原因所在。
代碼驗(yàn)證
大致可以判斷是 ImageLoaderTools$2.run() 方法出現(xiàn)了問題,下面找到這個(gè)方法進(jìn)行代碼上的驗(yàn)證:
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<String, SoftReference<Bitmap>> mHashMap_caches; 25 26 private ArrayList<ImageLoadTask> maArrayList_taskQueue; 27 28 private Handler mHandler = new Handler() { 29 public void handleMessage(android.os.Message msg) { 30 ImageLoadTask loadTask = (ImageLoadTask) msg.obj; 31 loadTask.callback.imageloaded(loadTask.path, loadTask.bitmap); 32 }; 33 }; 34 35 private Thread mThread = new Thread() { 36 37 public void run() { 38 39 while (isLoop) { 40 41 while (maArrayList_taskQueue.size() > 0) { 42 43 try { 44 ImageLoadTask task = maArrayList_taskQueue.remove(0); 45 46 if (Constant.LOADPICTYPE == 1) { 47 byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET); 48 task.bitmap = BitMapTools.getBitmap(bytes, 40, 40); 49 } else if (Constant.LOADPICTYPE == 2) { 50 InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET); 51 task.bitmap = BitMapTools.getBitmap(in, 1); 52 } 53 54 if (task.bitmap != null) { 55 mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap)); 56 File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); 57 if (!dir.exists()) { 58 dir.mkdirs(); 59 } 60 String[] path = task.path.split("/"); 61 String filename = path[path.length - 1]; 62 File file = new File(dir, filename); 63 BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap); 64 Message msg = Message.obtain(); 65 msg.obj = task; 66 mHandler.sendMessage(msg); 67 } 68 } catch (IOException e) { 69 e.printStackTrace(); 70 } catch (Exception e) { 71 e.printStackTrace(); 72 } 73 74 synchronized (this) { 75 try { 76 wait(); 77 } catch (InterruptedException e) { 78 e.printStackTrace(); 79 } 80 } 81 82 } 83 84 } 85 86 }; 87 88 }; 89 90 public ImageLoaderTools(Context context) { 91 this.mContext = context; 92 httptool = new HttpTools(context); 93 mHashMap_caches = new HashMap<String, SoftReference<Bitmap>>(); 94 maArrayList_taskQueue = new ArrayList<ImageLoaderTools.ImageLoadTask>(); 95 mThread.start(); 96 } 97 98 private class ImageLoadTask { 99 String path; 100 Bitmap bitmap; 101 Callback callback; 102 } 103 104 public interface Callback { 105 void imageloaded(String path, Bitmap bitmap); 106 } 107 108 public void quit() { 109 isLoop = false; 110 } 111 112 public Bitmap imageLoad(String path, Callback callback) { 113 Bitmap bitmap = null; 114 String[] path1 = path.split("/"); 115 String filename = path1[path1.length - 1]; 116 117 if (mHashMap_caches.containsKey(path)) { 118 bitmap = mHashMap_caches.get(path).get(); 119 if (bitmap == null) { 120 mHashMap_caches.remove(path); 121 } else { 122 return bitmap; 123 } 124 } 125 126 File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); 127 128 File file = new File(dir, filename); 129 130 bitmap = BitMapTools.getBitMap(file.getAbsolutePath()); 131 if (bitmap != null) { 132 return bitmap; 133 } 134 135 ImageLoadTask task = new ImageLoadTask(); 136 task.path = path; 137 task.callback = callback; 138 maArrayList_taskQueue.add(task); 139 140 synchronized (mThread) { 141 mThread.notify(); 142 } 143 144 return null; 145 } 146 147 }
以上代碼即是 ImageLoaderTools 圖片工具類的全部代碼,先不著急去研究這個(gè)類的代碼實(shí)現(xiàn)過程,先來看看這個(gè)類是怎么被調(diào)用的:
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 被調(diào)用的過程非常簡單:1.ImageLoaderTools 實(shí)例化;2.執(zhí)行 imageLoad() 方法加載圖片。
在 ImageLoaderTools 類的構(gòu)造函數(shù)(90行-96行)進(jìn)行實(shí)例化過程中完成了網(wǎng)絡(luò)工具 HttpTools 初始化、新建一個(gè)圖片緩存 Map、新建一個(gè)下載隊(duì)列、開啟下載線程的操作。這時(shí)候請注意開啟線程的操作,開啟線程后執(zhí)行 run() 方法(35行-88行),這時(shí) isLoop 的值是默認(rèn)的 true,maArrayList_taskQueue.size() 是為 0 的,在任務(wù)隊(duì)列 maArrayList_taskQueue 中還沒有加入下載任務(wù)之前這個(gè)循環(huán)會(huì)一直循環(huán)下去。在執(zhí)行 imageLoad() 方法加載圖片時(shí)會(huì)首先去緩存 mHashMap_caches 中查找該圖片是否已經(jīng)被下載過,如果已經(jīng)下載過則直接返回與之對應(yīng)的 bitmap 資源,如果沒有查找到則會(huì)往 maArrayList_taskQueue 中添加下載任務(wù)并喚醒對應(yīng)的下載線程,之前開啟的線程在發(fā)現(xiàn) maArrayList_taskQueue.size() > 0 后就進(jìn)入下載邏輯,下載完任務(wù)完成后將對應(yīng)的圖片資源加入緩存 mHashMap_caches 并更新 UI,下載線程執(zhí)行 wait() 方法被掛起。一個(gè)圖片下載的業(yè)務(wù)邏輯這樣理解起來很順暢,似乎沒有什么問題。開始我也這樣認(rèn)為,但后來在仔細(xì)的分析代碼的過程中發(fā)現(xiàn)如果同樣一張圖片資源重新被加載就會(huì)出現(xiàn)死循環(huán)。還記得緩存 mHashMap_caches 么?如果一張圖片之前被下載過,那么緩存中就會(huì)有這張圖片的引用存在。重新去加載這張圖片的時(shí)候如果重復(fù)的去初始化 ImageLoaderTools,線程會(huì)被開啟,而使用 imageLoad() 方法加載圖片時(shí)發(fā)現(xiàn)緩存中存在這個(gè)圖片資源,則會(huì)將其直接返回,注意這里使用的是 return bitmap; 那就意味著 imageLoad() 方法里添加下載任務(wù)到下載隊(duì)列的代碼不會(huì)被執(zhí)行到,這時(shí)候 run() 方法中的 isLoop = true 并且 maArrayList_taskQueue.size() = 0,這樣內(nèi)層 while 里的邏輯也就是掛起線程的關(guān)鍵代碼 wait() 永遠(yuǎn)不會(huì)被執(zhí)行到,而外層 while 的判斷條件一直為 true,就這樣程序出現(xiàn)了死循環(huán)。死循環(huán)才是手機(jī)發(fā)燙、卡頓、高 CPU 占用率的真正原因所在。
解決方案
準(zhǔn)確的定位到代碼問題所在后,提出解決方案就很簡單了,這里提供的解決方案是將 wait() 方法從內(nèi)層 while 循環(huán)提到外層 while 循環(huán)中,這樣重復(fù)加載同一張圖片時(shí),死循環(huán)一出現(xiàn)線程就被掛起,這樣就可以避免死循環(huán)的出現(xiàn)。代碼如下:
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<Bitmap>(task.bitmap)); 22 File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); 23 if (!dir.exists()) { 24 dir.mkdirs(); 25 } 26 String[] path = task.path.split("/"); 27 String filename = path[path.length - 1]; 28 File file = new File(dir, filename); 29 BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap); 30 Message msg = Message.obtain(); 31 msg.obj = task; 32 mHandler.sendMessage(msg); 33 } 34 } catch (IOException e) { 35 e.printStackTrace(); 36 } catch (Exception e) { 37 e.printStackTrace(); 38 } 39 40 } 41 42 synchronized (this) { 43 try { 44 wait(); 45 } catch (InterruptedException e) { 46 e.printStackTrace(); 47 } 48 } 49 50 } 51 52 }; 53 54 };
最后再附上代碼修改后代碼運(yùn)行的性能圖,和之前的多次被重復(fù)執(zhí)行,效率有了質(zhì)的提升,手機(jī)發(fā)燙、卡頓、高 CPU 占用率的現(xiàn)象也消失了。


浙公網(wǎng)安備 33010602011771號