Android性能優化——內存優化

Android分配個應用的大小是有限制,且在設備出廠之后已經確定,單個應用可用的最大內存的配置位于/system/build.prop文件中的dalvik.vm.heapgrowthlimit配置項。
雖然Android使用的JVM具有內存管理(自動回收)的能力,但是對內存使用不當會導致應用出現異常,包括常見的OOM、內存泄漏、內存抖動等引發的崩潰、卡頓等現象。我們一般主要針對這三種內存問題進行優化處理:

  • OOM
    Out of memory, 內存溢出,當應用申請內存發現超出了JVM的最大限制時候,就會拋出內存溢出異常,引發程序崩潰。引發OOM常見原因有:內存泄漏的累積導致無法申請更多內存、創建大內存對象(如大容量數組、載入大的文件、載入大的圖片等)。
  • 內存泄漏
    一個對象的超出了其生命周期,導致JVM無法回收。這樣無法回收的對象堆積多了會導致應用可能無法申請到內存進而導致OOM。常見的內存泄漏有:
    • 單例持引起的內存泄漏,如單例持有activity、context、view、drawabl等
    • 靜態變量引起的內存泄漏,如靜態變量持有activity、context、view、drawabl等
    • 非靜態內部類引起的內存泄漏,原因:非靜態內部類會隱式持有外部類實例
    • 匿名內部類引起的內存泄漏,如handler、線程匿名內部類runnable、callback等
    • 資源未釋放引起的內存泄漏,如讀寫文件沒有關閉、網絡流操作沒有關閉、Bitmap沒有釋放等
    • 廣播沒有及時取消注冊
  • 內存抖動
    內存抖動是因為在頻繁的創建、回收對象,引發的頻繁GC,進而影響主線程,最終導致卡頓現象。

要知道怎么正確的使用內存,首先需要了解java虛擬機(即JVM)的內存管理機制。

JVM內存管理機制

JVM內存管理機制是通過根搜索算法,在合適的時期檢索對象是否可達,當對象不可達時會被回收,如果對象可達則不會被回收。

對象的生命周期

一個對象從創建到銷毀回收是其生命周期的表現,在開發階段我們是可以預測到對象的生命周期范圍,什么時候創建什么時候回收,如果沒有被正常回收就會引發對象不可被回收導致的內存泄漏。

哪些對象需要回收

使用根搜索算法GC Root Trace通過一系列名為GC Root的對象作為起點,向下搜索,搜索所經過的路徑稱為引用鏈,當一個對象到GC Root沒有應用鏈相連,則表明此對象需要回收。以下是可以作為GC Root的對象:

  • 全局靜態變量引用的對象
  • 全局常量引用的對象
  • 虛擬機棧幀中本地變量表中引用的對象
  • 本地方法棧幀中本地變量表中引用的對象
什么時候回收內存

介紹什么時候回收內存前,先介紹下Android虛擬機的堆塊的管理情況,Android虛擬機遵循java虛擬機堆內存分代管理的機制,主要劃分為:新生代、老年代,其中新生代又分為1個Eden(新生代)和2個survisor(Eden幸存的對象),eden、survisor內存默認按8:1:1,因為很多對象創建使用過后就會回收,真正存活下來的不會很多,所以給eden分配80%的占比可以有效提升內存使用率,每次使用eden和1個survisor,回收時將存活的復制到另一個survisor中,然后清空eden和survisor,什么時候會回收內存呢?一般是在各個內存分代區內存不足或者內存快滿時會觸發內存回收即GC。新生代觸發的是Minor GC,因為新生代大多數是朝生夕滅,所以Minor GC比較頻繁,但速度會比較快;當survisor中內存不足或者存活年齡達到一定在就會將相應的survisor中的對象復制到老年代中,老年代內存回收是Major GC/Full GC,一般都會伴隨至少一次的Minor GC,Major GC速度相對比較慢,相比Minor GC可能會慢10倍。

怎么回收對象

