Androd 勇闖高階性能優化之佈局優化篇
Android的佈局管理器本身就是個UI組件,所有的佈局管理器都是ViewGroup的子類,而ViewGroup是View的子類,所以佈局管理器可以當成普通的UI組件使用,也可以作為容器類使用,可以調用多個重載addView()向佈局管理器中添加組件,並且佈局管理器可以互相嵌套,當然不推薦過多的嵌套 (如果要兼容低端機型,最好不要超過5層)。
🔥 佈局層級管理
讓咱們一起瞭解一下每當系統繪制一個佈局時,都會發生一些什麼。這一過程由兩個步驟完成:
💥 繪制(Measurement)
- 1:根佈局測量自身。
- 2:根佈局要求它內部所有子組件測量自身。
- 3:所有自佈局都需要讓它們內部的子組件完成這樣的操作,直到遍歷完視圖層級中所有的View。
💥 擺放(Positioning)
- 1:當佈局中所有的View都完成瞭測量,根佈局則開始將它們擺放到合適的位置。
- 2:所有子佈局都需要做相同的事情,直到遍歷完視圖層級中所有的View。
💥 背景設置產生的過度繪制
- 組件背景:每個組件每設置一次背景, 該組件的區域就會增加一層繪制 , 如 LinearLayout 設置背景顏色 , 裡面的 TextView 設置背景顏色 , 都會增加該組件區域內的過渡繪制 ;
- 主題背景:Activity 界面的主題背景,會增加一次 GPU 繪制 ;
不要隨意給佈局中的 UI 組件設置背景 。如 ImageView 設置一張圖片,會增加一次繪制 ,再給該 ImageView 組件設置背景顏色, 那麼又會增加一次繪制, 那麼該 ImageView 組件肯定過渡繪制瞭。
💥 小結
當某個View的屬性發生變化(如:TextView內容變化或ImageView圖像發生變化),View自身會調用View.invalidate()方法(必須從 UI 線程調用),自底向上傳播該請求,直到根佈局(根佈局會計算出需要重繪的區域,進而對整個佈局層級中需要重繪的部分進行重繪)。佈局層級越復雜,UI加載的速度就越慢。因此,在編寫佈局的時候,盡可能地扁平化是非常重要的。
FrameLayout和TableLayout有各自的特殊用途,LinearLayout 和 RelativeLayout 是可以互換的,ConstraintLayout和RelativeLayout類似。也就是說,在編寫佈局時,可以選擇其中一種,咱們可以以不同的方式來編寫下面這個簡單的佈局。
🔥 小實驗(多種方式實現同一佈局)
💥 LinearLayout
第一種方式是使用LinearLayout,雖然可讀性比較強,但是性能比較差。由於嵌套LinearLayout會加深視圖層級,每次擺放子組件時,相對需要消耗更多的計算。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <View android:id="@+id/view_top_1" android:layout_width="match_parent" android:layout_height="100dp" android:background="@color/color_666666"/> <View android:id="@+id/view_top_2" android:layout_width="200dp" android:layout_height="100dp" android:background="@color/teal_200"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <View android:id="@+id/view_top_3" android:layout_width="100dp" android:layout_height="100dp" android:background="@color/color_FF773D"/> <View android:id="@+id/view_top_4" android:layout_width="100dp" android:layout_height="100dp" android:background="@color/purple_500"/> </LinearLayout> </LinearLayout>
LinearLayout視圖層級如下所示:
💥 使用RelativeLayout
第二種方法使用RelativeLayout,在這種情況下,你不需要嵌套其他ViewGroup,因為每個子View可以相當於其他View,或相對與父控件進行擺放。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:id="@+id/view_top_1" android:layout_width="match_parent" android:layout_height="100dp" android:background="@color/color_666666"/> <View android:id="@+id/view_top_2" android:layout_width="200dp" android:layout_below="@id/view_top_1" android:layout_height="100dp" android:background="@color/teal_200"/> <View android:id="@+id/view_top_3" android:layout_width="100dp" android:layout_below="@id/view_top_2" android:layout_height="100dp" android:background="@color/color_FF773D"/> <View android:id="@+id/view_top_4" android:layout_width="100dp" android:layout_below="@id/view_top_2" android:layout_toRightOf="@id/view_top_3" android:layout_height="100dp" android:background="@color/purple_500"/> </RelativeLayout>
RelativeLayout視圖層級如下所示:
通過兩種方式,可以很容易看出,第一種方式LinearLayout需要3個視圖層級和6個View,第二種方式RelativeLayout僅需要2個視圖層級和5個View。
當然,雖然RelativeLayout效率更高,但不是所有情況都能通過相對佈局的方式來完成控件擺放。所以通常情況下,這兩種方式需要配合使用。
註意:為瞭保證應用程序的性能,在創建佈局時,需要盡量避免重繪,佈局層級應盡可能地扁平化,這樣當View被重繪時,可以減少系統花費的時間。在條件允許的情況下,盡量的使用RelativeLayout和ConstraintLayout,而非LinearLayout,或者用GridLayoutl來替換LinearLayout。
咱們最常使用的是ViewGroup是LinearLayout,隻是因為它很容易看懂,編寫起來簡單,所以它就成瞭Android開發的首選。出於這個原因,Google推出瞭一個全新的ViewGroup。在適當的時候時候使用它,可以減少冗餘,它就是網格佈局GridLayout下。
🔥 佈局復用(<include/>和 <merge/> )
Android 提供瞭一個非常有用的標簽。在某些情況下,當你希望在其他佈局中用一些已存在的佈局時, 標簽可通過制定相關引用ID,將一個佈局添加到另一個佈局。
比如自定義一個標題欄,那麼可以按照下面的方式,創建一個可重復用的佈局文件。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <View android:id="@+id/view_top_1" android:layout_width="match_parent" android:layout_height="100dp" android:background="@color/color_666666"/> </RelativeLayout>
接著,將標簽放入相應的佈局文件中,替換掉對應的 View:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <include layout="@layout/include_layout"/> ... </RelativeLayout>
這麼一來,當你希望重用某些View時,就不用復制/粘貼的方式來實現,隻需要定義一個layout文件,然後通過 引用即可。
但是這樣做,可能會引入一個冗餘的ViewGroup(重用的佈局文件的根視圖)。為此,Android 提供瞭另一個標簽,用來幫我們減少佈局冗餘,讓層級變得更加扁平化。我們隻需要將可重用的根視圖,替換為 標簽即可。如下所示:
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <View android:id="@+id/view_top_1" android:layout_width="match_parent" android:layout_height="100dp" android:background="@color/color_666666"/> </merge>
如此一來,就沒有瞭冗餘的視圖控件,因為系統會忽略標簽,並將標簽中的視圖直接放置在相應的佈局文件中,替換標簽。
註意:使用此標簽時,需要記住它的兩個主要限制:
1、它隻能作為佈局文件的跟來使用。
2、每次調用LayoutInflater.inflate()時,必須為佈局文件提供一個View,作為它的父容器:LayoutInflater.from(this).inflate(R.layout.merge_layout,parent,true);
🔥 ViewStub
ViewStub是一個不可見的零大小View,可以作為一個節點被加入佈局文件,但他關聯的佈局,知道運行時通過調用 ViewStub.inflate() 或 View.setVisibility(View.VISIBLE) 方法,才會被繪制。
先看效果圖:
💥 ViewStub 設置
<ViewStub android:id="@+id/viewStub" android:inflatedId="@+id/subTree" android:layout="@layout/activity_imageview" android:layout_width="match_parent" android:layout_height="wrap_content" />
💥 顯示
上方 ViewStub 所關聯的佈局 activity_imageview 並不會被實例化(不要調用佈局內的控件,因為還沒加載會報空指針異常),隻有程序在運行期間調用瞭以下方法:
findViewById(R.id.viewStub).setVisibility(View.VISIBLE); ((ViewStub)findViewById(R.id.viewStub)).inflate();
在這期間不要調用關聯佈局內的控件,因為還沒唄加載沒有
一旦 ViewStub 變成 visible 或者被 inflate,它便不再可用(Id:viewStub沒瞭),因為它在佈局層級中的位置已經實例化出來的佈局所替代,因為不能被訪問,而應該使用 android:inflatedId 屬性中的ID。如下:
@Override public void onClick(View v) { switch (v.getId()){ case R.id.btn_view: break; case R.id.btn_scheme: //加載,選擇一種即可。 findViewById(R.id.v_stud).setVisibility(View.VISIBLE); //((ViewStub)findViewById(R.id.v_stud)).inflate(); //加載後layout所用ID subTree = findViewById(R.id.subTree); findViewById(R.id.btn_iv_basis).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this,"我是 ViewStud 加載的控件",Toast.LENGTH_SHORT).show(); } }); break; case R.id.btn_invisible: subTree.setVisibility(View.INVISIBLE); break; case R.id.btn_visible: subTree.setVisibility(View.VISIBLE); break; case R.id.btn_init: //ViewStub變為可見後再次調用會報空指針,因為id:viewStub 已經不存在瞭。 View viewStub = findViewById(R.id.viewStub); viewStub.setVisibility(View.GONE); break; } }
💥 小結
ViewStub 非常有用,我們可以通過 ViewStub 來延遲部分 View 的加載,縮短首次加載時間,以及減少一些不必要的內存分配。
🔥 自定義組件優化
在自定義View時需要註意,避免犯以下的性能錯誤:
- 在非必要時,對View進行重繪。
- 繪制一些不被用戶所看到的的像素,也就是過度繪制。(被覆蓋的地方)
- 在繪制期間做瞭一些非必要的操作,導致內存資源的消耗。
💥 優化
- View.invalite()是最最廣泛的使用操作,因為在任何時候都是刷新和更新視圖最快的方式。
在自定義View時要小心避免調用非必要的方法,因為這樣會導致重復強行繪制整個視圖層級,消耗寶貴的幀繪制周期。檢查清楚View.invalite()和View.requestLayout()方法調用時間位置,因為這會影響整個UI,導致GPU和它的幀速率變慢。
- 避免過渡重繪。為瞭避免過渡重繪,我們可以利用Canvas方法,隻繪制控件中所需要的部分。整個一般在重疊部分或控件時特別有用。相應的方法是Canvas.clipRect()(指定要被繪制的區域);
- 在實現View.onDraw()方法中,不應該在方法內及調用的方法中進行任何的對象分配。在該方法中進行對象分配,對象會被創建和初始化。而當View.onDraw()方法執行完畢時。垃圾回收器會釋放內存。如果View帶動畫,那麼View在一秒內會被重繪60次。所以要避免在View.onDraw()方法中分配內存。
永遠不要在View.onDraw()方法中及調用的方法中進行內存分配,避免帶來負擔。垃圾回收器多次釋放內存,會導致卡頓。最好的方式就是在View被首次創建出來時,實例化這些對象。
佈局優化到這裡就結束瞭,還是那句話後面如果有更好的方案會及時的添加進去,如果有老大有其他方案也可以留言哈,感謝!
到此這篇關於Androd 勇闖高階性能優化之佈局優化篇的文章就介紹到這瞭,更多相關Android 佈局優化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Android ViewStub使用方法學習
- Android使用ViewStub實現佈局優化方法示例
- ViewPager+Fragment實現側滑導航欄
- Android實現簡單計算器
- Android ListView仿微信聊天界面