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 工具來分析這些數據。
  • 藉助 Android SDK 中的 DDMS 工具。DDMS 可採集系統中某個正在運行的進程的函數調用信息。對開發者而言,此方法適用於沒有目標應用源代碼的情況。

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 簡介中的方法進行測試,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<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 圖片工具類的全部代碼,先不着急去研究這個類的代碼實現過程,先來看看這個類是怎麼被調用的:

複製代碼
 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<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 };
複製代碼

最後再附上代碼修改後代碼運行的性能圖,和之前的多次被重複執行,效率有了質的提升,手機發燙、卡頓、高 CPU 佔用率的現象也消失了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章