新生代使用復制算法,將eden和1個survisor的內存復制到另一個survisor中,接著清空原先的eden和survisor;老年代使用標記—整理算法,即先標記要回收的對象,再把存活的對象移到一段,接著就是清理掉端邊界以外的對象。

不論Minor GC還是Major GC在回收內存的時候都會阻塞其它的工作線程,等完成GC之后再恢復工作線程。

內存優化

上面講述了虛擬機內存管理機制,對應內存的優化有以下建議:

防止內存泄漏
  • 避免全局靜態變量持有資源對象:如activity、非applicationcontext、fragment、view等
  • 避免全局常量持有有資源對象:如activity、非applicationcontext、fragment、view等
  • 避免單例持有資源對象:如activity、非applicationcontext、fragment、view等
  • 對于內部類要么使用靜態內部類+弱引用,要么使用弱引用
  • 對于匿名內部類使用弱引用引用外部引用
  • 資源使用完之后,及時釋放:文件io、cursor、網絡io用完之后及時釋放
防止內存抖動
  • 避免創建大內存對象,如:大內存數組、加載大文件或者圖片
  • 避免頻繁創建對象,如:避免在for語句中創建大量對象
  • 需要頻繁使用的對象,可以通過緩存池復用,避免重復創建、釋放,在內存緊張OnTrimMemory /OnLowMemory 時適當釋放可以釋放的資源或者對象
  • 使用圖片是可以時候565或者對圖片進行裁剪、降低圖片質量,也可以使用Glide,滑動時暫停加載圖片,不滑動時恢復加載圖片
  • 字符串相加或者拼接通過StringBuilder替代,較少創建String對象節省內存
  • 使用SpareArray、ArrayMap替代HashMap

內存分析

LeakCannary

項目中依賴LeakCannary庫,使用LeakCannary可以檢測內存泄漏

Memory Profiler

使用Android Studio的內存分析器可以對內存分析,根據分析結果進行相應的優化
如需打開內存分析器,按以下步驟操作:

  • 依次點擊View——》Tools windows——》Profiler(或者點擊工具欄中的Profiler圖標)
  • 從Android Profiler工具欄中選擇要分析的設備和應用進程
  • 點擊MEMORY時間軸上的任意位置打開內存性能分析器

打開內存性能分析器后,其界面如下圖所示:


memory_profiler.png

1、強制執行垃圾回收按鈕
2、選擇捕獲堆轉存heap dump的按鈕
3、暫停/跳轉到實時內存數據的按鈕
4、事件時間軸:顯示應用活動狀態、用戶輸入事件(如touch的down/press等)、屏幕旋轉事件等
5、內存分類用量統計

  • total: 總共分配的對象的內存大小
  • Java: java/kotlin代碼分配的對象的內存大小
  • Native: c/c++分配的對象的內存大小
  • Graphics: 圖片緩沖區隊列向屏幕顯示像素所使用的內存大小(這部分是CPU共享的內存,而不是GPU)
  • Stack: 應用java和原生堆棧使用的內存。
  • Code: 應用處理代碼和資源的內存(包括處理dex字節碼、so庫、字體等)
  • others: 應用使用了系統不確定如何分類的內存大小
  • Allocated: 應用分配的java/kotlin對象數(不含c/c++分配的對象數)
    6、以圖表、坐標軸的方式顯示內存分配情況,x坐標顯示的是時間、y軸左側標記部分代表內存大小、y軸右側標記部分代表分配對象數、圖表部分代表各個類別分配的對象的內存大小
捕獲堆轉儲

選擇內存分析器中的Capture heap dump,點擊下方的Record按鈕,就開始捕獲堆轉儲了,可以點擊stop結束捕獲,結束捕獲之后會自動加載捕獲到堆轉儲。下圖是捕獲到heap dump之后,打開的界面


heap_dump2.png

1、過濾器
這部分主要用于對heap dump的數據進行過濾,過濾我們關注、需要分享的部分,包括

  • 選擇需要檢查的堆類型:
    • view all heap:檢查分配內存的所有堆
    • view app heap:默認,檢查應用在使用時分配內存的主堆
    • view image heap: 系統啟動映像,包括啟動期間預加載的類
    • view zygote heap: 檢查寫時復制堆,這部分是應用通過zygote 創建啟動進程時的堆

