Android應(yīng)用UI性能分析
在使用App時(shí)會(huì)發(fā)現(xiàn)有些界面啟動(dòng)卡頓、動(dòng)畫不流暢、列表等滑動(dòng)時(shí)也會(huì)卡頓出現(xiàn)這種情況,可以考慮對(duì)UI性能分析。
首先要清楚卡頓的原因,有以下幾種情況:
1. 人為在UI線程中做輕微耗時(shí)操作,導(dǎo)致UI線程卡頓;
2. 布局Layout過于復(fù)雜,無(wú)法在16ms內(nèi)完成渲染;( Android系統(tǒng)每隔16ms會(huì)發(fā)出VSYNC信號(hào)重繪我們的界面(Activity).為什么是16ms, 因?yàn)锳ndroid設(shè)定的刷新率是60FPS(Frame Per Second), 也就是每秒60幀的刷新率, 約合16ms刷新一次.這就意味著, 我們需要在16ms內(nèi)完成下一次要刷新的界面的相關(guān)運(yùn)算, 以便界面刷新更新. 然而, 如果我們無(wú)法在16ms內(nèi)完成此次運(yùn)算會(huì)怎樣呢?例如, 假設(shè)我們更新屏幕的背景圖片, 需要24ms來(lái)做這次運(yùn)算. 當(dāng)系統(tǒng)在第一個(gè)16ms時(shí)刷新界面, 然而我們的運(yùn)算還沒有結(jié)束, 無(wú)法繪出圖片. 當(dāng)系統(tǒng)隔16ms再發(fā)一次VSYNC信息重繪界面時(shí), 用戶才會(huì)看到更新后的圖片. 也就是說用戶是32ms后看到了這次刷新(注意, 并不是24ms). 這就是傳說中的丟幀(dropped frame),丟幀給用戶的感覺就是卡頓, 而且如果運(yùn)算過于復(fù)雜, 丟幀會(huì)更多, 導(dǎo)致界面常常處于停滯狀態(tài), 卡到爆.)
3. 同一時(shí)間動(dòng)畫執(zhí)行的次數(shù)過多,導(dǎo)致CPU或GPU負(fù)載過重;
4. View過度繪制,導(dǎo)致某些像素在同一幀時(shí)間內(nèi)被繪制多次,從而使CPU或GPU負(fù)載過重;
5. View頻繁的觸發(fā)measure、layout,導(dǎo)致measure、layout累計(jì)耗時(shí)過多及整個(gè)View頻繁的重新渲染;
6. 內(nèi)存頻繁觸發(fā)GC過多(同一幀中頻繁創(chuàng)建內(nèi)存),導(dǎo)致暫時(shí)阻塞渲染操作;
7. 冗余資源及邏輯等導(dǎo)致加載和執(zhí)行緩慢;
8. 臭名昭著的ANR;
如何分析?
分析UI卡頓我們一般都借助工具,通過工具一般都可以直觀的分析出問題原因,從而反推尋求優(yōu)化方案,具體如下細(xì)說各種強(qiáng)大的工具
1. 使用HierarchyViewer分析UI性能
我們可以通過SDK提供的工具HierarchyViewer來(lái)進(jìn)行UI布局復(fù)雜程度及冗余等分析
通過命令啟動(dòng)HierarchyViewer

接下來(lái)Hierarchy window窗口打開:

一個(gè)Activity的View樹,通過這個(gè)樹可以分析出View嵌套的冗余層級(jí),以及每個(gè)View在繪制的使用時(shí)長(zhǎng)也有表示。
2. 使用Lint進(jìn)行資源及冗余UI布局等優(yōu)化
冗余資源及邏輯等也可能會(huì)導(dǎo)致加載和執(zhí)行緩慢,這可以使用Link工具,來(lái)發(fā)現(xiàn)優(yōu)化這些問題的
在AndroidStudio 1.4版本中使用Lint最簡(jiǎn)單的辦法:就是將鼠標(biāo)放在代碼區(qū)點(diǎn)擊右鍵->Analyze->Inspect Code–>界面選擇你要檢測(cè)的模塊->點(diǎn)擊確認(rèn)開始檢測(cè),等待一下后會(huì)發(fā)現(xiàn)如下結(jié)果:

