目錄
- 一、四個方面
- 二、卡頓優化
- 1、Android系統顯示原理
- 2、卡頓根本原因
- 3、性能分析工具
- (1)Profile GPU Rendering
- (2)TraceView
- (3)Systrace UI 性能分析
- 4、優化建議
- (1)布局優化
- (2)避免過度繪制
- (3)啟動優化
- (4)合理的刷新機制
- (5)其他
- 三、內存優化
- 1、Android內存管理機制
- (1)Java對象生命周期
- (2)內存分配
- (3)內存回收機制
- 2、內存分析工具
- (1)Memory Monitor
- (2)Heap Viewer
- (3)Allocation Tracker
- (4)Memory Analyzer Tool(MAT)
- 3、常見內存泄漏場景
- 4、優化內存空間
- 四、穩定性優化
- 五、耗電優化
- 六、安裝包大小優化
一、四個方面
可以把用戶體驗的性能問題主要總結為4個類別:
-
流暢
-
穩定
-
省電、省流量
-
安裝包小
性能問題的主要原因是什么,原因有相同的,也有不同的,但歸根到底,不外乎內存使用、代碼效率、合適的策略邏輯、代碼質量、安裝包體積這一類問題,整理歸類如下:
從圖中可以看到,打造一個高質量的應用應該以4個方向為目標:快、穩、省、小。
-
快
:使用時避免出現卡頓,響應速度快,減少用戶等待的時間,滿足用戶期望。 -
穩
:減低 crash 率和 ANR 率,不要在用戶使用過程中崩潰和無響應。 -
省
:節省流量和耗電,減少用戶使用成本,避免使用時導致手機發燙。 -
小
:安裝包小可以降低用戶的安裝成本。
要想達到這4個目標,具體實現是在右邊框里的問題:卡頓、內存使用不合理、代碼質量差、代碼邏輯亂、安裝包過大,這些問題也是在開發過程中碰到最多的問題,在實現業務需求同時,也需要考慮到這點,多花時間去思考,如何避免功能完成后再來做優化,不然的話等功能實現后帶來的維護成本會增加。
二、卡頓優化
Android 應用啟動慢,使用時經常卡頓,是非常影響用戶體驗的,應該盡量避免出現。卡頓的場景有很多,按場景可以分為4類:UI 繪制、應用啟動、頁面跳轉、事件響應
,如圖:
這4種卡頓場景的根本原因可以分為兩大類:
-
界面繪制
:主要原因是繪制的層級深、頁面復雜、刷新不合理,由于這些原因導致卡頓的場景更多出現在UI和啟動后的初始界面以及跳轉到頁面的繪制上。 -
數據處理
:導致這種卡頓場景的原因是數據處理量太大,一般分為三種情況,一是數據在處理UI線程,二是數據處理占用CPU高,導致主線程拿不到時間片,三是內存增加導致GC頻繁,從而引起卡頓。
引起卡頓的原因很多,但不管怎么樣的原因和場景,最終都是通過設備屏幕上顯示來達到用戶,歸根到底就是顯示有問題,所以,要解決卡頓,就要先了解Android系統的顯示原理。
1、Android系統顯示原理
Android顯示過程可以簡單概括為:Android應用程序把經過測量、布局、繪制后的Surface緩存數據,通過SurfaceFlinger把數據渲染到顯示屏幕上, 通過Android的刷新機制來刷新數據。也就是說應用層負責繪制,系統層負責渲染,通過進程間通信把應用層需要繪制的數據傳遞到系統層服務,系統層服務通過刷新機制把數據更新到屏幕上
。
我們都知道在Android的每個View繪制中有三個核心步驟:Measure、Layout、Draw
。具體實現是從 ViewRootImp類的performTraversals() 方法開始執行,Measure和Layout都是通過遞歸來獲取View的大小和位置,并且以深度作為優先級,可以看出 層級越深、元素越多、耗時也就越長
。
真正把需要顯示的數據渲染到屏幕上,是通過系統級進程中的SurfaceFlinger服務來實現的,那么這個SurfaceFlinger服務主要做了哪些工作呢?如下:
-
響應客戶端事件,創建Layer與客戶端的Surface建立連接。
-
接收客戶端數據及屬性,修改Layer屬性,如尺寸、顏色、透明度等。
-
將創建的Layer內容刷新到屏幕上。
-
維持Layer的序列,并對Layer最終輸出做出裁剪計算。
既然是兩個不同的進程,那么肯定是需要一個跨進程的通信機制來實現數據傳遞,在Android顯示系統中,使用了Android的匿名共享內存:SharedClient,每一個應用和SurfaceFlinger之間都會創建一個SharedClient ,然后在每個SharedClient中,最多可以創建31個 SharedBufferStack,每個Surface都對應一個SharedBufferStack,也就是一個Window。
一個SharedClient對應一個Android應用程序,而一個Android應用程序可能包含多個窗口,即Surface。也就是說SharedClient包含的是SharedBufferStack的集合,其中在顯示刷新機制中用到了雙緩沖和三重緩沖技術。最后總結起來顯示整體流程分為三個模塊:應用層繪制到緩存區,SurfaceFlinger把緩存區數據渲染到屏幕,由于是不同的進程,所以使用Android的匿名共享內存SharedClient緩存需要顯示的數據來達到目的。
除此之外,我們還需要一個名詞:FPS。FPS表示每秒傳遞的幀數
。在理想情況下,60FPS就感覺不到卡,這意味著每個繪制時長應該在16ms以內
。但是 Android系統很有可能無法及時完成那些復雜的頁面渲染操作。Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需的60FPS。如果某個操作花費的時間是24ms ,系統在得到VSYNC信號時就無法正常進行正常渲染,這樣就發生了丟幀現象
。那么用戶在32ms內看到的會是同一幀畫面,這種現象在執行動畫或滑動列表比較常見,還有可能是你的Layout太過復雜,層疊太多的繪制單元,無法在16ms完成渲染,最終引起刷新不及時。
2、卡頓根本原因
根據Android系統顯示原理可以看到,影響繪制的根本原因有以下兩個方面:
-
繪制任務太重,繪制一幀內容耗時太長
。 -
主線程太忙,根據系統傳遞過來的VSYNC信號來時還沒準備好數據導致丟幀
。
繪制耗時太長,有一些工具可以幫助我們定位問題。主線程太忙則需要注意了,主線程關鍵職責是處理用戶交互,在屏幕上繪制像素,并進行加載顯示相關的數據,所以特別需要避免任何主線程的事情,這樣應用程序才能保持對用戶操作的即時響應
。總結起來,主線程主要做以下幾個方面工作:
-
UI生命周期控制
-
系統事件處理
-
消息處理
-
界面布局
-
界面繪制
-
界面刷新
除此之外,應該盡量避免將其他處理放在主線程中,特別復雜的數據計算和網絡請求等。
3、性能分析工具
性能問題并不容易復現,也不好定位,但是真的碰到問題還是需要去解決的,那么分析問題和確認問題是否解決,就需要借助相應的的調試工具,比如查看Layout層次的Hierarchy View、Android系統上帶的GPU Profile工具和靜態代碼檢查工具Lint等,這些工具對性能優化起到非常重要的作用,所以要熟悉,知道在什么場景用什么工具來分析。
(1)Profile GPU Rendering
在手機開發者模式下,有一個卡頓檢測工具叫做:Profile GPU Rendering,如圖:
它的功能特點如下:
-
一個圖形監測工具,能實時反應當前繪制的耗時
-
橫軸表示時間,縱軸表示每一幀的耗時
-
隨著時間推移,從左到右的刷新呈現
-
提供一個標準的耗時,如果高于標準耗時,就表示當前這一幀丟失
(2)TraceView
TraceView是Android SDK自帶的工具,用來分析函數調用過程,可以對Android的應用程序以及Framework層的代碼進行性能分析。它是一個圖形化的工具,最終會產生一個圖表,用于對性能分析進行說明,可以分析到每一個方法的執行時間,其中可以統計出該方法調用次數和遞歸次數,實際時長等參數維度,使用非常直觀,分析性能非常方便。
(3)Systrace UI 性能分析
Systrace是Android 4.1及以上版本提供的性能數據采樣和分析工具,它是通過系統的角度來返回一些信息。它可以幫助開發者收集Android關鍵子系統,如Surfaceflinger、WindowManagerService等Framework部分關鍵模塊、服務、View系統等運行信息,從而幫助開發者更直觀地分析系統瓶頸,改進性能。Systrace的功能包括跟蹤系統的I/O操作、內核工作隊列、CPU負載等,在UI顯示性能分析上提供很好的數據,特別是在動畫播放不流暢、渲染卡等問題上。
4、優化建議
布局優化
避免過度繪制
啟動優化
合理的刷新機制
其他
(1)布局優化
布局是否合理主要影響的是頁面測量時間的多少,我們知道一個頁面的顯示測量和繪制過程都是通過遞歸來完成的,多叉樹遍歷的時間與樹的高度h有關,其時間復雜度O(h),如果層級太深,每增加一層則會增加更多的頁面顯示時間,所以布局的合理性就顯得很重要。
那布局優化有哪些方法呢,主要通過 減少層級、減少測量和繪制時間、提高復用性
三個方面入手。總結如下:
-
減少層級
:合理使用RelativeLayout和LinerLayout,合理使用Merge。 -
提高顯示速度
:使用ViewStub,它是一個看不見的、不占布局位置、占用資源非常小的視圖對象。 -
布局復用
:可以通過標簽(include)來提高復用。 -
盡可能少用wrap_content
:wrap_content 會增加布局measure時計算成本,在已知寬高為固定值時,不用wrap_content。 -
其他
:刪除控件中無用的屬性。
(2)避免過度繪制
過度繪制是指在屏幕上的某個像素在同一幀的時間內被繪制了多次。在多層次重疊的UI結構中,如果不可見的UI也在做繪制的操作,就會導致某些像素區域被繪制了多次,從而浪費了多余的CPU以及GPU源。
如何避免過度繪制呢,如下:
-
布局上的優化
:移除XML中非必須的背景,移除Window默認的背景、按需顯示占位背景圖片。 -
自定義View優化
:使用 canvas.clipRect()來幫助系統識別那些可見的區域,只有在這個區域內才會被繪制。
(3)啟動優化
通過對啟動速度的監控,發現影響啟動速度的問題所在,優化啟動邏輯,提高應用的啟動速度。啟動主要完成三件事:UI布局、繪制和數據準備
。因此啟動速度優化就是需要優化這三個過程:
-
UI布局
:應用一般都有閃屏頁,優化閃屏頁的UI布局,可以通過Profile GPU Rendering檢測丟幀情況。 -
啟動加載邏輯優化
:可以采用分布加載、異步加載、延期加載策略來提高應用啟動速度。 -
數據準備
:數據初始化分析,加載數據可以考慮用線程初始化等策略。
(4)合理的刷新機制
在應用開發過程中,因為數據的變化,需要刷新頁面來展示新的數據,但頻繁刷新會增加資源開銷,并且可能導致卡頓發生,因此,需要一個合理的刷新機制來提高整體的UI流暢度。合理的刷新需要注意以下幾點:
-
盡量減少刷新次數
。 -
盡量避免后臺有高的CPU線程運行
。 -
縮小刷新區域
。
(5)其他
在實現動畫效果時,需要根據不同場景選擇合適的動畫框架來實現。有些情況下,可以用硬件加速方式來提供流暢度。
三、內存優化
在Android系統中有個垃圾內存回收機制,在虛擬機層自動分配和釋放內存,因此不需要在代碼中分配和釋放某一塊內存,從應用層面上不容易出現內存泄漏和內存溢出等問題,但是需要內存管理。Android系統在內存管理上有一個Generational Heap Memory模型,內存回收的大部分壓力不需要應用層關心,Generational Heap Memory有自己一套管理機制,當內存達到一個閾值時,系統會根據不同的規則自動釋放系統認為可以釋放的內存,也正是因為Android程序把內存控制的權力交給了Generational Heap Memory,一旦出現內存泄漏和溢出方面的問題,排查錯誤將會成為一項異常艱難的工作。除此之外,部分Android應用開發人員在開發過程中并沒有特別關注內存的合理使用,也沒有在內存方面做太多的優化,當應用程序同時運行越來越多的任務,加上越來越復雜的業務需求時,完全依賴Android的內存管理機制就會導致一系列性能問題逐漸呈現,對應用的穩定性和性能帶來不可忽視的影響,因此,解決內存問題和合理優化內存是非常有必要的。
1、Android內存管理機制
Android應用都是在 Android的虛擬機上運行,應用 程序的內存分配與垃圾回收都是由虛擬機完成的。在Android系統,虛擬機有兩種運行模式:Dalvik和ART。
(1)Java對象生命周期
一般Java對象在虛擬機上有7個運行階段:
創建階段->應用階段->不可見階段->不可達階段->收集階段->終結階段->對象空間重新分配階段
(2)內存分配
在Android系統中,內存分配實際上是對堆的分配和釋放。當一個Android程序啟動,應用進程都是從一個叫做Zygote的進程衍生出來,系統啟動 Zygote 進程后,為了啟動一個新的應用程序進程,系統會衍生Zygote進程生成一個新的進程,然后在新的進程中加載并運行應用程序的代碼。其中,大多數的RAM pages被用來分配給Framework代碼,同時促使RAM資源能夠在應用所有進程之間共享。
但是為了整個系統的內存控制需要,Android系統會為每一個應用程序都設置一個硬性的Dalvik Heap Size最大限制閾值,整個閾值在不同設備上會因為RAM大小不同而有所差異。如果應用占用內存空間已經接近整個閾值時,再嘗試分配內存的話,就很容易引起內存溢出的錯誤。
(3)內存回收機制
我們需要知道的是,在Java中內存被分為三個區域:Young Generation(新生代)、Old Generation(年老代)、Permanent Generation(持久代)。最近分配的對象會存放在Young Generation區域。對象在某個時機觸發GC回收垃圾,而沒有回收的就根據不同規則,有可能被移動到Old Generation,最后累積一定時間在移動到Permanent Generation 區域。系統會根據內存中不同的內存數據類型分別執行不同的GC操作
。GC通過確定對象是否被活動對象引用來確定是否收集對象,進而動態回收無任何引用的對象占據的內存空間。但需要注意的是頻繁的GC會增加應用的卡頓情況,影響應用的流暢性,因此需要盡量減少系統GC行為,以便提高應用的流暢度,減小卡頓發生的概率。
2、內存分析工具
做內存優化前,需要了解當前應用的內存使用現狀,通過現狀去分析哪些數據類型有問題,各種類型的分布情況如何,以及在發現問題后如何發現是哪些具體對象導致的,這就需要相關工具來幫助我們。
(1)Memory Monitor
Memory Monitor是一款使用非常簡單的圖形化工具,可以很好地監控系統或應用的內存使用情況,主要有以下功能:
-
顯示可用和已用內存,并且以時間為維度實時反應內存分配和回收情況。
-
快速判斷應用程序的運行緩慢是否由于過度的內存回收導致。
-
快速判斷應用是否由于內存不足導致程序崩潰。
(2)Heap Viewer
Heap Viewer的主要功能是查看不同數據類型在內存中的使用情況,可以看到當前進程中的Heap Size的情況,分別有哪些類型的數據,以及各種類型數據占比情況。通過分析這些數據來找到大的內存對象,再進一步分析這些大對象,進而通過優化減少內存開銷,也可以通過數據的變化發現內存泄漏。
(3)Allocation Tracker
Memory Monitor和Heap Viewer都可以很直觀且實時地監控內存使用情況,還能發現內存問題,但發現內存問題后不能再進一步找到原因,或者發現一塊異常內存,但不能區別是否正常,同時在發現問題后,也不能定位到具體的類和方法。這時就需要使用另一個內存分析工具Allocation Tracker,進行更詳細的分析,Allocation Tracker可以分配跟蹤記錄應用程序的內存分配,并列出了它們的調用堆棧,可以查看所有對象內存分配的周期。
(4)Memory Analyzer Tool(MAT)
MAT是一個快速,功能豐富的Java Heap分析工具,通過分析Java進程的內存快照HPROF分析,從眾多的對象中分析,快速計算出在內存中對象占用的大小,查看哪些對象不能被垃圾收集器回收,并可以通過視圖直觀地查看可能造成這種結果的對象。
3、常見內存泄漏場景
如果在內存泄漏發生后再去找原因并修復會增加開發的成本,最好在編寫代碼時就能夠很好地考慮內存問題,寫出更高質量的代碼,這里列出一些常見的內存泄漏場景,在以后的開發過程中需要避免這類問題。
-
資源性對象未關閉
:比如Cursor、File文件等,往往都用了一些緩沖,在不使用時,應該及時關閉它們。 -
注冊對象未注銷
:比如事件注冊后未注銷,會導致觀察者列表中維持著對象的引用。 -
類的靜態變量持有大數據對象
。 -
非靜態內部類的靜態實例
。 -
Handler臨時性內存泄漏
:如果Handler是非靜態的,容易導致Activity或Service不會被回收。 -
容器中的對象沒清理造成的內存泄漏
。 -
WebView
:WebView存在著內存泄漏的問題,在應用中只要使用一次WebView,內存就不會被釋放掉。
除此之外,內存泄漏可監控,常見的就是用LeakCanary第三方庫,這是一個檢測內存泄漏的開源庫,使用非常簡單,可以在發生內存泄漏時告警,并且生成leak tarce分析泄漏位置,同時可以提供Dump文件進行分析。
4、優化內存空間
沒有內存泄漏,并不意味著內存就不需要優化,在移動設備上,由于物理設備的存儲空間有限,Android 系統對每個應用進程也都分配了有限的堆內存,因此使用最小內存對象或者資源可以減小內存開銷,同時讓GC 能更高效地回收不再需要使用的對象,讓應用堆內存保持充足的可用內存,使應用更穩定高效地運行。常見做法如下:
-
對象引用
:強引用、軟引用、弱引用、虛引用四種引用類型,根據業務需求合理使用不同,選擇不同的引用類型。 -
減少不必要的內存開銷
:注意自動裝箱,增加內存復用,比如有效利用系統自帶的資源、視圖復用、對象池、Bitmap對象的復用。 -
使用最優的數據類型
:比如針對數據類容器結構,可以使用ArrayMap數據結構,避免使用枚舉類型,使用緩存Lrucache等等。 -
圖片內存優化
:可以設置位圖規格,根據采樣因子做壓縮,用一些圖片緩存方式對圖片進行管理等等。
四、穩定性優化
Android應用的穩定性定義很寬泛,影響穩定性的原因很多,比如內存使用不合理、代碼異常場景考慮不周全、代碼邏輯不合理等,都會對應用的穩定性造成影響。其中最常見的兩個場景是:Crash和ANR,這兩個錯誤將會使得程序無法使用,比較常用的解決方式如下:
-
提高代碼質量
:比如開發期間的代碼審核,看些代碼設計邏輯,業務合理性等。 -
代碼靜態掃描工具
:常見工具有Android Lint、Findbugs、Checkstyle、PMD等等。 -
Crash監控
:把一些崩潰的信息,異常信息及時地記錄下來,以便后續分析解決。 -
Crash上傳機制
:在Crash后,盡量先保存日志到本地,然后等下一次網絡正常時再上傳日志信息。
五、耗電優化
在移動設備中,電池的重要性不言而喻,沒有電什么都干不成。對于操作系統和設備開發商來說,耗電優化一致沒有停止,去追求更長的待機時間,而對于一款應用來說,并不是可以忽略電量使用問題,特別是那些被歸為“電池殺手”的應用,最終的結果是被卸載。因此,應用開發者在實現需求的同時,需要盡量減少電量的消耗。
在Android5.0以前,在應用中測試電量消耗比較麻煩,也不準確,5.0之后專門引入了一個獲取設備上電量消耗信息的API:Battery Historian。Battery Historian是一款由Google提供的Android系統電量分析工具,和Systrace一樣,是一款圖形化數據分析工具,直觀地展示出手機的電量消耗過程,通過輸入電量分析文件,顯示消耗情況,最后提供一些可供參考電量優化的方法。
除此之外,還有一些常用方案可提供:
-
計算優化,避開浮點運算等
。 -
避免WaleLock使用不當
。 -
使用Job Scheduler
。
六、安裝包大小優化
應用安裝包大小對應用使用沒有影響,但應用的安裝包越大,用戶下載的門檻越高,特別是在移動網絡情況下,用戶在下載應用時,對安裝包大小的要求更高,因此,減小安裝包大小可以讓更多用戶愿意下載和體驗產品。
常用應用安裝包的構成,如圖所示:
從圖中我們可以看到:
-
assets文件夾
:存放一些配置文件、資源文件,assets不會自動生成對應的 ID,而是通過AssetManager類的接口獲取。 -
res
:res是resource的縮寫,這個目錄存放資源文件,會自動生成對應的ID并映射到 .R文件中,訪問直接使用資源ID。 -
META-INF
:保存應用的簽名信息,簽名信息可以驗證APK文件的完整性。 -
AndroidManifest.xml
:這個文件用來描述Android應用的配置信息,一些組件的注冊信息、可使用權限等。 -
classes.dex
:Dalvik字節碼程序,讓Dalvik虛擬機可執行,一般情況下,Android應用在打包時通過Android SDK中的dx工具將Java字節碼轉換為Dalvik字節碼。 -
resources.arsc
:記錄著資源文件和資源ID之間的映射關系,用來根據資源ID尋找資源。
減少安裝包大小的常用方案:
-
代碼混淆
:使用ProGuard代碼混淆器工具,它包括壓縮、優化、混淆等功能。 -
資源優化
:比如使用Android Lint刪除冗余資源,資源文件最少化等。 -
圖片優化
:比如利用AAPT工具對PNG格式的圖片做壓縮處理,降低圖片色彩位數等。 -
避免重復功能的庫,使用WebP圖片格式
。 -
插件化
:比如功能模塊放在服務器上,按需下載,可以減少安裝包大小。