Android群英傳讀書筆記——第十章:Android效能優化
第十章目錄
- 10.1 佈局優化
- 10.1.1 Android UI渲染機制
- 10.1.2 避免Overdraw
- 10.1.3 優化佈局層級
- 10.1.4 避免巢狀過多無用佈局
- 10.1.5 Hierarchy Viewer
- 10.2 記憶體優化
- 10.2.1 什麼是記憶體
- 10.2.2 獲取Android系統記憶體資訊
- 10.2.3 記憶體回收
- 10.2.4 記憶體優化例項
- 10.3 Lint工具
- 10.4 使用Android Studio的Memory Monitor工具
- 10.5 使用TraceView工具優化App效能
- 10.5.1 生成TraceView日誌的兩種方法
- 10.5.2 開啟TraceView日誌
- 10.5.3 分析TraceView日誌
- 10.6 使用MAT工具分析App記憶體狀態
- 10.6.1 生成HPROF檔案
- 10.6.2 分析HPROF檔案
- 10.7 使用Dumpsys命令分析系統狀態
第十章讀書筆記
佈局優化
記憶體優化
使用各種工具來進行分析、優化
10.1 佈局優化
10.1.1 Android UI 渲染機制
- 玩過射擊遊戲的兄弟都知道,FPS保持在60以後才算比較流暢,有的好電腦好顯示卡在100多甚至200多
- Android中通過VSYNC訊號觸發對UI的渲染,間隔時間是16ms,1秒鐘顯示60幀畫面的時間,也就是1000/60
- 如果系統每次渲染的時間都在16ms以內,那麼是很流暢的
- 16ms內無法完成繪製,即使耗時20ms,那麼也會在下一個16ms後再繪製
- 那麼本來16ms的時候我要顯示了,結果變成了在32ms的時候才顯示,延遲了一倍,32ms顯示值顯示了一幀
- 真的不要小看幾ms的時間,直接導致嚴重的卡頓,玩穿越火線60的fps和30的fps,完全是兩種體驗
檢測UI渲染的工具,那就是開發者選項中的監控一欄下的“GPU”呈現模式分析,選擇在螢幕上顯示條形圖
- 藍色:測量繪製Display List的時間
- 紅色:OpenGl渲染Display List的時間
- 黃色:CPU等待GPU處理的時間
- 綠色的橫線:YSYNC時間16ms,儘量保證所有的條形圖都控制在這條綠線之下
10.1.2 避免Overdraw
- 定義:螢幕上某一畫素點被重複繪製多次,就是過度繪製
- 過度繪製會浪費很多的CPU、GPU資源,常見的就是我們在多個佈局中重複設定背景色,這個是很重要的
- 不要對背景設定alpha值,會導致兩次draw,所以在需要時再去設定
- 開發者選項中硬體加速渲染一欄下的“除錯GPU過度繪製”,顯示過度繪製區域
- 常見的原因:太多疊加的背景,太多疊加的View,複雜的Layout層級
手機過度繪製再結合JakeWharton大神的Scalple的開源外掛,過度繪製這一塊,沒什麼問題了
使用步驟:
1、在gradle中加入如下程式碼:
compile 'com.jakewharton.scalpel:scalpel:1.1.2'
2、使用的時候你的根節點必須是ScalpelFrameLayout
View mainView = getLayoutInflater().inflate(R.layout.activity_main, null); ScalpelFrameLayout mScalpelFrameLayout = new ScalpelFrameLayout(this); mScalpelFrameLayout.addView(mainView); mScalpelFrameLayout.setLayerInteractionEnabled(true); //開啟 3D 效果 //mScalpelFrameLayout.setDrawIds(true); //是否顯示控制元件 id //mScalpelFrameLayout.setDrawViews(false); //是否展示控制元件內容,預設為 true //mScalpelFrameLayout.setChromeColor(Color.RED); //修改邊框顏色 //mScalpelFrameLayout.setChromeShadowColor(Color.YELLOW); //修改陰影顏色 setContentView(mScalpelFrameLayout);
10.1.3 優化佈局層級
- Android中,系統對View進行測量、佈局和繪製時,都是通過對View數的遍歷來進行操作的
- 如果View樹太高,就會嚴重影響測量、佈局和繪製的速度,因此適當減少View樹的高度,Google建議View樹高度不宜超過10層
- 現在版本的Android,Google已經使用RelativeLayout來替代LinearLayout作為預設的根佈局,,其原因就是降低LinearLayout巢狀所產生布局樹高度
- 那麼現在的版本其實是用約束佈局來作為預設的佈局
10.1.4 避免巢狀過多無用佈局
10.1.4.1 使用<include>標籤重用Layout
- 使用<include>標籤重用Layout
- 比如這個名叫common_ui的xml檔案,寬高設為0dp,迫使開發則在使用時對寬高進行賦值:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="0dp" android:layout_height="0dp" android:text="this is a common ui" android:textSize="30sp"> </TextView>
- 在主佈局中引用,include裡面的屬性會覆蓋common_ui裡的屬性:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <include layout="@layout/common_ui" android:layout_width="match_parent" android:layout_height="wrap_content" /> </RelativeLayout>
10.1.4.2 使用<ViewStub>實現View的延遲載入
- 命名一個not_often_use的xml檔案
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="not often use" android:textSize="30sp" /> </RelativeLayout>
- 在主佈局中引用:
<?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"> <ViewStub android:id="@+id/not_often_use" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/not_often_use" /> </RelativeLayout>
- 這個時候在佈局中看不到這個ViewStub佈局的,有兩種方法可以顯示這個View
ViewStub mViewStub = (ViewStub) findViewById(R.id.not_often_use); mViewStub.setVisibility(View.VISIBLE); // 或者 ViewStub mViewStub = (ViewStub) findViewById(R.id.not_often_use); View inflateView = mViewStub.inflate(); // 區別: // inflate可以返回引用佈局
<ViewStub>和View.GONE的區別:
- 都在初始化時不顯示
- <ViewStub>會在需要時才去渲染,效率更高些
- View.GONE是在初始化佈局的時候就已經載入了,只不過不可見
10.1.5 Hierarchy Viewer
- 通常情況下,Hierarchy Viewer無法在真機上使用,只能在模擬器上使用
- Google大神提供了一個開源軟體View Server,讓普通手機也能使用Hierarchy Viewer,專案地址:http://github.com/romainguy/ViewServer
- Hierarchy Viewer位於sdk\tools目錄下,開啟hierarchyviewer.bat啟動程式
- 關於Hierarchy Viewer的使用可檢視下面的部落格:Hierarchy Viewer
佈局優化的一些小總結
借這個機會說一下LinearLayout和RelativeLayout的效能對比
有的人說LinearLayout效能好,有的人說RelativeLayout效能好,真是到底哪個好
一個View要繪製到螢幕上,會經歷onMeasure、onLayout、onDraw三個階段,探討效能問題,那麼我們比較一下這三個方法執行時間的長短:
LinearLayout Measure:0.738ms Layout:0.176ms draw:7.655ms RelativeLayout Measure:2.280ms Layout:0.153ms draw:7.696ms
onMeasure方法的執行時間,LinearLayout比RelativeLayout短很多
- 因為RelativeLayout是基於相對位置的,B依賴A,C依賴B,所以會對子View做兩次measure,橫向一次,縱向一次
- 因為LinearLayout是進行橫向或者縱向的一次,所以只會measure一次,但如果設定了weight,LinearLayout會避開設定過weight的view再measure一次,所以也會measure兩次
還有一個問題,view的measure存在以下優化:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { ... mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
如果我們或者我們的子View沒有要求強制重新整理,而父View給子View傳入的值也沒有變化(也就是說子View的位置沒變化),就不會做無謂的測量。RelativeLayout在onMeasure中做橫向測量時,縱向的測量結果尚未完成,只好暫時使用myHeight傳入子View系統。這樣會導致在子View的高度和RelativeLayout的高度不相同時(設定了Margin),上述優化會失效,在View系統足夠複雜時,效率問題就會很明顯。LinearLayout在這方面不需要考慮,所以,在使用RelativeLayout時,儘量使用padding來代替margin,
總體來說:
- LinearLayout對子View進行一次measure,Relative對子View進行兩次measure,LinearLayout的效能肯定要優於RelativeLayout,
- 使用LinearLayout儘量減少使用weight屬性
- 如果同樣的佈局需求,用LinearLayout需要兩層,但是RelativeLayout可能只需要一層,那麼RelativeLayout肯定就優於LinearLayout了
- 使用RelativeLayout時,儘量用padding代替margin,在measure時會有一個優化,如果在重新整理介面時,沒有變化就不measure,但是
再舉個例子,說一下渲染和過度繪製,一個這樣的佈局,嵌套了5個RelativeLayout:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.administrator.eventbustest.MainActivity"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:textSize="40sp" android:text="主介面" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentRight="true" android:text="跳轉到下一個Activity"/> </RelativeLayout> </RelativeLayout> </RelativeLayout> </RelativeLayout> </RelativeLayout>
過度渲染情況:可以看出儘管有5層巢狀,但是並沒有背景圖片或是背景色
一個畫素點只被繪製了一次,所以並沒有過度繪製
渲染時間:可以看出來只啟動這麼一個簡單的介面,渲染卻用了可能是4*0.16還多的時間,巢狀導致渲染很費時
下面我們給每一個RelativeLayout都加一個背景色
過度渲染情況:可以說是嚴重的過度繪製了,4x+,每一個畫素點繪製了5次
渲染時間:可以說時間已經超了太多,快爆了,直觀感覺可能有1.5秒左右才打開此介面
使用JakeWharton大神的外掛scalple來看一下,套了多少層:
通過這幾個簡單的開發技巧,可以優化不少佈局的問題
10.2 記憶體優化
10.2.1 什麼是記憶體
由於Android的沙箱機制,每個應用所分配的記憶體大小是有限度的,記憶體太低就會觸發LMK:Low Memory Killer機制,我們所說的記憶體是指手機的RAM,它包括以下幾個部分:
- 暫存器(Registers)
速度最快的儲存場所,因為暫存器位於處理器內部,在程式中無法控制- 棧(Stack)
存放基本型別的資料和物件的引用,但對像本身不存放在棧中,而是存放在堆中- 堆(Heap)
堆記憶體用來存放由new建立的物件和陣列,在堆中分配的記憶體,由Java虛擬機器的自動垃圾回收(GC)來管理- 靜態儲存區域(Static Field)
靜態儲存區域就是指在固定的位置存放應用程式執行時一直存在的資料,Java在記憶體中專門劃分了一個靜態儲存區域來管理一些特殊的資料變數如靜態的資料變數- 常量池(Constant Pool)
JVM虛擬機器必須為每個被裝載的型別維護一個常量池,常量池就是該型別所用到常量的一個有序集合,包括直接常量(基本型別,String)和對其他型別、欄位和方法的符號引用這些概念中最容易搞錯的是堆和棧的區分:
- 棧:當定義一個變數時,棧會為該變數分配記憶體空間,當該變數作用域結束後,這部分記憶體控制元件會被用作新的空間進行分配
- 堆:使用new的方式建立一個變數,那麼就會在堆中為這個物件分配記憶體控制元件,即使該物件的作用域結束,這部分記憶體也不會立即被回收,而是等待系統的GC進行回收
我們可以通過程式碼分析Heap中的記憶體狀態:
ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); int heapSize = manager.getLargeMemoryClass();
10.2.2 獲取Android系統記憶體資訊
10.2.2.1 Process Stats
Process Stats:
- 系統記憶體監視服務,通過”Setting-Developer-options-Process Stats”來開啟這個功能
- 也可以使用Dumpsys命令來獲取這些資訊:adb shell dumpsys procstats
- 這個應該也是開發者選項中的
10.2.2.2 Meminfo
Meminfo:
- 記憶體監視工具,通過”Settings-Apps-Running”中開啟這個介面
- 也可以使用Dumpsys命令來獲取這些資訊:adb shell dumpsys meminfo
- 手機設定中的開發者選項中正在執行的服務
10.2.3 記憶體回收
- Java對於C、C++這類語言最大的優勢就是不用手動管理系統資源,Java建立了垃圾收集器執行緒來自動進行資源的管理
- Java的GC是系統自動進行的,但何時進行卻是開發者無法控制的,即使呼叫System.gc()方法,也只是建議系統進行GC,系統不一定會採取你的建議
- 儘管再強大的演算法,也難免存在部分物件忘記回收的想象,這就是造成記憶體洩露的原因
10.2.4 記憶體優化例項
10.2.4.1 Bitmap優化
Bitmap優化:Bitmap是造成記憶體佔用過高或OOM的最大威脅
- 使用適當解析度和大小的圖片,開發中要根據對應的情況使用縮圖或原圖
- 及時回收記憶體,使用完bitmap後要及時recycler()釋放,不過Android4.0後被放置在堆中,不需要釋放了
- 使用圖片快取,通過記憶體快取和硬碟快取可以更好的使用Bitmap
10.2.4.2 程式碼優化
程式碼優化:任何Java類佔用大約500位元組的記憶體空間,建立一個類的例項會消耗大概15位元組的記憶體
- 對常量使用static修飾符
- 使用靜態方法,靜態方法會比普通方法提高15%左右的訪問速度
- 減少不必要的成員變數 ,這點在Android Lint工具上已經整合檢測了,如果一個變數可以定義為區域性變數,則會建議你不要定義為成員變數
- 減少不必要的物件,使用基礎型別會比使用物件更加節省資源, 同時更應該避免頻繁建立短作用域的變數
- 儘量不要使列舉、少使用迭代器
- 對Cursor、Receiver、Sensor、Fiie等物件,要非常注意對它們的建立,回收與註冊、解註冊
- 避免使用IOC框架,IOC通常使用註解、反射來進行實現,雖然現在Java對反射的效率已經進行了很好的優化,但大量使用反射依然會帶來效能的下降
- 使用RenderScript、OpenGL來進行非常複雜的繪圖操作
- 使用SurfaceView來替代View進行大量、頻繁的繪圖操作
- 儘量使用檢視快取,而不是每次都執行inflaler()方法解析檢視
10.3 Lint工具
- Android Lint工具是Android Studio中整合的一個Android程式碼提示工具,可以給你的佈局、程式碼提供非常強大的幫助。
- 工具欄->Analyze->Inspect Code,選擇後可以選擇對應的範圍,可以全部檔案,也可以選擇當前的類
- 選全部檔案呢,可以刪除無用的程式碼
- 選當前類,你可以看到你編寫完的程式碼的一些問題,真的很好用
10.4 使用Android Studio的Memory Monitor工具
- Memory Monitor工具是Android Studio自帶的一個記憶體監視工具,通過Android Studio右下角”Memory Monitor”開啟”Memory Monitor”工具
- Android Studio3.0之後改為Android Profiler,比較全面
10.5 使用TraceView工具優化App效能
TraceView也是Android SDK內建的一個工具
10.5.1 生成TraceView日誌的兩種方法
- 利用Debug類幫助我們生成日誌檔案
- 利用Android Device Monitor 工具輔助生成日誌檔案
10.5.1.1 通過程式碼生成精確範圍的TraceView日誌
10.5.1.2 通過Android Device Monitor生成TraceView日誌
還有一個就是通過Android profiler來生成
這裡不提了,網上一搜一大推使用的方法
10.5.2 開啟TraceView日誌
- 可以使用SDK中的”sdk\tools\traceview.bat”工具開啟
- 可以在Android Device Monitor工具下,的File選單選擇Open File開啟
10.5.3 分析TraceView日誌
10.5.3.1 時間軸區域
時間軸區域:顯示了不同執行緒在不同時間段內的執行情況
- 在時間軸中,每一行都代表了一個獨立的執行緒
- 使用滑鼠滾輪可以放大時間軸
- 不同色塊代表不同的執行方法,色塊的長度,代表了方法所執行的時間
10.5.3.2 Profile區域
Profile區域:顯示你選擇的色塊所代表的方法在該色塊所處的時間段內的效能分析
- Incl CPU Time:某方法佔用CPU的時間
- Excl CPU Time:某方法本身(不包含子方法)佔用CPU的時間
- Incl Real Time:某方法真正執行的時間
- Excl Real Time:某方法本身(不包含子方法)真正執行的時間
- Calls+RecurCalls:呼叫次數+遞歸回調的次數
每個時間都包含兩列,一個是實際的時間,另一個是百分比,如果佔用時間長且Calls+RecurCalls次數少,那麼就可以列為懷疑物件
10.6 使用MAT工具分析App記憶體狀態
MAT工具是一個分析記憶體的強力助手
10.6.1 生成HPROF檔案
開啟Android Device Monitor工具,選擇要監聽的執行緒,並點選選單欄中的”Update Heap”按鈕
在Heap標籤中點選”Cause GC”按鈕,就會顯示當前記憶體狀態
這裡有一 個判斷當前是否存在記憶體洩漏的小技巧:當我們不停地點選”Cause GC”按鈕的時,如果”data object”一欄中的”Total size”有明顯變化,就代表可能存在記憶體洩漏
上面是手動檢視Heap狀態,下面點選選單欄的”Dump HPROF File”按鈕
系統會生成一個.hprof檔案,預設名為包名.hprof,不過還不能直接使用MAT工具檢視,還需要進行格式轉換,在SDK目錄的platform-tools目錄下,使用hprof-conv工具幫助轉換,命令如下:
D:\sdk\platform-tools>hprof-conv F:\Heap\com.handsome.heap.hprof heap.hprof
格式命令:”hprof-conv infile outfile”生成heap.hprof檔案利用MAT工具就可以進行記憶體分析
10.6.2 分析HPROF檔案
開啟MAT工具,選擇”Open a Heap Dump”選項,進入分析:
- Histogram
- Histogram直方圖,用於顯示記憶體中每個物件的數量、大小和名稱
- 在選擇物件上單擊滑鼠右鍵,在彈出的快捷選單中選擇”List objects-with incoming references”選項檢視具體的物件
- Dominator Tree
- Dominator Tree支配樹會將記憶體中的物件按照大小進行排序,並顯示物件之間的引用結構
- 物件已經按照”Retained Heap”進行排序了,即按照物件及其持有的引用的記憶體總和進行排序,通過分析記憶體佔用大的物件找出記憶體消耗的原因
10.7 使用Dumpsys命令分析系統狀態
- 使用Dumpsys命令,只需要輸入”adb shell dumpsys+引數”即可
- adb shell dumpsys activity:檢視Activity棧的詳細資訊
- adb shell dumpsys meminfo:檢視記憶體資訊
- adb shell dumpsys battery:檢視電池資訊
- adb shell dumpsys package:檢視包資訊
- adb shell dumpsys wifi:顯示Wi-Fi資訊
- adb shell dumpsys alarm:顯示alarm資訊
- adb shell dumpsys procstats:顯示記憶體資訊
總結
這章學到的內容還是很多的
佈局的優化,開發者選項中幾種工具的使用,Scalple,Hierarchy Viewer等等
記憶體的優化,主要是Android Lint工具真是非常的方便,然後就是Android Profiler
TraceView、MAT工具、Dumpsys這幾個工具等到之後需要的話再去好好熟悉一下