如果存在冗余的UI層級(jí)嵌套,會(huì)進(jìn)行高亮顯示, 我們根據(jù)提示可以點(diǎn)擊跳進(jìn)去進(jìn)行優(yōu)化處理掉的。
3. 使用Memory監(jiān)測(cè)及GC打印與Allocation Tracker進(jìn)行UI卡頓分析
由于Android系統(tǒng)會(huì)依據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的GC操作,常見應(yīng)用開發(fā)中導(dǎo)致GC頻繁執(zhí)行的原因主要可能是因?yàn)槎虝r(shí)間內(nèi)有大量頻繁的對(duì)象創(chuàng)建與釋放操作,也就是俗稱的內(nèi)存抖動(dòng)現(xiàn)象,或者短時(shí)間內(nèi)已經(jīng)存在大量?jī)?nèi)存暫用介于閾值邊緣,接著每當(dāng)有新對(duì)象創(chuàng)建時(shí)都會(huì)導(dǎo)致超越閾值觸發(fā)GC操作
如何查看?
Android Studio 工具提供內(nèi)存查看器:

根據(jù)內(nèi)存抖動(dòng)現(xiàn)象,查看log日志進(jìn)行分析:

如何看到,這種不停的大面積打印GC導(dǎo)致所有線程暫停的操作必定會(huì)導(dǎo)致UI視覺的卡頓,所以我們要避免此類問題的出現(xiàn),具體的常見優(yōu)化方式如下:
- 檢查代碼,盡量避免有些頻繁觸發(fā)的邏輯方法中存在大量對(duì)象分配;
- 盡量避免在多次for循環(huán)中頻繁分配對(duì)象;
- 避免在自定義View的onDraw()方法中執(zhí)行復(fù)雜的操作及創(chuàng)建對(duì)象(譬如Paint的實(shí)例化操作不要寫在onDraw()方法中等);
- 對(duì)于并發(fā)下載等類似邏輯的實(shí)現(xiàn)盡量避免多次創(chuàng)建線程對(duì)象,而是交給線程池處理。
有了上面說明GC導(dǎo)致的性能后我們就該定位分析問題了,我們可以通過運(yùn)行DDMS->Allocation Tracker標(biāo)簽打開一個(gè)新窗口,然后點(diǎn)擊Start Tracing按鈕,接著運(yùn)行你想分析的代碼,運(yùn)行完畢后點(diǎn)擊GetAllocations按鈕就能夠看見一個(gè)已分配對(duì)象的列表,如下:

