天天看點

Android應用性能優化最佳實踐.2.3 布局優化

<b>2.3 布局優化</b>

<b></b>

布局是否合理主要影響的是頁面測量時間的多少,我們知道一個頁面的顯示測量和繪制過程都是通過遞歸來完成的,多叉樹周遊的時間與樹的高度h相關,其時間複雜度為o(h),如果層級太深,每增加一層則會增加更多的頁面顯示時間。

任何時候view中的繪制内容發生變化時,都需要重新建立displaylist、渲染displaylist,更新到螢幕上等一系列操作。這個流程的表現性能取決于view的複雜程度、view的狀态變化以及渲染管道的執行性能。例如,假設某個button的大小需要增大到目前的兩倍,在增大button大小之前,需要通過父view重新計算并擺放其他子view的位置。修改view的大小會觸發整個hierarcyview的重新計算大小的操作。如果是修改view的位置,則會觸發hierarchview重新計算其他view的位置。如果布局很複雜,就很容易導緻嚴重的性能問題。

在優化前首先講解兩個布局優化的常用工具。

2.3.1 常用布局優化工具

1.?hierarchy viewer

hierarchy viewer是android sdk自帶的一款可視化調試工具,用來檢查layout嵌套及繪制時間,以可視化的布局角度直覺擷取layout布局設計和各種屬性資訊,開發者在調試和布局ui界面時可以很友善地使用,提高使用者的開發效率。

出于安全考慮,hierarchy viewer隻能連接配接android開發版手機或模拟器。

在應用程式debug模式中,無法啟動hierarchy viewer。

接下來一步步介紹如何使用hierarchy viewer。

step1:構造頁面

先構造一個簡單的頁面layoutperactivity,該頁面如圖2-20所示,然後啟動應用,進入這個頁面。

圖2-20 頁面顯示

step2:打開hierarchy view

eclipse和android studio都帶有hierarchy view工具,下面介紹在這兩款ide上打開hierarchy view的方法。

android studio

在android studio上可以直接在快捷工具欄打開android device monitor,如圖2-21所示。android device monitor上就有hierarchy view視圖,可直接檢視。

或者選擇tools-&gt;android-&gt;android device monitor菜單,直接打開hierarchy view。

eclipse

在eclipse的adt android插件中,不能直接啟動hierachy viewer,可以從android sdk工具包中,通過指令行的方式啟動,在android sdk下的tools目錄下,在指令行方式下運作hierachyviewer。

使用as打開後的整體視窗如圖2-22所示。

圖2-22 hierarchy view整體視窗

可以看到4個視窗,各個視窗的功能如下:

windows:顯示目前裝置資訊,以及目前裝置的所有頁面清單。

view properties:目前選中view的屬性。

treeview:把activity中所有控件(view)的層次結構從左到右顯示出來,其中最右邊部分是最底層的控件(view)。

tree overview:全局概覽,以縮略圖的方式顯示整個應用中各控件的層次關系,并且框出treeview視窗中顯示部分在全局中的位置,如果一個界面中的控件和層級比較多,可以通過滑鼠移動這個顯示區域移動。

layout view:整體layout布局圖,以手機螢幕上真實位置呈現出來,在treeview中選中某一個控件時,會在layout view用紅色的框标注。

step3:使用hierarchy viewer檢視層級和耗時

檢視層級圖:在windows視窗頁,選擇需要檢視的元件,輕按兩下或單擊load view hierarchy按鈕即可打開。輕按兩下後在tree view界面中,從左到右把所有控件層級圖顯示出來,這樣就可以看到整體界面的層級深度。

檢視某個view的耗時:在快捷鍵工具欄中單擊obtain layout times for tree rooted at selected node按鈕,如圖2-23所示。

圖2-23 檢視單個view的耗時

這時可以看到tree view中的頁面增加了屬性,單擊一個控件,可以看到這個view的耗時情況。

根據圖2-24從上往下看,1 view表示這個控件是這個樹下的最後一個控件,即表示是它本身,下面的時間表示measure、layout以及draw三個階段的耗時。最後一個框有不同色的三個訓示燈,分别對應目前控件在測量、布局以及畫視圖三個階段,顔色表示這個控件占用的時間百分比,如果是綠色的,表示該控件在該階段比其他50%的控件的速度要快,黃色表示比其他50%的控件的速度要慢,紅色表示該控件在該階段的處理速度是最慢的,就需要注意了。

