1 通過(guò)TraceView發(fā)現(xiàn)程序代碼可優(yōu)化的點(diǎn)
1.1 TraceView簡(jiǎn)介
TraceView 簡(jiǎn)介
TraceView 是 Android 平臺(tái)特有的數(shù)據(jù)采集和分析工具,它主要用于分析 Android 中應(yīng)用程序的 hotspot。TraceView 本身只是一個(gè)數(shù)據(jù)分析工具,而數(shù)據(jù)的采集則需要使用 Android SDK 中的 Debug 類(lèi)或者利用 DDMS 工具。二者的用法如下:
開(kāi)發(fā)者在一些關(guān)鍵代碼段開(kāi)始前調(diào)用 Android SDK 中 Debug 類(lèi)的 startMethodTracing 函數(shù),并在關(guān)鍵代碼段結(jié)束前調(diào)用 stopMethodTracing 函數(shù)。這兩個(gè)函數(shù)運(yùn)行過(guò)程中將采集運(yùn)行時(shí)間內(nèi)該應(yīng)用所有線(xiàn)程(注意,只能是 Java 線(xiàn)程)的函數(shù)執(zhí)行情況,并將采集數(shù)據(jù)保存到 /mnt/sdcard/ 下的一個(gè)文件中。開(kāi)發(fā)者然后需要利用 SDK 中的 TraceView 工具來(lái)分析這些數(shù)據(jù)。
1.2 TraceView使用
借助 Android SDK 中的 DDMS 工具。DDMS 可采集系統(tǒng)中某個(gè)正在運(yùn)行的進(jìn)程的函數(shù)調(diào)用信息。對(duì)開(kāi)發(fā)者而言,此方法適用于沒(méi)有目標(biāo)應(yīng)用源代碼的情況。
DDMS 中 TraceView 使用示意圖如下,調(diào)試人員可以通過(guò)選擇 Devices 中的應(yīng)用后點(diǎn)擊
在Android Studio --> Tools --> Android --> Android Device Monitor打開(kāi)DDMS
按鈕 Start Method Profiling(開(kāi)啟方法分析)和點(diǎn)擊
Stop Method Profiling(停止方法分析)![]()
點(diǎn)擊開(kāi)始錄制后,我們就可以開(kāi)始操作App,想測(cè)試哪里就點(diǎn)哪里(步步高打火機(jī),哪里不會(huì)點(diǎn)哪里)。
錄制完成后點(diǎn)擊同一個(gè)按鈕結(jié)束,就可以看到以下的TraceView界面。

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

先說(shuō)說(shuō)標(biāo)題欄上的各個(gè)指標(biāo)是什么意思:

結(jié)合Excl Real Time查看方法自身耗時(shí),同時(shí)注意CPU占用率,CPU占用率達(dá)到100%的基本上都很可疑了,需要看看是否有死循環(huán)調(diào)用。
結(jié)合方法的調(diào)用次數(shù)查看,方法調(diào)用次數(shù)特別多的也可以看看有什么可以?xún)?yōu)化的地方。
1.4 點(diǎn)開(kāi)調(diào)用占用CPU時(shí)間最長(zhǎng)的第一個(gè)方法,Thread.run()方法,看看是哪里調(diào)用了

看起來(lái)這個(gè)方法非??梢桑酱a里看看

FlowerCanvasLayout里面的mThread變量的run方法果然干了件坑爹的事情,我們都知道代碼是要在CPU里跑的(這特么不是廢話(huà)嗎?),很多剛開(kāi)始開(kāi)發(fā)Android的同學(xué)覺(jué)得,雖然Android主線(xiàn)程不能隨便進(jìn)行耗時(shí)操作,同理也不能死循環(huán),那我在子線(xiàn)程里死循環(huán)就可以啦,但是這樣是有問(wèn)題的,在子線(xiàn)程中進(jìn)行死循環(huán)操作,雖然CPU會(huì)剝奪子線(xiàn)程的時(shí)間片,但是子線(xiàn)程里會(huì)搶占主線(xiàn)程的時(shí)間片,就好像雖然一個(gè)子線(xiàn)程能搶的時(shí)間片不多,但是如果有多個(gè)子線(xiàn)程呢?子線(xiàn)程里還有死循環(huán)的代碼,這是萬(wàn)萬(wàn)不可的,因此這里我們需要在子線(xiàn)程中單次循環(huán)進(jìn)行線(xiàn)程掛起,在合適的時(shí)候喚醒此線(xiàn)程避免一直進(jìn)行死循環(huán)等待。
1.5 JSON在主線(xiàn)程解析
繼續(xù)通過(guò)TraceView查找調(diào)用特別耗時(shí)的方法,看到一個(gè),圖片可能不好看清,主要看看方法調(diào)用時(shí)間,

查看代碼,發(fā)現(xiàn)在SocketActivity中調(diào)用了SocketMessageHelper.handleSocketMessage,看看這個(gè)方法里干了什么。

