1 通過TraceView發現程序代碼可優化的點
1.1 TraceView簡介
TraceView 簡介
TraceView 是 Android 平臺特有的數據采集和分析工具,它主要用于分析 Android 中應用程序的 hotspot。TraceView 本身只是一個數據分析工具,而數據的采集則需要使用 Android SDK 中的 Debug 類或者利用 DDMS 工具。二者的用法如下:
開發者在一些關鍵代碼段開始前調用 Android SDK 中 Debug 類的 startMethodTracing 函數,并在關鍵代碼段結束前調用 stopMethodTracing 函數。這兩個函數運行過程中將采集運行時間內該應用所有線程(注意,只能是 Java 線程)的函數執行情況,并將采集數據保存到 /mnt/sdcard/ 下的一個文件中。開發者然后需要利用 SDK 中的 TraceView 工具來分析這些數據。
1.2 TraceView使用
借助 Android SDK 中的 DDMS 工具。DDMS 可采集系統中某個正在運行的進程的函數調用信息。對開發者而言,此方法適用于沒有目標應用源代碼的情況。
DDMS 中 TraceView 使用示意圖如下,調試人員可以通過選擇 Devices 中的應用后點擊
在Android Studio --> Tools --> Android --> Android Device Monitor打開DDMS
按鈕 Start Method Profiling(開啟方法分析)和點擊
Stop Method Profiling(停止方法分析)![]()
點擊開始錄制后,我們就可以開始操作App,想測試哪里就點哪里(步步高打火機,哪里不會點哪里)。
錄制完成后點擊同一個按鈕結束,就可以看到以下的TraceView界面。
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 的關鍵依據。
1.3 然后我們根據Incl Cpu Time進行降序排列,看看那些方法調用的時間最長,如下圖所示:
先說說標題欄上的各個指標是什么意思:
結合Excl Real Time查看方法自身耗時,同時注意CPU占用率,CPU占用率達到100%的基本上都很可疑了,需要看看是否有死循環調用。
結合方法的調用次數查看,方法調用次數特別多的也可以看看有什么可以優化的地方。
1.4 點開調用占用CPU時間最長的第一個方法,Thread.run()方法,看看是哪里調用了
看起來這個方法非常可疑,到代碼里看看
FlowerCanvasLayout
里面的mThread
變量的run方法
果然干了件坑爹的事情,我們都知道代碼是要在CPU里跑的(這特么不是廢話嗎?),很多剛開始開發Android的同學覺得,雖然Android主線程不能隨便進行耗時操作,同理也不能死循環,那我在子線程里死循環就可以啦,但是這樣是有問題的,在子線程中進行死循環操作,雖然CPU會剝奪子線程的時間片,但是子線程里會搶占主線程的時間片,就好像雖然一個子線程能搶的時間片不多,但是如果有多個子線程呢?子線程里還有死循環的代碼,這是萬萬不可的,因此這里我們需要在子線程中單次循環進行線程掛起,在合適的時候喚醒此線程避免一直進行死循環等待。
1.5 JSON在主線程解析
繼續通過TraceView查找調用特別耗時的方法,看到一個,圖片可能不好看清,主要看看方法調用時間,
查看代碼,發現在SocketActivity
中調用了SocketMessageHelper.handleSocketMessage
,看看這個方法里干了什么。
這個JSON是服務器通過Socket分發的各種事件,非常長,連Logcat都無法完成打印出來,可想而知在主線程里解析這么長的JSON字符串會導致多么的卡頓。
解決辦法:把JSON解析移動到工作線程中完成,解析完成后分發給主線程
1.6 更多的優化例子持續更新...
主要的是要學會使用TraceView找出App中可以優化的點,每個例子只是一種方法
1.7 寫代碼過程中避免主線程卡頓的注意事項:
1)不要大量使用new Thread()的方式初始化子線程,這樣會導致大量的線程創建活動,線程創建是很耗時的,而且還帶有內存占用(好像是64KB?),盡量使用線程池的方式復用線程。
2)不要創建太多子線程,太多子線程會搶占主線程時間片,導致UI卡頓,使用緩存線程池。
3)創建子線程時記得設置優先級為較低優先級
線程池框架:
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "TaskExecutor #" + mCount.getAndIncrement());
t.setPriority(Thread.MIN_PRIORITY);
return t;
}
};
HandlerThread:
mHandlerThread = new HandlerThread(threadName, android.os.Process.THREAD_PRIORITY_BACKGROUND);
mHandlerThread.start();
4)不要讓主線程和工作線程競爭同一個鎖,容易讓主線程卡頓等待,導致ANR,盡量讓主線程不需要獲取鎖,需要獲取鎖的方法盡量在子線程調用。
5)解析JSON等耗時操作不要在主線程執行
6)不要讓工作線程進行死循環,這樣會大大增加CPU使用率,增加設備耗電并且降低主線程的效率。
7)減少SharePreferences打開關閉次數,盡量合并寫入,減少磁盤讀取寫入次數,使用apply()代替commit(),這個雖然是簡單的優化,但是能大大減少主線程讀寫文件帶來的卡頓(SharePreference是XML文件,使用commit同步寫入的話在主線程讀寫磁盤會有性能損耗,使用apply異步寫入代替,很多開發人員不重視這一點)
8)避免在主線程操作文件和數據庫
9)使用適當大小的Buffer讀寫文件 ,過小的Buffer會導致多次讀寫磁盤,例如一個1M的文件,你使用1K的Buffer就需要讀十次,10M的文件呢?
//buffer的大小根據業務文件平均大小選擇
FileInputStream in = ...
byte[] buffer = new byte[8196];
while (len = in. read(buffer,0,8196)) != -1) {
}
10)除非必要,否則盡量不要使用索引(AUTOINCREMENT),使用索引需要維護多一張索引表,寫入時都需要進行多次寫入磁盤,會影響寫入效率,頻繁查詢的表才適合使用索引,頻繁寫入少查詢的表不適合使用索引。
SQLite創建一個叫sqlite_sequence的內部表來記錄該表使用的最大行號。如果指定使用AUTOINCREMENT來創建表,則sqlite_sequence也隨之創建。UPDATE、INSERT、DELETE語句可能會修改sqlite_sequence的內容。因為維護sqlite_sequence表帶來的額外開銷會導致INSERT的效率降低。
11)避免使用低效率的API,如上面的JSON解析方法,原來的代碼直接使用了Java自帶的JSON API解析,這個庫的解析效率較低,替換使用GSON解析,并且解析方法放到工作線程中。
12)某些特別消耗計算能力的方法,可以通過RenderThread放到GPU中調用。