到這裡我們就知道如何使用hierarchy view來分析一個頁面的層級和耗時,并且使用這個工具,使用者可以很友善地檢視和調試應用中的ui界面,分析其性能,建議開發者在開發階段也使用這款工具。但一個應用的界面非常多,如果一個個這樣分析的話效率非常低,是以再介紹另外一個工具lint,用于檢查所有頁面的層級,并把深度高于n(自定義)的界面輸出,然後通過hierarchy view工具來仔細分析。

2.?布局層級檢查

android lint是android sdk tools 16(adt 16)之後引入的代碼檢查工具,通過代碼靜态檢查,可以發現潛在的代碼問題,并給出優化建議。android-lint檢查工具使用的方式有以下兩種:

指令行使用腳本執行。

在ide中使用視圖化工具。

lint的檢查結果分為6類,如圖2-25所示。

圖2-25 android lint示意圖

correctness(正确性)

security(安全性)

performance(性能)

usability(可用性)

accessibility(可達性)

國際化

問題的嚴重程度(severity)從高到低依次是:

fatal

error

warning

information

ignore

掃描規則和缺陷級别可以在file→settings→inspections→android lint中配置,lint的功能非常強大,強烈建議開發者深入學習使用方法,這裡隻講解如何使用android lint來發現xml布局檢查。使用lint掃描前,先配置需要檢查的項目,隻需要檢查layout層級深度。先進入file→settings→inspections→android lint。

如圖2-26所示,這裡的配置隻掃描layout的層級和view的個數,如下所示:

toodeeplayout:表示布局太深,預設層級超過10層會提示該問題,可以自定義環境變量android_lint_max_depth來修改。布局深度增加會導緻記憶體消耗也随之增加,是以布局盡可能淺而寬。

toomanyviews:表示控件太多,預設超過80個控件會提示該問題。

在android studio中啟動lint,從菜單欄選擇analyze→inspect code,進去後可以指定掃描的範圍,可以是整個工程,也可以是一個module或單獨的檔案。啟動掃描,掃描結果如圖2-27所示。

圖2-26 設定lint規則

可以很清楚地顯示哪個layout有問題,進而打開對應檔案并修改。介紹完兩個工具,接下來我們從多個方面來優化ui,讓應用使用更流暢。

2.3.2 布局優化方法

在android應用開發中,常用的布局方式主要有linearlayout、relativelayout、framelayout等,通過這些布局可以實作各種各樣的界面。我們需要知道如何高效地使用這些布局方式來組織ui控件,布局的好壞影響到繪制的時間,本節将通過減少layout層級,減少測量、繪制時間,提高複用性三個方面來優化布局,優化的目的就是減少層級,讓布局扁平化,以提高繪制的時間,提高布局的複用性節省開發和維護成本。

1.?減少層級

層級越少,測試和繪制的時間就越短,通常減少層級有以下兩個常用方案:

合理使用relativelayout和linearlayout。

合理使用merge。

(1)relativelayout與linearlayout

使用linearlayout布局的具體方法,見代碼清單2-1。

代碼清單2-1 使用linearlayout布局

&lt;linearlayout xmlns:android="http:// schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical"&gt;

    &lt;linearlayout

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:orientation="horizontal"&gt;

        &lt;textview

            android:id="@+id/layout_per_txt_1"

            android:layout_width="wrap_content"

            android:layout_height="100dp"

            android:text="title"

            android:textsize="20sp" /&gt;

        &lt;linearlayout

            android:layout_width="match_parent"

            android:orientation="vertical"&gt;

            &lt;textview

                android:id="@+id/layout_per_txt_2"

                android:layout_width="fill_parent"

                android:layout_height="50dp"

                android:text="des"/&gt;

            &lt;imageview

                android:id="@+id/imageview_2"

                android:layout_width="wrap_content"

                android:layout_height="wrap_content"

                android:background="@mipmap/ic_launcher" /&gt;

        &lt;/linearlayout&gt;

    &lt;/linearlayout&gt;