這個(gè)JSON是服務(wù)器通過(guò)Socket分發(fā)的各種事件,非常長(zhǎng),連Logcat都無(wú)法完成打印出來(lái),可想而知在主線(xiàn)程里解析這么長(zhǎng)的JSON字符串會(huì)導(dǎo)致多么的卡頓。
解決辦法:把JSON解析移動(dòng)到工作線(xiàn)程中完成,解析完成后分發(fā)給主線(xiàn)程
1.6 更多的優(yōu)化例子持續(xù)更新...
主要的是要學(xué)會(huì)使用TraceView找出App中可以?xún)?yōu)化的點(diǎn),每個(gè)例子只是一種方法
1.7 寫(xiě)代碼過(guò)程中避免主線(xiàn)程卡頓的注意事項(xiàng):
1)不要大量使用new Thread()的方式初始化子線(xiàn)程,這樣會(huì)導(dǎo)致大量的線(xiàn)程創(chuàng)建活動(dòng),線(xiàn)程創(chuàng)建是很耗時(shí)的,而且還帶有內(nèi)存占用(好像是64KB?),盡量使用線(xiàn)程池的方式復(fù)用線(xiàn)程。
2)不要?jiǎng)?chuàng)建太多子線(xiàn)程,太多子線(xiàn)程會(huì)搶占主線(xiàn)程時(shí)間片,導(dǎo)致UI卡頓,使用緩存線(xiàn)程池。
3)創(chuàng)建子線(xiàn)程時(shí)記得設(shè)置優(yōu)先級(jí)為較低優(yōu)先級(jí)
線(xiàn)程池框架:
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)不要讓主線(xiàn)程和工作線(xiàn)程競(jìng)爭(zhēng)同一個(gè)鎖,容易讓主線(xiàn)程卡頓等待,導(dǎo)致ANR,盡量讓主線(xiàn)程不需要獲取鎖,需要獲取鎖的方法盡量在子線(xiàn)程調(diào)用。
5)解析JSON等耗時(shí)操作不要在主線(xiàn)程執(zhí)行
6)不要讓工作線(xiàn)程進(jìn)行死循環(huán),這樣會(huì)大大增加CPU使用率,增加設(shè)備耗電并且降低主線(xiàn)程的效率。
7)減少SharePreferences打開(kāi)關(guān)閉次數(shù),盡量合并寫(xiě)入,減少磁盤(pán)讀取寫(xiě)入次數(shù),使用apply()代替commit(),這個(gè)雖然是簡(jiǎn)單的優(yōu)化,但是能大大減少主線(xiàn)程讀寫(xiě)文件帶來(lái)的卡頓(SharePreference是XML文件,使用commit同步寫(xiě)入的話(huà)在主線(xiàn)程讀寫(xiě)磁盤(pán)會(huì)有性能損耗,使用apply異步寫(xiě)入代替,很多開(kāi)發(fā)人員不重視這一點(diǎn))
8)避免在主線(xiàn)程操作文件和數(shù)據(jù)庫(kù)
9)使用適當(dāng)大小的Buffer讀寫(xiě)文件 ,過(guò)小的Buffer會(huì)導(dǎo)致多次讀寫(xiě)磁盤(pán),例如一個(gè)1M的文件,你使用1K的Buffer就需要讀十次,10M的文件呢?
//buffer的大小根據(jù)業(yè)務(wù)文件平均大小選擇
FileInputStream in = ...
byte[] buffer = new byte[8196];
while (len = in. read(buffer,0,8196)) != -1) {
}
10)除非必要,否則盡量不要使用索引(AUTOINCREMENT),使用索引需要維護(hù)多一張索引表,寫(xiě)入時(shí)都需要進(jìn)行多次寫(xiě)入磁盤(pán),會(huì)影響寫(xiě)入效率,頻繁查詢(xún)的表才適合使用索引,頻繁寫(xiě)入少查詢(xún)的表不適合使用索引。
SQLite創(chuàng)建一個(gè)叫sqlite_sequence的內(nèi)部表來(lái)記錄該表使用的最大行號(hào)。如果指定使用AUTOINCREMENT來(lái)創(chuàng)建表,則sqlite_sequence也隨之創(chuàng)建。UPDATE、INSERT、DELETE語(yǔ)句可能會(huì)修改sqlite_sequence的內(nèi)容。因?yàn)榫S護(hù)sqlite_sequence表帶來(lái)的額外開(kāi)銷(xiāo)會(huì)導(dǎo)致INSERT的效率降低。
11)避免使用低效率的API,如上面的JSON解析方法,原來(lái)的代碼直接使用了Java自帶的JSON API解析,這個(gè)庫(kù)的解析效率較低,替換使用GSON解析,并且解析方法放到工作線(xiàn)程中。
12)某些特別消耗計(jì)算能力的方法,可以通過(guò)RenderThread放到GPU中調(diào)用。
2 參考
Android 編程下的 TraceView 簡(jiǎn)介及其案例實(shí)戰(zhàn)
Android移動(dòng)性能實(shí)戰(zhàn)