- 布局可以說是APP最重要的一項了,用戶感知極強,無論你的代碼寫的如何,用戶也不知道,用戶只能看到和操作APP,更漂亮合理的布局,更流暢的體驗才是好APP。
- 比如微信,操作起來卡,用戶只會覺得是手機不行,而不會是微信不行,但其他APP卡,用戶就覺得是APP不行,而不是手機不行。┓( ′?` )┏
- Android性能優化 - 啟動速度優化 也可一起學習。
1.卡頓分析
1.1 刷新率
- 大多數用戶感知到的卡頓等性能問題的最主要根源都是因為渲染性能。從設計師的角度,他們希望App能夠有更多的動畫,圖片等時尚元素來實現流暢的用戶體驗。但是Android系統很有可能無法及時完成那些復雜的界面渲染操作。Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,為了能夠實現60fps,這意味著程序的大多數操作都必須在16ms內完成。
-
上面那段話就是,APP做的越來越炫酷,動畫和視頻一大堆,啟動就要顯示視頻廣告,這些對于旗艦Android機是無壓力的,但對于老手機,或者是入門手機,復雜的頁面,計算量大,CPU、GPU處理不過來,也就無法流暢顯示了。
16ms刷新 -
如果你的某個操作花費時間是24ms,系統在得到VSYNC信號的時候就無法進行正常渲染,這樣就發生了丟幀現象。那么用戶在32ms內看到的會是同一幀畫面。
24ms刷新
1.2 PerfDog
-
下圖,在PerfDog,使用華為P30Pro,查看微博的刷新情況,靜態的微博內容在不滑動的時候,刷新率就是0fps,快速滑動時,刷新率在60fps左右,還能查看CPU和內存是使用情況。
微博 -
下圖,而在微博播放視頻時,刷新率一直就是60fps左右了。
微博視頻 下圖,普通的APP都基本能達到60fps,相機就不是了,相機拍照穩定在30fps,而自拍時,開啟美顏,降到24fps了,看來相機加AI美顏是比較吃性能的。
-
小知識,電影或者是網上看的視頻一般是24幀/秒的速度播放的,即可以省性能,效果也不錯,索尼A7M3相機可以錄制120幀的慢動作,可以做4倍或者5倍升格。
相機
1.3 CPU Profile
- Android性能優化 - 啟動速度優化 里有講怎么使用 Profile 看各個方法的耗時,布局的加載也是會顯示的,也可以用來分析卡頓的可能情況。
2.布局優化
2.1 過度繪制
Overdraw(過度繪制)描述的是屏幕上的某個像素在同一幀的時間內被繪制了多次。在多層次的UI結構里面,如果不可見的UI也在做繪制的操作,這就會導致某些像素區域被繪制了多次。這就浪費大量的CPU以及GPU資源。
-
藍色綠色是比較好的情況,紅色就是層級較多了,為了實現好看的效果,就會套多層布局,過度繪制的多了消耗性能,對與入門機就會卡頓了。
過度繪制 手機進入開發人員選項,調試GPU過度繪制,打開顯示過度繪制區域。
-
貝殼APP的布局大多是藍色綠色的,說明他們APP就沒什么過度繪制的情況,非常好。
貝殼 -
到下面的列表就會有過度繪制的情況,但區域不大,只有內容的部分。
在這里插入圖片描述 -
開發人員選項,顯示布局邊界,可以看到貝殼的布局層級確實不多,也非常的清晰工整。
貝殼 -
微博APP的過度繪制區域基本占滿整個屏幕了,除了微博還有微信淘寶等列表APP也是大多數紅色的,原因可能是列表類的APP,除了父布局,里面還要套RecyclerView,再套itemview,無法避免的過度繪制;但整個item都過度繪制了,貝殼就會比較好一些。
微博 -
微博的布局看起來就會復雜一些了。
在這里插入圖片描述
2.2 解決過度繪制
- 1.上面的微博跟貝殼比較,微博的item是有過度繪制的情況,那么我們在寫RecyclerView的時候,如果RecyclerView的父布局、RecyclerView、item三者的背景只要其中一個設置就可以了,沒有設置背景就不會渲染,否則就會有過度繪制的情況。
- 2.父布局套子布局也是盡量只設置其中一個背景,除非沒辦法都需要背景。
- 3.子view一般繪制后是會覆蓋父view,所以一般選擇把背景設置在子view。
- 4.視圖的層級結構能減少就減少,層級越多繪制速度越慢。
- 5.盡量少設置view的透明度,如果一個view設置了alpha,那他需要知道下面的view是什么內容,再繪制自己,就是過度繪制。如果是文字有透明度,可以在色號里就設置好。
2.3 層級優化
- Android studiol有布局層級的工具,Layout Inspector,運行起來app后,可以看到每個頁面的層級結構。層級太多,肯定就會造成卡頓,啟動慢,在啟動優化有說,
-
左邊可以看到布局樹的具體內容。布局樹
- 像ScrollView里面只能放一個ViewGroup,是不可縮減的,但 LinearLayout套LinearLayout 是可以通過ConstraintLayout解決的,約束布局可以說是結合了線性布局跟相對布局的優點,能有效減少層級。
view tree
2.4 使用merge
- 我們有一些布局是可以通用的,避免重復代碼,就可以使用 include。
- 但是,如果使用 include,但里面的布局又是一個 ViewGroup 的話,就會造成層級過多,這個時候就可以使用 merge 標簽了,里面的子view根據會外部include地方的ViewGroup來排列,從而減少層級。
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/layout_head" />
</LinearLayout>
<!-- layout_head -->
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_head"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="text1"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="text2"
android:textSize="16sp" />
</merge>
- 可以看到使用merge 里面的 view 直接在 LinearLayout 的層級里。
2.5 ViewStub
- 我們有時根據需求,先把布局畫好,然后把 android:visibility 設置成 "invisible" 或者是 "gone" ,invisible 和 gone 雖然看不見,但他們還是有初始化,占用這內存和資源,前者還占用著位置。
- 我們可以使用 ViewStub 來包裹這些需要隱藏顯示的 view,它是一個輕量級的view,不可見不占用資源,只有當設置 inflate 時才初始化顯示。
<ViewStub
android:id="@+id/viewStub_title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout="@layout/layout_title"
app:layout_constraintTop_toTopOf="parent" />
<!-- layout_title -->
<?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="50dp"
android:orientation="horizontal">
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:id="@+id/image_back"
android:padding="12dp"
android:src="@drawable/ic_back" />
<TextView
android:layout_width="0dp"
android:id="@+id/tv_title"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="title" />
</LinearLayout>
- 在Activity中 viewStub.inflate() 即可顯示,但不可重復調用inflate();否則報異常:
java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.View android.view.ViewStub.inflate()' on a null object reference。
viewStub = findViewById(R.id.viewStub_title);
viewStub.inflate();
//之后可以初始化里面的view
ImageView ivBack = findViewById(R.id.image_back);
ivBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
TextView tvTitle = findViewById(R.id.tv_title);
3.其他優化
3.1 不要在onDraw里創建對象
- 我們經常需要自定義view,在 onDraw() 方法里不要創建對象,因為自定義view繪制,會非常頻繁的調用 onDraw,雖然方法里的對象創建用完就被回收掉,但頻繁的創建銷毀對象會導致內存抖動和GC ,而 GC 多了就會卡頓。
避免ondraw里創建對象
3.2 異步加載布局
- LayoutInflater加載xml布局的過程會在主線程使用IO讀取XML布局文件進行XML解析,再根據解析結果利用反射創建布局中的View/ViewGroup對象。這個過程隨著布局的復雜度上升,耗時自然也會隨之增大。Android為我們提供了 Asynclayoutinflater 把耗時的加載操作在異步線程中完成,最后把加載結果再回調給主線程。
- Asynclayoutinflater 注意的地方:
- 1、使用異步 inflate,那么需要這個 layout 的 parent 的 generateLayoutParams 函數是線程安全的。
- 2、所有構建的 View 中必須不能創建 Handler 或者是調用 Looper.myLooper;(因為是在異步線程中加載的,異步線程默認沒有調用 Looper.prepare )。
- 3、AsyncLayoutInflater 不支持設置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;。
- 4、不支持加載包含 Fragment 的 layout。
- 5、如果 AsyncLayoutInflater 失敗,那么會自動回退到UI線程來加載布局。