&lt;/linearlayout&gt;

這是一個簡單的布局頁面,在根視圖上嵌套了兩個linearlayout,并且有兩層顯示界面,如圖2-28所示。

使用2.3.1節介紹的方法通過hierarchy view來檢視下層級情況,如圖2-29所示。

圖2-29 hierarchy view檢查結果

由圖2-29可以看到一共有7級,使用relativelayout進行優化,達到相同的布局效果,并且relativelayout允許子元素指定它們相對于其他元素或父元素的位置,有最大自由度的布局屬性,而且布局層次最淺,占用記憶體最少。修改見代碼清單2-2。

代碼清單2-2 使用relativelayou布局

&lt;relativelayout xmlns:android="http:// schemas.android.com/apk/res/android"

    &lt;textview

        android:id="@+id/layout_per_txt_1"

        android:layout_width="100dp"

        android:layout_height="100dp"

        android:background="@color/yellow"

        android:text="title"

        android:textcolor="#ffff0000"

        android:textsize="20sp" /&gt;

        android:id="@+id/layout_per_txt_2"

        android:layout_height="50dp"

        android:layout_toendof="@id/layout_per_txt_1"

        android:layout_torightof="@id/layout_per_txt_1"

        android:background="@color/blue"

        android:text="des"/&gt;

    &lt;imageview

        android:id="@+id/imageview_2"

        android:layout_width="wrap_content"

        android:layout_below="@id/layout_per_txt_2"

        android:background="@mipmap/ic_launcher" /&gt;

&lt;/relativelayout&gt;

這樣就可以減少兩個層級,用一個relativelayout就可以達到顯示的效果,再使用hierarchy view來檢視層級,可以看到減少到5層,如圖2-30所示。

圖2-30 優化後的hierarchy view檢查結果

但reativelayout也存在性能低的問題,原因是relativelayout會對子view做兩次測量,在relativelayout中子view的排列方式是基于彼此的依賴關系,因為這個依賴關系可能和布局中view的順序并不相同,在确定每個子view的位置時,需要先給所有子view做一次排序。如果在relativelayout中允許子view橫向和縱向互相依賴,就需要橫向、縱向分别進行一次排序測量。但如果在linearlayout中有weight屬性,也需要進行兩次測量,因為沒有更多的依賴關系,是以仍然會比relativelayout的效率高,在布局上relativelayout不如linearlayout快。

但是如果布局本身層次太深,還是推薦用relativelayout減少布局本身層次,相較于測量兩次,雖然會增加一些計算時間,但在體驗上影響不會特别大,如果優化掉兩層僅僅是增加一次測量,還是非常值得的,布局層次深會增加記憶體消耗,甚至引起棧溢出等問題,即使耗點時間,也不能讓應用不可用。

根據以上分析,可以總結出以下幾點布局原則:

盡量使用relativelayout和linearlayout。

在布局層級相同的情況下,使用linearlayout。

用linearlayout有時會使嵌套層級變多,應該使用relativelayout,使界面盡量扁平化。

由于android的碎片化程度很高,市面上的螢幕尺寸也是各式各樣,使用relativelayout能使建構的布局适應性更強,建構出來的ui布局對多螢幕的适配效果更好,通過指定ui控件間的相對位置,使不同螢幕上布局的表現基本保持一緻。當然,也不是所有情況下都得使用相對布局,根據具體情況選擇和搭配使用其他布局方式來實作最優布局。

(2)merge的使用

從名字上就可以看出,merge就是合并的意思。使用它可以有效優化某些符合條件的多餘的層級。使用merge的場合主要有以下兩處:

在自定義view中使用,父元素盡量是framelayout或者linearlayout。

在activity中整體布局,根元素需要是framelayout。

我們仍以前面的布局為例,在頁面增加一個自定義控件topbar,故在代碼清單2-2布局的基礎上增加如下代碼:

    &lt;com.ycl.androidtech.ui.topbar

        android:id="@+id/lay_out_topbar"

        android:layout_width="fill_parent"

        android:layout_height="50dp"/&gt;

    ……………

&lt;/relativelayout &gt;

其中topbar的xml布局如下:

