Android應(yīng)用UI性能分析
在使用App時會發(fā)現(xiàn)有些界面啟動卡頓、動畫不流暢、列表等滑動時也會卡頓出現(xiàn)這種情況,可以考慮對UI性能分析。
首先要清楚卡頓的原因,有以下幾種情況:
1. 人為在UI線程中做輕微耗時操作,導(dǎo)致UI線程卡頓;
2. 布局Layout過于復(fù)雜,無法在16ms內(nèi)完成渲染;( Android系統(tǒng)每隔16ms會發(fā)出VSYNC信號重繪我們的界面(Activity).為什么是16ms, 因為Android設(shè)定的刷新率是60FPS(Frame Per Second), 也就是每秒60幀的刷新率, 約合16ms刷新一次.這就意味著, 我們需要在16ms內(nèi)完成下一次要刷新的界面的相關(guān)運算, 以便界面刷新更新. 然而, 如果我們無法在16ms內(nèi)完成此次運算會怎樣呢?例如, 假設(shè)我們更新屏幕的背景圖片, 需要24ms來做這次運算. 當系統(tǒng)在第一個16ms時刷新界面, 然而我們的運算還沒有結(jié)束, 無法繪出圖片. 當系統(tǒng)隔16ms再發(fā)一次VSYNC信息重繪界面時, 用戶才會看到更新后的圖片. 也就是說用戶是32ms后看到了這次刷新(注意, 并不是24ms). 這就是傳說中的丟幀(dropped frame),丟幀給用戶的感覺就是卡頓, 而且如果運算過于復(fù)雜, 丟幀會更多, 導(dǎo)致界面常常處于停滯狀態(tài), 卡到爆.)
3. 同一時間動畫執(zhí)行的次數(shù)過多,導(dǎo)致CPU或GPU負載過重;
4. View過度繪制,導(dǎo)致某些像素在同一幀時間內(nèi)被繪制多次,從而使CPU或GPU負載過重;
5. View頻繁的觸發(fā)measure、layout,導(dǎo)致measure、layout累計耗時過多及整個View頻繁的重新渲染;
6. 內(nèi)存頻繁觸發(fā)GC過多(同一幀中頻繁創(chuàng)建內(nèi)存),導(dǎo)致暫時阻塞渲染操作;
7. 冗余資源及邏輯等導(dǎo)致加載和執(zhí)行緩慢;
8. 臭名昭著的ANR;
如何分析?
分析UI卡頓我們一般都借助工具,通過工具一般都可以直觀的分析出問題原因,從而反推尋求優(yōu)化方案,具體如下細說各種強大的工具
1. 使用HierarchyViewer分析UI性能
我們可以通過SDK提供的工具HierarchyViewer來進行UI布局復(fù)雜程度及冗余等分析
通過命令啟動HierarchyViewer
接下來Hierarchy window窗口打開:
一個Activity的View樹,通過這個樹可以分析出View嵌套的冗余層級,以及每個View在繪制的使用時長也有表示。
2. 使用Lint進行資源及冗余UI布局等優(yōu)化
冗余資源及邏輯等也可能會導(dǎo)致加載和執(zhí)行緩慢,這可以使用Link工具,來發(fā)現(xiàn)優(yōu)化這些問題的
在AndroidStudio 1.4版本中使用Lint最簡單的辦法:就是將鼠標放在代碼區(qū)點擊右鍵->Analyze->Inspect Code–>界面選擇你要檢測的模塊->點擊確認開始檢測,等待一下后會發(fā)現(xiàn)如下結(jié)果:
如果存在冗余的UI層級嵌套,會進行高亮顯示, 我們根據(jù)提示可以點擊跳進去進行優(yōu)化處理掉的。
3. 使用Memory監(jiān)測及GC打印與Allocation Tracker進行UI卡頓分析
由于Android系統(tǒng)會依據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的GC操作,常見應(yīng)用開發(fā)中導(dǎo)致GC頻繁執(zhí)行的原因主要可能是因為短時間內(nèi)有大量頻繁的對象創(chuàng)建與釋放操作,也就是俗稱的內(nèi)存抖動現(xiàn)象,或者短時間內(nèi)已經(jīng)存在大量內(nèi)存暫用介于閾值邊緣,接著每當有新對象創(chuàng)建時都會導(dǎo)致超越閾值觸發(fā)GC操作
如何查看?
Android Studio 工具提供內(nèi)存查看器:
根據(jù)內(nèi)存抖動現(xiàn)象,查看log日志進行分析:
如何看到,這種不停的大面積打印GC導(dǎo)致所有線程暫停的操作必定會導(dǎo)致UI視覺的卡頓,所以我們要避免此類問題的出現(xiàn),具體的常見優(yōu)化方式如下:
- 檢查代碼,盡量避免有些頻繁觸發(fā)的邏輯方法中存在大量對象分配;
- 盡量避免在多次for循環(huán)中頻繁分配對象;
- 避免在自定義View的onDraw()方法中執(zhí)行復(fù)雜的操作及創(chuàng)建對象(譬如Paint的實例化操作不要寫在onDraw()方法中等);
- 對于并發(fā)下載等類似邏輯的實現(xiàn)盡量避免多次創(chuàng)建線程對象,而是交給線程池處理。
有了上面說明GC導(dǎo)致的性能后我們就該定位分析問題了,我們可以通過運行DDMS->Allocation Tracker標簽打開一個新窗口,然后點擊Start Tracing按鈕,接著運行你想分析的代碼,運行完畢后點擊GetAllocations按鈕就能夠看見一個已分配對象的列表,如下:
UI布局優(yōu)化措施有哪些?
盡可能的減少布局的嵌套層級(Google建議View樹的高度不宜超過10層。)
以前我們用Eclipse寫代碼時,自動生成的模板是以LinearLayout為根節(jié)點的,但是后面變成了RelativeLayout為根節(jié)點。
RelativeLayout可以讓視圖樹的層級少,但是LinearLayout的測量效率要高。
如果使用RelativeLayout,需要盡量避免嵌套;如果使用LinearLayout,保證層級不能太深。使用HierarchyViewer工具分析視圖樹,剔除沒有用到的布局元素
不用設(shè)置不必要的背景,避免過度繪制,比如父控件設(shè)置了背景色,子控件完全將父控件給覆蓋的情況下,那么父控件就沒必要設(shè)置背景了
<include>標簽
編寫頁面布局是可以將通用的UI提取出來,使用的時候使用<include>標簽將其引入。但是<include>標簽并不能減少布局層級,只能增加代碼可讀性。-
<merge>標簽
問:使用merge標簽有哪些優(yōu)點?
答:<merge/>標簽可以用于減少View樹的層次來優(yōu)化Android的布局,使用<include>包含布局的時候,系統(tǒng)會自動忽略merge層級。問:merge標簽有什么缺點么?
答:1. <merge/>只能作為XML布局的根標簽使用。
2.當Inflate以<merge/>開頭的布局文件時,必須指定一個父ViewGroup,并且必須設(shè)定attachToRoot為true。問:什么情況考慮使用Merge標簽?
答:1.子視圖不需要指定任何針對父視圖的布局屬性,例子中TextView僅僅需要直接添加到父視圖上用于顯示就行。
2.假如需要在LinearLayout里面嵌入一個布局(或者視圖),而恰恰這個布局(或者視圖)的根節(jié)點也是LinearLayout,這樣就多了一層沒有用的嵌套,無疑這樣只會拖慢程序速度。而這個時候如果我們使用merge根標簽就可以避免那樣的問題。 -
<ViewStub>標簽
問:使用ViewStub標簽有哪些優(yōu)點?
答:<ViewStub>標簽可以實現(xiàn)對一個view進行延遲加載,是一個輕量級的布局,它聲明在xml布局中,在Activity加載布局時,被它包裹的View看不見也不占布局位置,最重要的是:它不會實例化里邊的View,也不會產(chǎn)生繪制的消耗,直到它被主動inflate.問:使用ViewStub有啥需要注意的嗎?
答:1. 在要渲染的布局中并不支持<merge/>標簽。
2.ViewStub.infalte方法不能調(diào)用兩次,否則會出現(xiàn)異常。(因為一旦ViewStub visible/inflated,則ViewStub將從視圖框架中移除,其id也會失效)問:ViewStub標簽和View.GONE的區(qū)別?
答:ViewStub標簽只有在顯示時才去渲染整個布局,但是View.GONE在初始化布局時就已經(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)用啟動分析與優(yōu)化
App啟動方式
通常來說, 一個App啟動也會分如下兩種不同的狀態(tài):
1)冷啟動
當啟動應(yīng)用時,后臺沒有該應(yīng)用的進程,這時系統(tǒng)會重新創(chuàng)建一個新的進程分配給該應(yīng)用,這個啟動方式就是冷啟動。冷啟動因為系統(tǒng)會重新創(chuàng)建一個新的進程分配給它,所以會先創(chuàng)建和初始化Application類,再創(chuàng)建和初始化MainActivity類(包括一系列的測量、布局、繪制),最后顯示在界面上。2.)熱啟動
當啟動應(yīng)用時,后臺已有該應(yīng)用的進程(例:按back鍵、home鍵,應(yīng)用雖然會退出,但是該應(yīng)用的進程是依然會保留在后臺,可進入任務(wù)列表查看),所以在已有進程的情況下,這種啟動會從已有的進程中來啟動應(yīng)用,這個方式叫熱啟動。熱啟動因為會從已有的進程中來啟動,所以熱啟動就不會走Application這步了,而是直接走MainActivity(包括一系列的測量、布局、繪制),所以熱啟動的過程只需要創(chuàng)建和初始化一個MainActivity就行了,而不必創(chuàng)建和初始化Application,因為一個應(yīng)用從新進程的創(chuàng)建到進程的銷毀,Application只會初始化一次。
啟動時間的測量
關(guān)于Activity啟動時間的定義
對于Activity來說,啟動時,首先執(zhí)行的是onCreate()、onStart()、onResume()這些生命周期函數(shù),但即使這些生命周期方法回調(diào)結(jié)束了,應(yīng)用也不算已經(jīng)完全啟動,還需要等View樹全部構(gòu)建完畢,一般認為,setContentView中的View全部顯示結(jié)束了,算作是應(yīng)用完全啟動了。
那么這個時間,實際上是Activity啟動,到Layout全部顯示的過程,但是要注意,這里并不包括數(shù)據(jù)的加載,因為很多App在加載時會使用懶加載模式,即數(shù)據(jù)拉取后,再刷新默認的UI。
基于上面的啟動流程我們盡量做到如下幾點
- Application的創(chuàng)建過程中盡量少的進行耗時操作
- 首屏Activity的渲染
關(guān)于Application
Application是程序的主入口,特別是很多第三方SDK都會需要在Application的onCreate里面做很多初始化操作,一般來說我們可以將這些初始化放在一個單獨的線程中處理, 為了方便今后管理,
優(yōu)化的方法,無非是通過以下幾個方面:
1) 異步初始化
這個很簡單,就是讓App在onCreate里面盡可能的少做事情,而利用手機的多核特性,盡可能的利用多線程,例如一些第三方框架的初始化,如果能放線程,就盡量的放入線程中,最簡單的,你可以直接new Thread(),當然,你也可以通過公共的線程池來進行異步的初始化工作,這個是最能夠壓縮啟動時間的方式2) 后臺任務(wù)
使用IntentService不同于Service, 它是工作在后臺線程的.3) 界面預(yù)加載
當系統(tǒng)加載一個Activity的時候,onCreate()是一個耗時過程,那么在這個過程中,系統(tǒng)為了讓用戶能有一個比較好的體驗,實際上會先繪制一些初始界面,類似于PlaceHolder。
系統(tǒng)首先會讀取當前Activity的Theme,然后根據(jù)Theme中的配置來繪制,當Activity加載完畢后,才會替換為真正的界面。所以,Google官方提供的解決方案,就是通過android:windowBackground屬性,來進行加載前的配置,同時,這里不僅可以配置顏色,還能配置圖片.
自定義View如何優(yōu)化內(nèi)存?
- 不建議在draw或者layout的過程中去實例化對象