Android中布局UI的優(yōu)化總結(jié)

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

圖片.png

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

圖片.png

一個(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é)果:

圖片.png

如果存在冗余的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)存查看器:

圖片.png

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

圖片.png

如何看到,這種不停的大面積打印GC導(dǎo)致所有線程暫停的操作必定會(huì)導(dǎo)致UI視覺的卡頓,所以我們要避免此類問題的出現(xiàn),具體的常見優(yōu)化方式如下:

  1. 檢查代碼,盡量避免有些頻繁觸發(fā)的邏輯方法中存在大量對(duì)象分配;
  2. 盡量避免在多次for循環(huán)中頻繁分配對(duì)象;
  3. 避免在自定義View的onDraw()方法中執(zhí)行復(fù)雜的操作及創(chuàng)建對(duì)象(譬如Paint的實(shí)例化操作不要寫在onDraw()方法中等);
  4. 對(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ì)象的列表,如下:

圖片.png

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)

    1. Application的創(chuàng)建過程中盡量少的進(jìn)行耗時(shí)操作
    1. 首屏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ì)象
最后編輯于
?著作權(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)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,781評(píng)論 25 709
  • 太長(zhǎng)不看版:在 Android UI 布局過程中,遵守一些慣用、有效的布局原則,可以制作出高效且復(fù)用性高的 UI。...
    Mupceet閱讀 4,023評(píng)論 0 14
  • 注意事項(xiàng): 布局優(yōu)化;盡量使用include、merge、ViewStub標(biāo)簽,盡量不存在冗余嵌套及過于復(fù)雜布局(...
    HarryXR閱讀 5,351評(píng)論 1 19
  • 原文鏈接:https://yuchao.wang/article?id=37 應(yīng)用瘦身 首先打包 xxx.apk ...
    wangyuchao閱讀 1,069評(píng)論 0 0
  • 從2013年-2017年,四年時(shí)光,轉(zhuǎn)眼即逝。 或許因?yàn)樽约哼€要在學(xué)校待三年,所以,未曾想過自己會(huì)傷感,可能是被她...
    Xiangyan閱讀 336評(píng)論 0 0

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