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 提供了一個非常有用的標籤。在某些情況下,當你希望在其他佈局中用一些已存在的佈局時,<include/>
標籤可通過制定相關引用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>
接著,將<include/>
標籤放入相應的佈局檔案中,替換掉對應的 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檔案,然後通過 <include/>
引用即可。
但是這樣做,可能會引入一個冗餘的ViewGroup(重用的佈局檔案的根檢視)。為此,Android 提供了另一個標籤,用來幫我們減少佈局冗餘,讓層級變得更加扁平化。我們只需要將可重用的根檢視,替換為 <merge/>
標籤即可。如下所示:
<?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>
如此一來,就沒有了冗餘的檢視控制元件,因為系統會忽略<merge/>
標籤,並將<merge/>
標籤中的檢視直接放置在相應的佈局檔案中,替換<include/>
標籤。
注意:使用此標籤時,需要記住它的兩個主要限制:
1、它只能作為佈局檔案的跟來使用。
2、每次呼叫LayoutInflater.inflate()時,必須為
<merge/>
佈局檔案提供一個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被首次創建出來時,例項化這些物件。
佈局優化到這裡就結束了,還是那句話後面如果有更好的方案會及時的新增進去,有其他方案大佬歡迎留言哈,感謝!