&lt;?xml version="1.0" encoding="utf-8"?&gt;

    android:orientation="horizontal" android:layout_width="match_parent"

    android:layout_height="@dimen/topbar_height"&gt;

        android:layout_gravity="center"

        android:id="@+id/backimg"

        android:layout_width="60dp"

        android:layout_height="44dp"

        android:background="@drawable/img_top_back"

        android:focusable="true" /&gt;

        android:id="@+id/titletextview"

        android:layout_centervertical="true"

        android:text="标題"/&gt;

顯示結果如圖2-31所示。這種布局在一些清單的item中非常常見,而且清單中item本身的層級比較深,是以優化顯得更有意義。

我們使用hierarchyview檢視增加topbar後的布局層級,如圖2-32所示。可以看到,就是這麼簡單的一個布局,卻把層級增加了兩級,從圖2-32中很明顯地看出topbar後一層的linearlayout是多餘的,這時可以使用merge把這一層消除。

圖2-32 增加topbar後的布局層級

使用merge來優化布局,使用merge标簽替換linearlayout後,原來的linearlayout屬性也沒有用了,修改後的代碼如代碼清單2-3。

代碼清單 2-3

&lt;merge xmlns:android="http:// schemas.android.com/apk/res/android"

        android:background="@drawable/img_top_back"  /&gt;

        android:singleline="true"

        android:text="标題"

        android:textsize="18sp" /&gt;

&lt;/merge&gt;

運作後再使用hierarchy view檢視目前層級,如圖2-33所示。

圖2-33 合并後的布局層級

這樣就把多餘的linearlayout消除了,原理是在android布局的源碼中,如果是merge标簽,那麼直接将其中的子元素添加到merge标簽parent中,這樣就保證了不會引入額外的層級。

如果merge代替的布局元素為linearlayout,在自定義布局代碼中将linearlayout的屬性添加到引用上,如垂直或水準布局、背景色等。

但merge不是所有地方都可以任意使用,有以下幾點要求:

merge隻能用在布局xml檔案的根元素。

使用merge來加載一個布局時,必須指定一個viewgroup作為其父元素,并且要設定加載的attachtoroot參數為true(參照inf?late(int, viewgroup, boolean))。

不能在viewstub中使用merge标簽。原因就是viewstub的inf?late方法中根本沒有attachtoroot的設定。

這一節講了如何減少層級,那麼在android系統中,多少層才是合理的呢?當然是越少越好,但從lint檢查的配置上看,超過10層才會報警,實際上在開發時,随着産品設計的豐富和多樣性,很容易超過10層,根據實際開發過程中超過15層就要重視并準備做優化,20層就必須修改了。在實在沒有辦法優化的情況下,需要把複雜的層級用自繪控件來實作,自繪控件中的圖層層級再多,在布局上也隻是一層,但這樣也會帶來過度繪制的問題,在後一章節中會重點介紹這個問題的優化方案。

在activiy的總布局中使用merge,但又想設定整體的屬性(布局方式或背景色),可以不使用setcontentview方法加載layout,而使用(id/content)将framelayout取出來,在代碼中手動加載布局,但如果層級壓力不大(小于10級),則沒有必要,因為這樣代碼的維護性較差。

2.?提高顯示速度

我們在開發的過程中會碰到這樣的場景或者顯示邏輯:某個布局當中的子布局非常多,但并不是所有元素都同時顯示出來,而是二選一或者n選一,打開這個界面根據不同的場景和屬性顯示不同的layout。例如:一個頁面對不同的使用者(未登入、普通使用者、會員)來說,顯示的布局不同。或者,有些使用者喜歡對不同的元素使用invisible或者gone隐藏,通過設計元素的visable屬性來控制,這樣雖然達到了隐藏的目的,但效率非常低,原因是即使将元素隐藏,它們仍在布局中,仍會測試和解析這些布局。android提供了viewstub控件來解決這個場景。

viewstub是一個輕量級的view,它是一個看不見的,并且不占布局位置,占用資源非常小的視圖對象。可以為viewstub指定一個布局,加載布局時,隻有viewstub會被初始化,然後當viewstub被設定為可見時,或是調用了viewstub.inf?late()時,viewstub所指向的布局會被加載和執行個體化,然後viewstub的布局屬性都會傳給它指向的布局。這樣,就可以使用viewstub來設定是否顯示某個布局。

