<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->android->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布局
<linearlayout xmlns:android="http:// schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<linearlayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<textview
android:id="@+id/layout_per_txt_1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="title"
android:textsize="20sp" />
<linearlayout
android:layout_width="match_parent"
android:orientation="vertical">
<textview
android:id="@+id/layout_per_txt_2"
android:layout_width="fill_parent"
android:layout_height="50dp"
android:text="des"/>
<imageview
android:id="@+id/imageview_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher" />
</linearlayout>
</linearlayout>
</linearlayout>
這是一個簡單的布局頁面,在根視圖上嵌套了兩個linearlayout,并且有兩層顯示界面,如圖2-28所示。
使用2.3.1節介紹的方法通過hierarchy view來檢視下層級情況,如圖2-29所示。
圖2-29 hierarchy view檢查結果
由圖2-29可以看到一共有7級,使用relativelayout進行優化,達到相同的布局效果,并且relativelayout允許子元素指定它們相對于其他元素或父元素的位置,有最大自由度的布局屬性,而且布局層次最淺,占用記憶體最少。修改見代碼清單2-2。
代碼清單2-2 使用relativelayou布局
<relativelayout xmlns:android="http:// schemas.android.com/apk/res/android"
<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" />
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"/>
<imageview
android:id="@+id/imageview_2"
android:layout_width="wrap_content"
android:layout_below="@id/layout_per_txt_2"
android:background="@mipmap/ic_launcher" />
</relativelayout>
這樣就可以減少兩個層級,用一個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布局的基礎上增加如下代碼:
<com.ycl.androidtech.ui.topbar
android:id="@+id/lay_out_topbar"
android:layout_width="fill_parent"
android:layout_height="50dp"/>
……………
</relativelayout >
其中topbar的xml布局如下:
<?xml version="1.0" encoding="utf-8"?>
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="@dimen/topbar_height">
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" />
android:id="@+id/titletextview"
android:layout_centervertical="true"
android:text="标題"/>
顯示結果如圖2-31所示。這種布局在一些清單的item中非常常見,而且清單中item本身的層級比較深,是以優化顯得更有意義。
我們使用hierarchyview檢視增加topbar後的布局層級,如圖2-32所示。可以看到,就是這麼簡單的一個布局,卻把層級增加了兩級,從圖2-32中很明顯地看出topbar後一層的linearlayout是多餘的,這時可以使用merge把這一層消除。
圖2-32 增加topbar後的布局層級
使用merge來優化布局,使用merge标簽替換linearlayout後,原來的linearlayout屬性也沒有用了,修改後的代碼如代碼清單2-3。
代碼清單 2-3
<merge xmlns:android="http:// schemas.android.com/apk/res/android"
android:background="@drawable/img_top_back" />
android:singleline="true"
android:text="标題"
android:textsize="18sp" />
</merge>
運作後再使用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
<include
android:id="@+id/topbar"
layout="@layout/common_top_bar"
android:layout_height="@dimen/topbar_height" />
<viewstub
android:id="@+id/viewstub_text"
android:layout="@layout/viewstub_text_layout1"/>
android:id="@+id/viewstub_image"
android:layout="@layout/layout_bitmap_show"/>
在調用時,根據需求切換不同的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的布局複用可以通過<include>标簽來實作,就像提取代碼公用部分一樣,在編寫android布局檔案時,也可以将相同的部分提取出來,在使用時,用<include>添加進去。例如:
……
</ linearlayout >
例如,在大部分應用中,基本上所有的應用都會帶有頭部欄(topbar),主要是顯示标題和傳回鍵功能,這樣隻需要維護一份代碼,就可以修改所有的顯示效果。這個示例的topbar布局xml如下:
android:background="@color/black"
android:ellipsize="end"
android:gravity="center"
android:textcolor="#ffffffff"
類似于topbar的這類常用控件,包括菜單,可以把具體實作抽象到頁面的基類(baseactivity)中,這樣布局和具體的實作都收歸到一個地方,友善維護。
提高布局效率的方法總體來說就是減少層級,提高繪制速度和布局複用。影響布局效率主要有以下幾點:
布局的層級越少,加載速度越快。
減少同一層級控件的數量,加載速度會變快。
一個控件的屬性越少,解析越快。
根據本節的分析,對優化的總結如下:
盡量多使用relativelayout或linearlayout,不要使用絕對布局absolutelayout。
将可複用的元件抽取出來并通過< include />标簽使用。
使用< viewstub />标簽加載一些不常用的布局。
使用< merge />标簽減少布局的嵌套層次。
盡可能少用wrap_content,wrap_content會增加布局measure時的計算成本,已知寬高為固定值時,不用wrap_content。
删除控件中的無用屬性。