TraceView 是 Android 平臺配備一個很好的性能分析的工具。它能夠經過圖形化的方式讓咱們瞭解咱們要跟蹤的程序的性能,而且能具體到 method。詳細內容參考:Profiling with Traceview and dmtracedumphtml
TraceView 簡介java
TraceView 是 Android 平臺特有的數據採集和分析工具,它主要用於分析 Android 中應用程序的 hotspot。TraceView 自己只是一個數據分析工具,而數據的採集則須要使用 Android SDK 中的 Debug 類或者利用 DDMS 工具。兩者的用法以下:android
開發者在一些關鍵代碼段開始前調用 Android SDK 中 Debug 類的 startMethodTracing 函數,並在關鍵代碼段結束前調用 stopMethodTracing 函數。這兩個函數運行過程當中將採集運行時間內該應用全部線程(注意,只能是 Java 線程)的函數執行狀況,並將採集數據保存到 /mnt/sdcard/ 下的一個文件中。開發者而後須要利用 SDK 中的 TraceView 工具來分析這些數據。緩存
藉助 Android SDK 中的 DDMS 工具。DDMS 可採集系統中某個正在運行的進程的函數調用信息。對開發者而言,此方法適用於沒有目標應用源代碼的狀況。網絡
DDMS 中 TraceView 使用示意圖以下,調試人員能夠經過選擇 Devices 中的應用後點擊 按鈕 Start Method Profiling(開啓方法分析)和點擊
Stop Method Profiling(中止方法分析)
app
開啓方法分析後對應用的目標頁面進行測試操做,測試完畢後中止方法分析,界面會跳轉到 DDMS 的 trace 分析界面,以下圖所示:ide
TraceView 界面比較複雜,其 UI 劃分爲上下兩個面板,即 Timeline Panel(時間線面板)和 Profile Panel(分析面板)。上圖中的上半部分爲 Timeline Panel(時間線面板),Timeline Panel 又可細分爲左右兩個 Pane:函數
左邊 Pane 顯示的是測試數據中所採集的線程信息。由圖可知,本次測試數據採集了 main 線程,傳感器線程和其它系統輔助線程的信息。工具
右邊 Pane 所示爲時間線,時間線上是每一個線程測試時間段內所涉及的函數調用信息。這些信息包括函數名、函數執行時間等。由圖可知,Thread-1412 線程對應行的的內容很是豐富,而其餘線程在這段時間內幹得工做則要少得多。oop
另外,開發者能夠在時間線 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 簡介中的方法進行測試,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 佔用率的現象也消失了。