布局優化的核心思想是優化布局嵌套層級(層級越少,View繪制時越快)
一、Android系統屏幕UI刷新機制
首先需要明白一個概念,如果我們想要屏幕流暢的運行,就必須保證UI全部的測量、布局和繪制的時間在16ms內
為什么是16ms? 因為人眼與大腦之間的協作無法感知超過60fps的畫面更新,而16ms也就是每秒刷新60fps 16ms=1000/60Hz,也就是說超過16ms用戶就會感知到卡頓.
熟悉屏幕UI刷新機制,首先需要了解下刷新率和幀率:
刷新率(Refresh Rate): 指屏幕在一秒內刷新屏幕的次數,例如60HZ
幀率(Frame Rate): 指GPU在一秒內操作畫面的幀數,例如30fps,60fps
在一個典型的顯示系統中,一般包括CPU、GPU、display(可以理解為屏幕或顯示器)三個部分,CPU負責計算數據,把計算好數據交給GPU,GPU會對圖形數據進行渲染,渲染好后放到buffer里存起來,然后display負責把buffer里的數據呈現到屏幕上.
顯示過程簡單的說就是 CPU/GPU準備好數據,存入buffer,display每隔一段時間去buffer里取數據,然后顯示出來.但是刷新頻率和幀率并不是總能保持相同的節奏.
針對上述情況,Android系統中引入了VSYNC的機制(Vertical Synchronization 垂直同步,我們可以理解為幀同步),
它為了保證GPU生成幀的速度和display刷新的速度保持一致,Android系統會每16ms就會發出一次VSYNC信號觸發UI渲染更新.
VSYNC最重要的作用是防止出現畫面撕裂(screen tearing).所謂畫面撕裂 是指一個畫面上出現了兩幀畫面的內容(如下圖).
為什么會出現這種情況呢?這種情況一般是因為顯卡輸出幀的速度高于顯示器的刷新速度,導致顯示器并不能及時處理輸出的幀,
而最終出現了多個幀的畫面都留在了顯示器上的問題.
畫面撕裂也就是幀率超過刷新率情況(圖一):
我們看下圖一,在沒有VSync的情況下屏幕刷新第二幀畫面的時候 GPU生成了2 3兩個畫面 導致畫面撕裂現象,如果引入VSYNC機制后,要求繪制只能在收到VSYNC信號之后才能進行,因此杜絕了GPU一直不停的進行繪制,幀的生成速度高于屏幕的刷新速度,導致生成的幀不能被正常顯示,只能丟棄,這樣就出現了丟幀的情況
其實android設備更多的情況是幀率小于刷新頻率情況(圖二):
我們看下圖二,GPU生成幀的速度從60fps突然掉到60fps以下,就會出現某些幀顯示的畫面內容就會與上一幀的畫面相同.這樣一來,用戶在兩個16ms看到的是同一幀畫面,因此會給用戶卡頓不流暢的感受.
圖一:
更多的情況是GPU生成的幀率小于刷新頻率情況(圖二):
幀率從60fps突然掉到60fps以下,就會出現某些幀顯示的畫面內容就會與上一幀的畫面相同.因此給用戶卡頓不流暢的感受.
二、布局的選擇
首先FrameLayout能實現的優先使用FrameLayout,因為Framelayout是最簡單高效的ViewGroup(為什么它是最高效的呢?最簡單的辦法就是我們可以通過查看它源碼的行數,FrameLayout,源代碼行數是最少的,代碼邏輯也是最簡單的)
其次優先選擇RelativeLayout,因為RelativeLayout可以簡單實現LinearLayout嵌套才能實現的布局(等下還會介紹一個ConstraintLayout,它可以在不嵌套布局的情況下更簡單的完成更多復雜的布局)
最后是當在RelativeLayout和LinearLayout在不嵌套情況下同時能夠滿足需求時,優先選擇LinearLayout,因為RelativeLayout功能相對復雜,同時會有重復繪制的情況
什么是重復繪制?
重復測量視圖并不一定是因為錯誤。RelativeLayout就需要經常對它的子視圖測量兩次,以確
保所有子視圖被放置在了正確的位置。如果 LinearLayout 的子視圖設置了 layout weight屬
性,那么 LinearLayou也需要測量兩次以確定子視圖的確切尺寸。如果是嵌套的LinearLayout
或者是RelativeLayout,測量的次數會呈指數增長(兩層嵌套會進行4次測量,3層嵌套會進行
8 次測量,等等)。
- 避免過度繪制(Overdraw)
Overdraw: 指屏幕上某一個像素點在同一幀的時間內被繪制了多次.
在多層次的UI結構中,如果不可見的UI也在做繪制的操作,就會導致某些像素區域被繪制了多次,浪費大量的CPU以及GPU資源,我們可以在手機的設置—->開發者選項—->打開"調試GPU過度繪制" 查看Overdraw過度繪制情況.
如圖 通過4種顏色展示不同程度的Overdraw的情況:
暗紅: overdraw 4倍.像素繪制了五次或者更多.必須得優化了
淡紅: overdraw 3倍.像素繪制了四次.小范圍是可以接受,可以試著去優化.
綠色: overdraw 2倍.像素繪制了三次.中等大小的綠色區域是可以接受的
藍色: overdraw 1倍.像素繪制了兩次.大片的藍色是可以接受的
沒有顏色: 沒有overdraw.像素只繪制了一次
舉一個導致overdraw的場景:
當我們代碼中ViewPager和ViewPager中的fragment都設置了背景色,這樣會導致背景色在同一個像素點上重復繪制,而針對這種情況,我們可以去掉ViewPager的背景色來避免overdraw
- ConstraintLayout
ConstraintLayout是Android Studio 2.2中主要的新增功能之一,它可以在不嵌套任何布局的情況下構建復雜的布局.它與RelativeLayout非常相似,所有的view都依賴于相鄰控件的相對關系.而ConstraintLayout比RelativeLayout更加靈活,在AndroidStudio中進行拖拽即可完成布局.
以往 我們都是通過嵌套或者使用RelativeLayout來完成復雜的布局,但是通過使用Systrace大量測試表明:嵌套式層次結構和 RelativeLayout(會對其每個子對象重復測量兩次)的特性導致性能低下.因此,針對復雜的布局,我們毫無疑問優先選擇ConstraintLayout.
ConstraintLayout用法介紹及ConstraintLayout和RelativeLayout測試對比性能優勢
三、優化控件的使用
- include標簽
include標簽可以在一個布局中引入另外一個布局.如果我們程序的所有界面都有一個公共的部分,這個時候最好的做法就是將這個公共的部分提取到一個獨立的布局文件中,然后在每個界面的布局文件中來引用這個公共的布局
作用: 為了提高代碼的復用性,減少代碼;將布局中公共部分抽取供其他layout使用,但可能會導致多余的布局嵌套
用法如下:
<!-- 1.定義公共部分布局: include_layout.xml -->
<?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">
<TextView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:text="Back" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Title"
android:textSize="20sp" />
<TextView
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:text="Finish" />
</RelativeLayout>
<!-- 2.使用include_layout.xml -->
<?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">
<!--在<include>標簽中,我們也可以覆蓋layout中定義的所有屬性-->
<include
layout="@layout/include_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
style="@style/textStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="HAHA" />
</LinearLayout>
<!--定義style-->
<style name="textStyle">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">@android:color/black</item>
<item name="android:gravity">center_vertical</item>
</style>
如上代碼針對style的抽取,同樣也提高代碼的復用性
除了抽取公共部分代碼外,include還可以將我們布局代碼進行模塊化,也就是當我們頁面邏輯非常復雜,單純布局代碼就一千多行的時候,不管是誰來維護這樣的代碼,看著就會頭疼,這個時候我們可以將布局進行分類,使用include標簽抽取分成不同模塊來引入,模塊化后的代碼更便于提高維護的效率
- merge標簽
merge標簽是include標簽的輔助擴展,為了防止在引用布局文件時產生多余的布局嵌套
作用: 解決布局層級的優化,減少布局嵌套的層次,提高布局加載的效率
用法如下:
<!--定義merge_layout.xml-->
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!--根標簽必須是merge-->
<Button
android:id="@+id/ok"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="OK" />
<Button
android:id="@+id/cancel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Cancel" />
</merge>
<!--使用merge_layout.xml-->
<?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">
<!--merge標簽使用的是父布局的特性
(也就是這里是垂直的LinearLayout,merge中兩個button也就是垂直的LinearLayout屬性)-->
<include layout="@layout/merge_layout" />
</LinearLayout>
使用場景為:當父布局和子布局相同時,可以利用merge標簽來減少一層布局嵌套,merge標簽使用的是父布局的特性
- ViewStub標簽
ViewStub只有加載該布局的時候才占用資源,INVISIBLE狀態是不會繪制出來的(ViewStub雖說也是View的一種,但是它沒有大小,沒有繪制功能,也不參與布局,資源消耗非常低,將它放置在布局當中基本可以認為是完全不會影響性能的)
用法如下:
<!--viewstub_layout.xml-->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Button2" />
</FrameLayout>
<!--使用ViewStub-->
<?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">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Button1" />
<ViewStub
android:id="@+id/view_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/viewstub_layout" />
</LinearLayout>
四、原生View控件的優化:
android系統提供的一些原生控件并不是完美的,或者說結合我們實際復雜的開發環境來說,壓根就不會有絕對完美的控件,為了提高我們使用控件的效率進而提升程序的質量,因此我們需要針對原生控件做一些局部優化.
- ListView的優化
復用convertView(重用View可以減少重新分配緩存 避免內存頻繁分配/回收),
使用ViewHolder(原因是findViewById方法耗時較大,如果控件個數過多,會嚴重影響性能,而使用ViewHolder主要是為了可以省去這個時間.通過setTag,getTag直接獲取這個View),
以及數據多的情況下進行分批加載等等
- WebView的優化
在當下的Android開發中,Webview的身影隨處可見,尤其是在Hybrid App(混合模式移動應用)中,更是不可或缺,而Webview的性能卻是有待改善的(手機QQ中大概有70%以上的業務都是由H5開發).
Hybrid App(混合模式移動應用)是指介于web-app、native-app這兩者之間的app,兼具“Native App良好用戶交互體驗的優勢”和“Web App跨平臺開發的優勢”。
全局WebView(混合模式移動應用開發中,在客戶端剛啟動時,就初始化一個全局的WebView待用,并隱藏;當用戶訪問了WebView時,直接使用這個WebView加載對應網頁,并展示.這種方法可以比較有效的減少WebView在App中的首次打開時間,當用戶訪問頁面時,不需要等待初始化WebView的時間)
客戶端代理數據請求(在客戶端初始化WebView的同時,直接由native開始網絡請求數據;當頁面初始化完成后,向native獲取其代理請求的數據.這個方法雖然不能減小WebView初始化時間,但數據請求和WebView初始化可以并行進行,這樣總體的頁面加載時間就縮短了,這種方式是參考騰訊所分享的在手機QQ混合開發的做法)
優化網頁加載速度(先設置WebView禁止加載圖片,再覆寫WebViewClient的onPageFinished()方法,頁面加載結束后再加載圖片)
還有其他各種優化的方式,不再一一列舉,總結起來都是圍繞兩點:
1.在使用前預先初始化好WebView,從而減小耗時
2.在初始化的同時,通過Native來完成一些網絡請求等過程,使得WebView初始化不是完全的阻塞后續過程
摘選自美團技術團隊 -- WebView性能優化總結:
一個加載網頁的過程中,native、網絡、后端處理、CPU都會參與,各自都有必要的工作和依賴關系;讓他們相互并行處理而不是相互阻塞才可以讓網頁加載更快:
WebView初始化慢,可以在初始化同時先請求數據,讓后端和網絡不要閑著。
后端處理慢,可以讓服務器分trunk輸出,在后端計算的同時前端也加載網絡靜態資源。
腳本執行慢,就讓腳本在最后運行,不阻塞頁面解析。
同時,合理的預加載、預緩存可以讓加載速度的瓶頸更小。
WebView初始化慢,就隨時初始化好一個WebView待用。
DNS和鏈接慢,想辦法復用客戶端使用的域名和鏈接。
腳本執行慢,可以把框架代碼拆分出來,在請求頁面之前就執行好。
- ViewPager的延遲加載
ViewPager有一個 “預加載”的機制,默認會把ViewPager當前位置的左右相鄰頁面預先初始化(預加載),這樣設計是為了ViewPager左右滑動會更加流暢,但如果當前APP頁面數量不多,并且每個頁面資源占用很大,用戶可能只在一個頁面使用,不需要切換頁面,這時,我們就沒必要使用預加載來消耗手機的資源了
怎樣去做到延遲加載呢?
setOffscreenPageLimit(int limit)用來設置ViewPager預加載的數量,默認是1,
1也就是會預加載左右相鄰頁面,所以我們設置的值應該小于1,但事實上設置小于1是不生效的.因此這種方式行不通.
ViewPager中,預加載數量值的變量為DEFAULT_OFFSCREEN_PAGES,這個值是private,因此子類繼承ViewPager也是行不通.
以下通過兩種方式來實現延遲加載:
1.從Fragment著手,只有Fragment可見的時候才去加載數據
2.自定義一個ViewPager,把原生ViewPager代碼全拷過來,修改加載數變量DEFAULT_OFFSCREEN_PAGES值為0;
(我們這里使用的是低版本ViewPager代碼,Android 4.0的代碼,低版本的代碼相對更少,邏輯相對更簡單)
// 方式一:
public class LazyFragment extends Fragment {
private boolean mIsInit;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mIsInit = true; // View 控件的初始化
isLoadData(); // 這里還需滿足條件1: 視圖對用戶可見
return super.onCreateView(inflater, container, savedInstanceState);
}
/**
* 這個方法是通知Fragment的UI是否可見,當參數isVisibleToUser為true的時候,fragment的UI是可見的,為false的時候為不可見
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
isLoadData(); // 還需滿足條件2: 視圖已經初始化
}
private void isLoadData() {
if (getUserVisibleHint()) {
if (mIsInit) {
// 滿足以上兩個條件才加載數據
}
}
}
}
// 方式二:
https://github.com/ansen360/Sample/blob/master/app/src/main/java/com/ansen/sample/LazyViewPager.java
4. 其他優化點:
刪除控件中無用屬性
減少不必要的infalte
布局上的優化。移除 XML 中非必須的背景,移除 Window 默認的背景、按需顯示占位背景圖片
自定義View優化.避免onDraw方法聲明太多變量,使用canvas.clipRect()來幫助系統識別那些可見的區域,只有在這個區域內才會被繪制
相關鏈接直達:
Android APP性能優化之 ---- 優化監測工具(四)