代碼清單2-4是兩個viewstub通過不同的初始化來加載兩個不同的布局,以滿足使用者的需求。

代碼清單2-4 使用viewstub

    &lt;include

        android:id="@+id/topbar"

        layout="@layout/common_top_bar"

        android:layout_height="@dimen/topbar_height" /&gt;

    &lt;viewstub

        android:id="@+id/viewstub_text"

        android:layout="@layout/viewstub_text_layout1"/&gt;

        android:id="@+id/viewstub_image"

        android:layout="@layout/layout_bitmap_show"/&gt;

在調用時,根據需求切換不同的layout,這樣可以提高頁面初始化的速度,使用代碼如下:

view view = inflater.inflate(r.layout.fm_xml_show, container, false);

if (changeview) {

    viewstub stub = (viewstub) view.findviewbyid(r.id.viewstub_text);

    stub.inflate();

    changeview = false;

} else {

    viewstub stub = (viewstub) view.findviewbyid(r.id.viewstub_image);

    changeview = true;

}

viewstub顯示有兩種方式,上面代碼使用的是inf?late方法,也可以直接使用viewstub.setvisibiltity(view.visible)方法。

使用viewstub時需要注意以下幾點:

viewstub隻能加載一次,之後viewstub對象會被置為空。換句話說,某個被viewstub指定的布局被加載後,就不能再通過viewstub來控制它了。是以它不适用于需要按需顯示隐藏的情況。

viewstub隻能用來加載一個布局檔案,而不是某個具體的view,當然也可以把view寫在某個布局檔案中。如果想操作一個具體的view,還是使用visibility屬性。

viewstub中不能嵌套merge标簽。

不過這些限制都無傷大雅,我們還是能夠用viewstub來做很多事情,viewstub的主要使用場景如下:

在程式運作期間,某個布局在加載後,就不會有變化,除非銷毀該頁面再重新加載。

想要控制顯示與隐藏的是一個布局檔案,而非某個view。

因為viewstub隻能inf?late一次,之後會被置空,無法繼續使用viewstub來控制布局。是以當需要在運作時不止一次顯示和隐藏某個布局時,使用viewstub是無法實作的。這時隻能使用view的可見性來控制。

3.?布局複用

我們在開發應用時還會碰到另一個常見的場景,就是一個相同的布局在很多頁面(activity或fragment)會用到,如果給這些頁面的布局檔案都統一加上相同的布局代碼,維護起來就很麻煩,可讀性也差,一旦需要修改,很容易有漏掉的地方,android的布局複用可以通過&lt;include&gt;标簽來實作,就像提取代碼公用部分一樣,在編寫android布局檔案時,也可以将相同的部分提取出來,在使用時,用&lt;include&gt;添加進去。例如:

    ……

&lt;/ linearlayout &gt;

例如,在大部分應用中,基本上所有的應用都會帶有頭部欄(topbar),主要是顯示标題和傳回鍵功能,這樣隻需要維護一份代碼,就可以修改所有的顯示效果。這個示例的topbar布局xml如下:

    android:background="@color/black"

        android:ellipsize="end"

        android:gravity="center"

        android:textcolor="#ffffffff"

類似于topbar的這類常用控件,包括菜單,可以把具體實作抽象到頁面的基類(baseactivity)中,這樣布局和具體的實作都收歸到一個地方,友善維護。

提高布局效率的方法總體來說就是減少層級,提高繪制速度和布局複用。影響布局效率主要有以下幾點:

布局的層級越少,加載速度越快。

減少同一層級控件的數量,加載速度會變快。

一個控件的屬性越少,解析越快。

根據本節的分析,對優化的總結如下:

盡量多使用relativelayout或linearlayout,不要使用絕對布局absolutelayout。

将可複用的元件抽取出來并通過&lt; include /&gt;标簽使用。

使用&lt; viewstub /&gt;标簽加載一些不常用的布局。

使用&lt; merge /&gt;标簽減少布局的嵌套層次。

盡可能少用wrap_content,wrap_content會增加布局measure時的計算成本,已知寬高為固定值時,不用wrap_content。

删除控件中的無用屬性。