Android應(yīng)用卡頓優(yōu)化之路(一)-TraceView

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)用源代碼的情況。
在Android Studio --> Tools --> Android --> Android Device Monitor打開(kāi)DDMS

DDMS 中 TraceView 使用示意圖如下,調(diào)試人員可以通過(guò)選擇 Devices 中的應(yīng)用后點(diǎn)擊
按鈕 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概覽

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),如下圖所示:

根據(jù)Incl Cpu Time降序排列

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

指標(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)用了

image.png

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

FlowerCanvasLayout里面的mThread變量的run方法

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í)間,

查看另外一個(gè)特別耗時(shí)的方法,發(fā)現(xiàn)是解析JSON的操作

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

在主線(xiàn)程里使用解析JSON!

這個(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)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,374評(píng)論 25 708
  • 一、關(guān)于App性能優(yōu)化 1. 性能優(yōu)化分類(lèi) Google官方給出的性能優(yōu)化教程,主要分為以下幾類(lèi):1)布局與UI渲...
    cszhangchao閱讀 2,011評(píng)論 0 6
  • source: ICCV09authors: Daniel Glasner Shai Bagon Michal ...
    OWNSTYLEDU_大鵬閱讀 3,018評(píng)論 1 1
  • 時(shí)光的流逝,早已不再是最初的我們。 誰(shuí)敢想到這烏煙瘴氣,滿(mǎn)是塵埃的天空,最初竟是天空澄碧,纖云不染,和風(fēng)送暖的景象...
    Pressure_b82c閱讀 288評(píng)論 0 0
  • 記憶 的確沒(méi)有什么能跟記憶并駕齊驅(qū)的稱(chēng)為重要 深處當(dāng)下的每一刻 我們都會(huì)感到周遭的無(wú)聊乏味 或者碌碌無(wú)為 或者燈紅...
    蔓草島嶼閱讀 206評(píng)論 0 0

友情鏈接更多精彩內(nèi)容