Android開發中的佈局很重要嗎?那是當然。一切的顯示樣式都是由這個佈局決定的,你說能不重要嗎。要實現一個好的佈局,不只是實現了、顯示出來就完了,不管層次,堆砌代碼也可以實現功能,但是這顯然違背了Android佈局設計的原則。可能你會說,Android佈局設計哪有什麼原則,我可以明確告訴你,當然有,只要有利於提高最終效果的方法、意識,我們都可以把它提升爲原則。在Android佈局設計中,這個最終效果就是快的頁面加載速度,好的流暢度,而這個方法就太多了,鑑於本人水平有限,下面就說幾種自己實踐過的、切實有效的方法吧。前奏太長,羅哩羅嗦,好吧,開始說起。
最近做演出項目,碰到了一個以前沒見過的錯誤StackOverflowError,具體錯誤如下:
查過資料之後才發現,原因就出在佈局的嵌套層次上,說明白點兒就是“嵌套層次過深,導致棧溢出”,這樣就引出了一個問題,我們在Android開發中經常聽到的是內存泄漏、堆溢出(聽起來都頭皮發麻的OOM),好像很少聽說有棧溢出的,好了,找找Android中對棧大小的限制吧。抱歉,沒找到,只在支離破碎的國外論壇中間看到有說1K的,有說4K的,反正沒看到有官方的說明,糾結良久還是一籌莫展,看哪位大蝦如果知道的話就指點一下本鳥吧。。。。。。既然遇到了問題,還是轉回頭來解決問題吧,無法提高棧的大小,那就只能提高棧的使用率了,最直接的方法就是縮減層級,而且縮減層級還有一個重要的好處就是提高頁面加載速度,因爲Android中的佈局是嵌套加載的,多一層佈局就要耗費很長的加載時間。這時候我才體會到了merge的妙用,不只是merge,下面我會結合hierarchyviewer工具來分別講述一下merge,ViewStub,include在佈局優化中的作用。
1、merge
顧名思義,就是合併、融合的意思。使用它可以有效的將某些符合條件的多餘的層級優化掉。使用merge的場合主要有兩處:
(1) 自定義View中使用,父元素儘量是FrameLayout,當然如果父元素是其他佈局,而且不是太複雜的情況下也是可以使用的
(2) Activity中的整體佈局,根元素需要是FrameLayout
下面這個例子將融合這兩種情況,來展示如何縮減佈局層次。
總體顯示界面如圖所示:
其中粉紅色圈住的部分爲我們的自定義View。
整個界面的佈局layout_mergedemo.xml如下:
自定義佈局view_item.xml如下:
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
- <TextView
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:textSize="25sp"
- android:textColor="#FF0000FF"
- android:gravity="center_horizontal"
- android:text="我的家園"/>
- <com.example.myandroiddemo.MyItemView
- android:id="@+id/myitem_view"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center" />
- </FrameLayout>
自定義View中使用下面來解析佈局:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="horizontal" >
- <ImageView
- android:id="@+id/view_item_img"
- android:layout_width="50dp"
- android:layout_height="50dp"
- android:scaleType="fitXY"/>
- <TextView
- android:id="@+id/view_item_txt"
- android:layout_width="fill_parent"
- android:layout_height="50dp"
- android:gravity="center_vertical"
- android:layout_marginLeft="10dp"
- android:textSize="20sp"
- android:textColor="#FFFF0000"/>
- </LinearLayout>
- public class MyItemView extends LinearLayout {
- ......
- private void initView(Context context) {
- mContext = context;
- View view = LayoutInflater.from(mContext).inflate(R.layout.view_item, this, true);
- mMyItemImg = (ImageView)view.findViewById(R.id.view_item_img);
- mMyItemText = (TextView)view.findViewById(R.id.view_item_txt);
- }
- ......
- }
整個功能開發完成之後,使用hierarchyviewer來看一下佈局層次:
我們發現簡單的一個功能竟然使用了六層佈局,包括每個Window自動添加的PhoneWindow$DecorView和FrameLayout(id/content)。明顯可以看到這個佈局存在冗餘,比如第二層和第三層的FrameLayout,比如第四層的MyItemView(LinearLayout子類)和第五層的LinearLayout,都可以縮減。
好了,該我們的merge標籤大顯身手了。將佈局這樣修改:簡化layout_mergedemo.xml:
簡化view_item.xml:
- <merge xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools">
- <TextView
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:textSize="25sp"
- android:textColor="#FF0000FF"
- android:gravity="center_horizontal"
- android:text="我的家園"/>
- <com.example.myandroiddemo.MyItemView
- android:id="@+id/myitem_view"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center" />
- </merge>
- <merge xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools" >
- <ImageView
- android:id="@+id/view_item_img"
- android:layout_width="50dp"
- android:layout_height="50dp"
- android:scaleType="fitXY"/>
- <TextView
- android:id="@+id/view_item_txt"
- android:layout_width="fill_parent"
- android:layout_height="50dp"
- android:gravity="center_vertical"
- android:layout_marginLeft="10dp"
- android:textSize="20sp"
- android:textColor="#FFFF0000"/>
- </merge>
因爲這裏merge代替的佈局元素爲LinearLayout,而不是FrameLayout,所以我們需要在自定義佈局代碼中將LinearLayout的屬性添加上,比如垂直或者水平佈局,比如背景色等,此處設置爲水平佈局:
- private void initView(Context context) {
- setOrientation(LinearLayout.HORIZONTAL);
- ……
- }
OK,就是這麼簡單,再來看看佈局層次吧:
哈哈,只有四層了,加載速度也明顯加快,特別是如果佈局比較複雜,子View較多的情況下,合理使用merge能大大提高程序的速度和流暢性。
但是使用merge標籤還是有一些限制的,具體有以下幾點:
(1)merge只能用在佈局XML文件的根元素
(2)使用merge來inflate一個佈局時,必須指定一個ViewGroup作爲其父元素,並且要設置inflate的attachToRoot參數爲true。(參照inflate(int, ViewGroup, boolean))
(3)不能在ViewStub中使用merge標籤。最直觀的一個原因就是ViewStub的inflate方法中根本沒有attachToRoot的設置
說到這兒,merge的使用也就講完了。還想嘮叨一下做的演出項目,因爲本人水平有限,並且產品提的這個顯示需求確實比較BT,所以整個項目的顯示框架做了很多嵌套,具體點就是ActivityGroup中嵌套ViewFlipper,然後再嵌套TabHost,然後再嵌套自己的Activity,大體數了一下,最多竟然有20幾層,My God!知道Android佈局設計的原則是什麼嗎?最好是10層以下,儘量不要超過15層,如果再多性能就會下降,也可能會出現問題,就是我們看到的StackOverFlow,這個項目被證實也確實在某些低端機上發生了這種錯誤,比如HTC的某某機型,OPPO的某某機型,(不要聲討我,沒有惡意貶低的意思<_>),最終我使用merge縮呀縮呀,把大部分的佈局都縮減到了15層以下,一測試,通過!
還要說一下,因爲Window窗體(比如Activity)加載時會自動添加PhoneWindow$DecorView和FrameLayout(id/content)兩層佈局,所以如果我們在Activity的自定義佈局根元素中使用merge,而且想設置總體背景什麼的,可以用(id/content)將FrameLayout取出來,再設置屬性,可以這樣實現:
- //setContentView(R.layout.layout_showset);
- FrameLayout frameLayout = (FrameLayout)this.getWindow().getDecorView().findViewById(android.R.id.content);
- frameLayout.setBackgroundResource(R.drawable.bg_repeated_main);
- LayoutInflater.from(this).inflate(R.layout.layout_showset, frameLayout, true);
好了,就這樣吧!!!
2、ViewStub
這是什麼玩應兒呢?其實就是一個輕量級的頁面,我們通常使用它來做預加載處理,來改善頁面加載速度和提高流暢性,ViewStub本身不會佔用層級,它最終會被它指定的層級取代。
還是說說演出項目吧,還說?對了,實踐才能發現問題嘛,在哪兒發現問題就在那兒改進。由於項目中用到了比較多的動畫,而且嵌套佈局比較複雜,所以在Android低端機上進行頁面切換時候經常讓人感覺卡卡的,不怎麼流暢,因爲頁面切換動畫和標題旋轉動畫是同時進行的,所以爲了達到更好的體驗就需要使用一種方法,在動畫進行時儘量的減少其他操作,特別是頁面加載重繪。趕緊想辦法,起初我想先將要加載的頁面所有的組件都初始爲gone顯示狀態,整個頁面只留一下加載滾動條,後來發現這是不行滴,因爲在Android的機制裏,即使是將某一個控件的visibility屬性設置爲不可見的gone,在整個頁面加載過程中還是會加載此控件的。再後來就用到了ViewStub,在做頁面切換動畫時,只在頁面中放一個loading加載圖標和一個ViewStub標籤,像下面這樣:
layout_loading.xml佈局文件:
- <merge xmlns:android="http://schemas.android.com/apk/res/android">
- <ViewStub
- android:id="@+id/viewstub"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"/>
- <NetErrAndLoadView
- android:id="@+id/start_loading_lay"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" />
- </merge>
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.layout_loading);
- mLoadHandler = new Handler();
- mLoadingView = (NetErrAndLoadView)findViewById(R.id.start_loading_lay);
- mLoadingView.startLoading();
- mViewStub = (ViewStub)findViewById(R.id.viewstub);
- mViewStub.setLayoutResource(R.layout.layout_main);
- mLoadHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- mViewStub.inflate();
- mLoadingView.hide();
- }
- },500);
- }
ViewStub也是有少許缺點的,下面所說:
1、 ViewStub只能Inflate一次,之後ViewStub對象會被置爲空。按句話說,某個被ViewStub指定的佈局被Inflate後,就不能夠再通過ViewStub來控制它了。所以它不適用 於需要按需顯示隱藏的情況。
2、 ViewStub只能用來Inflate一個佈局文件,而不是某個具體的View,當然也可以把View寫在某個佈局文件中。如果想操作一個具體的view,還是使用visibility屬性吧。
3、 VIewStub中不能嵌套merge標籤。(前面好像說過)
不過這些確定都無傷大雅,我們還是能夠用ViewStub來做很多事情。
有時候真的不得不佩服google的Android團隊的遠見性和架構性,這種細微的設計都能考慮的到,想用就有。
3、include
製造這個標籤純碎是爲了佈局重用,在項目中通常會存在一些佈局公用的部分,比如自定義標題,我們不需要把一份代碼Ctrl C, Ctrl V的到處都是,嚴重違背程序簡潔化、模塊兒化的設計思想,畢竟作爲現代化的碼農,也需要與時俱進,不再抱着以前那種賣代碼的思想來編寫程序了(一行代碼幾個錢?)。這裏就不寫例子了,隨便網上搜一下,include的使用一大堆,簡單說一下如何調用吧,比如現在有一個公用佈局head.xml,如果在其他的佈局中引用,只需要這樣添加include標籤:
- <include android:layout_width="fill_parent" android:layout_height="wrap_content" layout="@layout/head" />
OK,到這兒我也囉嗦完了,現在腦子裏只有一個想法,既然Android的設計者都絞盡腦汁幫我們想到了各種便利用途,我們再不使用就不單是腦子有問題的問題了,就是沒把用戶需求放在第一位,是得罪上帝的。哈哈,說大了,輕噴。。。。。。不過這幾個標籤確實很有用,建議在合適的地方都體驗一下,還是那句話,誰用誰知道<_>。
說明:以上都是基礎內容,只不過可能注意用的人不多,這些標籤的使用我也是學習,然後用到了項目當中,本人只是結合自己的理解敘述一遍。如果有更好的改進方法,或者是認爲上面的文字有誤,歡迎交流。