UI布局優(yōu)化措施有哪些?
盡可能的減少布局的嵌套層級(jí)(Google建議View樹的高度不宜超過10層。)
以前我們用Eclipse寫代碼時(shí),自動(dòng)生成的模板是以LinearLayout為根節(jié)點(diǎn)的,但是后面變成了RelativeLayout為根節(jié)點(diǎn)。
RelativeLayout可以讓視圖樹的層級(jí)少,但是LinearLayout的測(cè)量效率要高。
如果使用RelativeLayout,需要盡量避免嵌套;如果使用LinearLayout,保證層級(jí)不能太深。使用HierarchyViewer工具分析視圖樹,剔除沒有用到的布局元素
不用設(shè)置不必要的背景,避免過度繪制,比如父控件設(shè)置了背景色,子控件完全將父控件給覆蓋的情況下,那么父控件就沒必要設(shè)置背景了
<include>標(biāo)簽
編寫頁(yè)面布局是可以將通用的UI提取出來(lái),使用的時(shí)候使用<include>標(biāo)簽將其引入。但是<include>標(biāo)簽并不能減少布局層級(jí),只能增加代碼可讀性。-
<merge>標(biāo)簽
問:使用merge標(biāo)簽有哪些優(yōu)點(diǎn)?
答:<merge/>標(biāo)簽可以用于減少View樹的層次來(lái)優(yōu)化Android的布局,使用<include>包含布局的時(shí)候,系統(tǒng)會(huì)自動(dòng)忽略merge層級(jí)。問:merge標(biāo)簽有什么缺點(diǎn)么?
答:1. <merge/>只能作為XML布局的根標(biāo)簽使用。
2.當(dāng)Inflate以<merge/>開頭的布局文件時(shí),必須指定一個(gè)父ViewGroup,并且必須設(shè)定attachToRoot為true。問:什么情況考慮使用Merge標(biāo)簽?
答:1.子視圖不需要指定任何針對(duì)父視圖的布局屬性,例子中TextView僅僅需要直接添加到父視圖上用于顯示就行。
2.假如需要在LinearLayout里面嵌入一個(gè)布局(或者視圖),而恰恰這個(gè)布局(或者視圖)的根節(jié)點(diǎn)也是LinearLayout,這樣就多了一層沒有用的嵌套,無(wú)疑這樣只會(huì)拖慢程序速度。而這個(gè)時(shí)候如果我們使用merge根標(biāo)簽就可以避免那樣的問題。 -
<ViewStub>標(biāo)簽
問:使用ViewStub標(biāo)簽有哪些優(yōu)點(diǎn)?
答:<ViewStub>標(biāo)簽可以實(shí)現(xiàn)對(duì)一個(gè)view進(jìn)行延遲加載,是一個(gè)輕量級(jí)的布局,它聲明在xml布局中,在Activity加載布局時(shí),被它包裹的View看不見也不占布局位置,最重要的是:它不會(huì)實(shí)例化里邊的View,也不會(huì)產(chǎn)生繪制的消耗,直到它被主動(dòng)inflate.問:使用ViewStub有啥需要注意的嗎?
答:1. 在要渲染的布局中并不支持<merge/>標(biāo)簽。
2.ViewStub.infalte方法不能調(diào)用兩次,否則會(huì)出現(xiàn)異常。(因?yàn)橐坏¬iewStub visible/inflated,則ViewStub將從視圖框架中移除,其id也會(huì)失效)問:ViewStub標(biāo)簽和View.GONE的區(qū)別?
答:ViewStub標(biāo)簽只有在顯示時(shí)才去渲染整個(gè)布局,但是View.GONE在初始化布局時(shí)就已經(jīng)添加在布局樹上了。
<ViewStub>用法:
((ViewStub)findViewById(R.id.viewstub)).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.viewstub)).inflate();
Android性能優(yōu)化之App應(yīng)用啟動(dòng)分析與優(yōu)化
App啟動(dòng)方式
通常來(lái)說, 一個(gè)App啟動(dòng)也會(huì)分如下兩種不同的狀態(tài):
1)冷啟動(dòng)
當(dāng)啟動(dòng)應(yīng)用時(shí),后臺(tái)沒有該應(yīng)用的進(jìn)程,這時(shí)系統(tǒng)會(huì)重新創(chuàng)建一個(gè)新的進(jìn)程分配給該應(yīng)用,這個(gè)啟動(dòng)方式就是冷啟動(dòng)。冷啟動(dòng)因?yàn)橄到y(tǒng)會(huì)重新創(chuàng)建一個(gè)新的進(jìn)程分配給它,所以會(huì)先創(chuàng)建和初始化Application類,再創(chuàng)建和初始化MainActivity類(包括一系列的測(cè)量、布局、繪制),最后顯示在界面上。2.)熱啟動(dòng)
當(dāng)啟動(dòng)應(yīng)用時(shí),后臺(tái)已有該應(yīng)用的進(jìn)程(例:按back鍵、home鍵,應(yīng)用雖然會(huì)退出,但是該應(yīng)用的進(jìn)程是依然會(huì)保留在后臺(tái),可進(jìn)入任務(wù)列表查看),所以在已有進(jìn)程的情況下,這種啟動(dòng)會(huì)從已有的進(jìn)程中來(lái)啟動(dòng)應(yīng)用,這個(gè)方式叫熱啟動(dòng)。熱啟動(dòng)因?yàn)闀?huì)從已有的進(jìn)程中來(lái)啟動(dòng),所以熱啟動(dòng)就不會(huì)走Application這步了,而是直接走M(jìn)ainActivity(包括一系列的測(cè)量、布局、繪制),所以熱啟動(dòng)的過程只需要?jiǎng)?chuàng)建和初始化一個(gè)MainActivity就行了,而不必創(chuàng)建和初始化Application,因?yàn)橐粋€(gè)應(yīng)用從新進(jìn)程的創(chuàng)建到進(jìn)程的銷毀,Application只會(huì)初始化一次。
啟動(dòng)時(shí)間的測(cè)量
關(guān)于Activity啟動(dòng)時(shí)間的定義
對(duì)于Activity來(lái)說,啟動(dòng)時(shí),首先執(zhí)行的是onCreate()、onStart()、onResume()這些生命周期函數(shù),但即使這些生命周期方法回調(diào)結(jié)束了,應(yīng)用也不算已經(jīng)完全啟動(dòng),還需要等View樹全部構(gòu)建完畢,一般認(rèn)為,setContentView中的View全部顯示結(jié)束了,算作是應(yīng)用完全啟動(dòng)了。
那么這個(gè)時(shí)間,實(shí)際上是Activity啟動(dòng),到Layout全部顯示的過程,但是要注意,這里并不包括數(shù)據(jù)的加載,因?yàn)楹芏郃pp在加載時(shí)會(huì)使用懶加載模式,即數(shù)據(jù)拉取后,再刷新默認(rèn)的UI。
基于上面的啟動(dòng)流程我們盡量做到如下幾點(diǎn)
- Application的創(chuàng)建過程中盡量少的進(jìn)行耗時(shí)操作
- 首屏Activity的渲染
關(guān)于Application
Application是程序的主入口,特別是很多第三方SDK都會(huì)需要在Application的onCreate里面做很多初始化操作,一般來(lái)說我們可以將這些初始化放在一個(gè)單獨(dú)的線程中處理, 為了方便今后管理,
優(yōu)化的方法,無(wú)非是通過以下幾個(gè)方面:
1) 異步初始化
這個(gè)很簡(jiǎn)單,就是讓App在onCreate里面盡可能的少做事情,而利用手機(jī)的多核特性,盡可能的利用多線程,例如一些第三方框架的初始化,如果能放線程,就盡量的放入線程中,最簡(jiǎn)單的,你可以直接new Thread(),當(dāng)然,你也可以通過公共的線程池來(lái)進(jìn)行異步的初始化工作,這個(gè)是最能夠壓縮啟動(dòng)時(shí)間的方式2) 后臺(tái)任務(wù)
使用IntentService不同于Service, 它是工作在后臺(tái)線程的.3) 界面預(yù)加載
當(dāng)系統(tǒng)加載一個(gè)Activity的時(shí)候,onCreate()是一個(gè)耗時(shí)過程,那么在這個(gè)過程中,系統(tǒng)為了讓用戶能有一個(gè)比較好的體驗(yàn),實(shí)際上會(huì)先繪制一些初始界面,類似于PlaceHolder。
系統(tǒng)首先會(huì)讀取當(dāng)前Activity的Theme,然后根據(jù)Theme中的配置來(lái)繪制,當(dāng)Activity加載完畢后,才會(huì)替換為真正的界面。所以,Google官方提供的解決方案,就是通過android:windowBackground屬性,來(lái)進(jìn)行加載前的配置,同時(shí),這里不僅可以配置顏色,還能配置圖片.
自定義View如何優(yōu)化內(nèi)存?
- 不建議在draw或者layout的過程中去實(shí)例化對(duì)象