我們應用端一般主要分析view app heap進行分析主堆,排在java層面的內存問題

  • 選擇如何安排分派
    • Arrang by class:默認,根據類名稱對所有內存分配進行分組
    • Arrang by package: 根據包名對所有內存分配進行分組
    • Arrange by callstack: 根據調用堆棧對所有內存分配進行分組

一般采用采用Arrang by class過濾占用內存占比比較高的類進行分析,Arrang by package根據包名定位自己代碼、三方代碼的內存問題

  • 選擇顯示那些類型的數據
    • Show all class: 默認,顯示所有的類
    • Show activity/fragment Leak: 顯示發生內存泄漏的activity/fragment
    • Show project class: 進顯示項目相關的類
  • 輸入過濾:在輸入框中可以輸入類名/包名來快速定位到具體類/包名下類的內存分配情況

2、統計信息

  • classes: 類類型總數,不是實例對象哦
  • Leak:發生內存泄漏的數量
  • count: 總關創建的使用的實例對象數
  • Native Size: 原生c/c++使用的內存總量
  • Shallow Size: java使用的內存總量
  • Retained Size: 還在使用保留的內存總量

3、創建的對象數其分配內存情況
這部分會列舉過濾之后的所有類名、分配的對象數及內存使用情況,包括

  • Class Name: 類名
  • Allocations: 此類創建的實例對象數量
  • Native Size: 此類總共使用的原生內存總量(只有android7.0+設備才能看到)(單位字節)
  • Shallow Size: 此類使用的java內存總量(單位字節)
  • Retained Size: 此類實例對象仍存活而保留的內存總大小(單位字節)

4、類實例對象列表及其實例對象的詳細信息
在3中點擊某一個類,會在下半部分顯示此類的所有實例對象的信息,如點擊圖中的bitmap。
這部分左側顯示類的實例對象列表:實例對象+地址;點擊某個實例會在右側顯示此實例內存分配的詳細信息,包括:

  • Fields
    實例對象每個字段信息,包括如下信息:

    • Instance 此字段的名稱及其類型,如果是基本數據類型和String會同時顯示此字段的當前值
    • Depth: 此字段字段可達的最短跳數,表示的是任意一個GC Root到此字段的最短鏈路邊數
    • Native Size: 原生內存中此字段的內存大小(只有Android7.0+上的設備才會看到此列)
    • Shallow Size: Java 內存中此字段的內存大小
    • Retained Size: 此字段目前還保留的內存大小
  • References:
    實例對象的引用鏈信息,References中包括如下信息:

    • Reference: 實例對象的引用鏈,可以依次點擊展開顯示此實例被哪些實例對象所引用,通過引用鏈可以最終追蹤到GC Root
    • Depth: 此實例對象可達的最短跳數,表示的是任意一個GC Root到此實例對象的最短鏈路邊數
    • Native Size: 原生內存中此實例對象的內存大小(只有Android7.0+上的設備才會看到此列)
    • Shallow Size: Java 內存中此實例對象的內存大小
    • Retained Size: 此實例對象目前還保留的內存大小

我們可以在Fields和References中分析,如果發現可以點如可能存在內存泄漏等,可以右鍵選擇Go to Instance顯示其實例內存數據;或者選擇Jump to source進入此實例對象所在的源碼片段。

一般我們使用內存分析器對內存進行分析時,注重點在于:

  • 首先關注內存占比比較高的類及其實例對象引用鏈情況,排查是否有內存泄漏、是否有優化空間
  • 關注某些類的實例對象數量比較大情況,排查是存在大量創建短時間內又銷毀引起內存抖動,結合源碼分析是否有優化空間,如使用緩存池等
  • 關注activity、context、view、Drawable等對象及其引用鏈情況,排查這些是否存在內存泄漏
  • 關注項目相關類的內存分配及其應用鏈情況,排查是否存在內存泄漏、使用不當情況等
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容