Android 編程下的TraceView 簡介及其案例實戰
TraceView 是 Android 平臺配備一個很好的性能分析的工具。它可以通過圖形化的方式讓我們了解我們要跟蹤的程序的性能,并且能具體到 method。詳細內容參考:Profiling with Traceview and dmtracedump
TraceView 簡介
TraceView 是 Android 平臺特有的數據采集和分析工具,它主要用于分析 Android 中應用程序的 hotspot。TraceView 本身只是一個數據分析工具,而數據的采集則需要使用 Android SDK 中的 Debug 類或者利用 DDMS 工具。二者的用法如下:
-
開發者在一些關鍵代碼段開始前調用 Android SDK 中 Debug 類的 startMethodTracing 函數,并在關鍵代碼段結束前調用 stopMethodTracing 函數。這兩個函數運行過程中將采集運行時間內該應用所有線程(注意,只能是 Java 線程)的函數執行情況,并將采集數據保存到 /mnt/sdcard/ 下的一個文件中。開發者然后需要利用 SDK 中的 TraceView 工具來分析這些數據。
</li> -
借助 Android SDK 中的 DDMS 工具。DDMS 可采集系統中某個正在運行的進程的函數調用信息。對開發者而言,此方法適用于沒有目標應用源代碼的情況。
</li> </ul>DDMS 中 TraceView 使用示意圖如下,調試人員可以通過選擇 Devices 中的應用后點擊
按鈕 Start Method Profiling(開啟方法分析)和點擊
Stop Method Profiling(停止方法分析)
開啟方法分析后對應用的目標頁面進行測試操作,測試完畢后停止方法分析,界面會跳轉到 DDMS 的 trace 分析界面,如下圖所示:
TraceView 界面比較復雜,其 UI 劃分為上下兩個面板,即 Timeline Panel(時間線面板)和 Profile Panel(分析面板)。上圖中的上半部分為 Timeline Panel(時間線面板),Timeline Panel 又可細分為左右兩個 Pane:
-
左邊 Pane 顯示的是測試數據中所采集的線程信息。由圖可知,本次測試數據采集了 main 線程,傳感器線程和其它系統輔助線程的信息。
</li> -
右邊 Pane 所示為時間線,時間線上是每個線程測試時間段內所涉及的函數調用信息。這些信息包括函數名、函數執行時間等。由圖可知,Thread-1412 線程對應行的的內容非常豐富,而其他線程在這段時間內干得工作則要少得多。
</li> -
另外,開發者可以在時間線 Pane 中移動時間線縱軸。縱軸上邊將顯示當前時間點中某線程正在執行的函數信息。
</li> </ul>上圖中的下半部分為 Profile Panel(分析面板),Profile Panel 是 TraceView 的核心界面,其內涵非常豐富。它主要展示了某個線程(先在 Timeline Panel 中選擇線程)中各個函數調用的情況,包括 CPU 使用時間、調用次數等信息。而這些信息正是查找 hotspot 的關鍵依據。所以,對開發者而言,一定要了解 Profile Panel 中各列的含義。下表列出了 Profile Panel 中比較重要的列名及其描述。
TraceView 實戰
了解完 TraceView 的 UI 后,現在介紹如何利用 TraceView 來查找 hotspot。一般而言,hotspot 包括兩種類型的函數:
-
一類是調用次數不多,但每次調用卻需要花費很長時間的函數。
</li> -
一類是那些自身占用時間不長,但調用卻非常頻繁的函數。
</li> </ul>測試背景: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; }
}</pre>
以上代碼即是 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); }</pre>
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(); } }
}
};
};</pre>
最后再附上代碼修改后代碼運行的性能圖,和之前的多次被重復執行,效率有了質的提升,手機發燙、卡頓、高 CPU 占用率的現象也消失了。
</div>
-
-