App內存優化

內存問題

內存直接會影響兩個問題:html

  • 異常。OOM、內存分配失敗、應用被殺、設備重啓等。
  • 卡頓。頻繁 GC,在 Dalvik 上尤其明顯,雖然 ART 作了優化,可是依然會形成設備卡頓。另外,頻繁的出發 LMK 也會形成卡頓。

ART 比 Dalvik 在內存分配和 GC 效率上,提高了 5~10 倍。java

擴展問題:OOM 能夠被 try-catch 住嗎?分狀況,若是在 try 語句中存在申請內存的操做,此時觸發的 OOM 是能夠被 catch 住的。可是並無用,由於 OOM 的觸發一般是壓死駱駝最後一根稻草,此處 catch 住了,在別的地方可能也會崩潰。一個有效的方案,是在 catch 塊中,釋放內存,來保證當前運行環境的健康。python

GC 的性能,能夠經過發送 SIGQUIT 信號獲取 ANR 日誌,可是在高版本中,獲取 traces.txt 文件須要 Root 權限,這裏不推薦。android

另一個分析 GC 的工具是使用 systrace。systrace 在 Google IO 2017 被推薦,是分析 Android 設備性能的主要工具。它其實是對 atrace 的主機端封裝容器,用於控制用戶空間和設置 ftrace 的設備端可執行文件。推薦閱讀《瞭解 Systrace》《手把手教你使用Systrace》《systrace》c++

內存問題的兩個誤區:

  • 內存佔用越少越好。
  • Native 內存不用管。

誤區一:內存佔用越少越好。

VSS、PSS、Java 堆內存不足均可能會引發異常和卡頓。web

可是到底佔用多少內存,這是由實際狀況,例如:設備、系統、當時運行環境等多個因素影響的。因此不存在一個標準的數值。可以作到的就是「用時分配,及時釋放」。正則表達式

上圖這樣的就算是好的效果,在須要時,能夠被釋放,而不是一直漲。shell

"一張圖,毀十優",在 Android 中,Bitmap 是內存佔用的大戶。數組

簡單回顧 Bitmap 在不一樣 Android 版本中的區別:瀏覽器

Android 3.0 以前,Bitmap 對象放在 Java 堆,像素數據放在 Native 內存。若是不手動調用 recycle,Bitmap Native 內存的回收徹底依賴 finalize 函數調用,這個時機是不可控的。

Android 3.0~7.0,Bitmap 對象和像素數據統一放在 Java 堆中。這樣就算咱們不調用 recycle,Bitmap 內存也會隨着對象一塊兒被回收。雖然這樣方便回收,可是 Bitmap 放在 Java 堆中會引起大量的 GC。

Android 8.0,利用 NativeAllocationRegistry 來保證 Bitmap 的像素數據放在 Native 內存,可是依然能夠和對象一塊兒被快速釋放。8.0 還新增了硬件位圖 Hardware Bitmap,它能夠減小圖片內存並提高繪製效率。

誤區二:Native 內存不用管

當內存不足的時候,LMK 就開始工做了,按照優先級,從後臺、桌面、服務、前臺、直到手機重啓。

Fresco 會把 Bitmap 放在 Native 內存中,它的原理是利用 libandroid_runtime.so 在 Native 中建立一個 Bitmap 對象,再按正常流程將 Bitmap 加載到 Java 堆中,接下來再把它繪製入前面申請的空的 Native Bitmap 中,而後釋放 Java 內存中的圖片,實現轉換的效果。

注意這樣很是規的操做,會帶來兩個問題:

  • 不一樣 Android 系統版本的兼容性問題。
  • 頻繁申請、釋放 Java Bitmap 致使內存抖動。

測量方法

1、Java 內存分配

Java 內存分配分析工具,經常使用的有 Allocation Tracker 和 MAT。

Allocation Tracker 的三個缺點:

獲取信息過於分散,數據中夾雜其餘信息。 沒法自動化分析。 中止時會把全部數據 dump 出來,此過程可能會形成手機徹底卡死,甚至可能引起 ANR。 Allocation Tracker 的開啓方式:

// dalvik
bool dvmEnableAllocTracker()
// art
void setAllocTrackingEnable()
複製代碼

2、Native 內存分配

Native 內存能夠使用 AddressSanitizer(ASan),Android 以前的版本對 ASan 的支持不太好,須要 Root 和一些額外的操做,自 8.0 開始,就方便不少了,而且無需 Root。

ASan 是一種基於編譯器的快速檢測工具,用於檢測原生代碼中的內存錯誤。ASan 會檢測堆棧和全局對象是否移除,可是不會檢測未初始化的讀取和內存泄露。ASan 在 Android 下的使用,參考指南。

調試 Native 內存,如今有兩種方法:

Malloc 調試 Malloc 鉤子 Malloc 調試能夠去調試 Native 內存的一些使用問題,例如堆破壞、內存泄露、非法地址等。

可是 Mallock 調試時,App 會變卡,有產生 ANR 的風險。

Malloc 調試在 8.0 以前須要 Root 權限,最簡單的方法是找一臺 8.0 的設備進行調試。

adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
複製代碼

Malloc 鉤子是在 Android P 以後纔有的。Android 的 libc 支持攔截在程序執行期間發生的全部分配/釋放調用,方便咱們構建自定義的內存檢測工具。

adb shell setprop wrap.<APP> '"LIBC_HOOKS_ENABLE=1"'
複製代碼

優化

內存優化,從三個維度入手:

  • 設備分級
  • Bitmap 優化
  • 內存泄露

1、設備分級

內存優化首先須要根據設備環境來綜合考慮。內存佔用並不是越少越好,而是應該在佔用內存和設備內存之間取平衡。

資源加載在內存中,意味着下次使用的時候會更快。所以咱們可讓高端設備使用更多的內存,作到針對設備性能的好壞使用不一樣的內存分配和回收策略。

一、設備分級

使用內存狀況對設備分級,相似 device-year-class 的策略,對低端設備能夠關閉複雜動畫或某些功能;使用 565 格式的圖片,使用更小的緩存內存等。

二、緩存管理

有一套統一的緩存管理機制,能夠適當地使用內存,當內存不足時,能夠有效的歸還內存。能夠使用 onTrimMemort 回調,根據不一樣狀態決定釋放多少內存。

三、進程模型

一個空的進程,也會佔用 10MB 的內存,而有些應用啓動就有十幾個進程。能夠經過減小應用啓動的進程數、減小常駐進程。

四、安裝包大小。

安裝包中的代碼、資源、圖片以及 so 庫的說起,和他們運行時佔用的內存有關係。減小 APK 的大小,也能夠優化低端機上的運行效果。例如各大 App 會推出 Xxx Lite 版本。

2、Bitmap優化

Bitmap 是內存優化的「永恆主題」。

方法一:統一圖片庫

圖片內存優化的前提是收攏圖片的調用,這樣能夠作總體的控制策略。

Glide、Fresco 或者是自研的圖片庫,不管用什麼,要作到統一。不然各個庫內部本身維護緩存,單看其實作的都很好,讓內存保持在一個良好的環境下,可是加起來就可能致使內存爆表。

方法二:統一監控

要對一些特殊的場景,作好監控。

大圖片監控。當知道顯示圖片控件的寬高時,是能夠算出這個圖片佔用的合理內存的。若是大於此合理值,就應該通報出來,提醒開發者,例如彈窗。 重複圖片監控。指的是 Bitmap 像素數據徹底一致,可是有多個不一樣對象存在。 圖片總內存。由於使用統一圖片庫,咱們很容易統計應用全部圖片佔用的內存。在發生 OOM 時,也能夠把圖片佔用的總內存,Top N 的圖片寫到 Log 中,幫助咱們排查。

3、內存泄露

內存泄露簡單來講,就是再也不使用的內存,沒法被回收。

內存泄露主要分兩種狀況:

  • 同一對象泄露。
  • 每次都泄露新的對象。

能夠經過集中策略監控內存泄露:

  • Java 內存泄露。使用相似 LeakCanary 自動化檢測方案,至少作到在 Java 層不存在內存泄露。
  • OOM 監控。OOM 時,記錄信息或者內存快照,達到後期分析的目的。 Native 內存泄露監控。推薦《微信 Android 終端內存優化實踐》
  • 針對沒法重編 so 的狀況。使用 PLT Hook 攔截庫的內存分配函數,就能夠重定向到本身的實現後記錄分配的內存地址、大小、來源 so 庫等信息。
  • 針對可重編的 so 狀況。經過 GCC 的 "-finstrument-functions"參數給全部函數插裝,莊中模擬調用棧,入棧出棧操做;經過 id 的 「--wrap」參數攔截內存分配和釋放函數。

內存泄露監控,Android 下佔時只有 Java 層的內存泄露有成熟的技術方案,剩下的都是實驗室方案,能夠在開發階段使用,不建議在線上 App內集成發佈。

內存監控

一、採集方式

針對部分用戶,每 5 分鐘採集一次 PSS、Java 堆、圖片總內存。

PSS:實際使用物理內存。

二、計算指標

經過標準的計算指標,來分析內存是否良好。

全部的優化,都是針對某一個指標進行優化。內存優化也是如此。須要有一個標準的指標來輔助咱們識別當前的狀況,以及優化的效果。

內存異常率:

內存 UV 異常率 = PPS 超過 400MB 的 UV / 採集 UV

觸頂率:

統計超過 85% 的內存佔用狀況。

內存 UV 觸頂率 = Java 堆佔用超過 85% 的 UV / 採集 UV

long javaMax = runtime.maxMemory();
long javaTotal = runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
// Java 內存使用超過最大限制的 85%
float proportion = (float) javaUsed / javaMax;
複製代碼

三、GC 監控

在開發階段,能夠經過 Debug.startAllocCounting 來監控 Java 內存分配和 GC 狀況。

long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();
複製代碼

Android 6.0 以上能夠拿到更精準的數據。

// 運行的 GC 次數
Debug.getRuntimeStat("art.gc.gc-count");
// GC 使用的總耗時,單位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式 GC 的次數
Debug.getRuntimeStat("art.gc.blocking-gc-count");
// 阻塞式 GC 的總耗時
Debug.getRuntimeStat("art.gc.blocking-gc-time");
阻塞式 GC 會暫停 App 線程,可能致使卡頓。
複製代碼

日誌

Dalvik 日誌消息

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
複製代碼
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
複製代碼

垃圾回收緣由

什麼觸發了垃圾回收以及是哪一種回收。可能出現的緣由包括:

  • GC_CONCURRENT 在您的堆開始佔用內存時能夠釋放內存的併發垃圾回收。
  • GC_FOR_MALLOC 堆已滿而系統不得不中止您的應用並回收內存時,您的應用嘗試分配內存而引發的垃圾回收。
  • GC_HPROF_DUMP_HEAP 當您請求建立 HPROF 文件來分析堆時出現的垃圾回收。
  • GC_EXPLICIT 顯式垃圾回收,例如當您調用 gc() 時(您應避免調用,而應信任垃圾回收會根據須要運行)。
  • GC_EXTERNAL_ALLOC 這僅適用於 API 級別 10 及更低級別(更新版本會在 Dalvik 堆中分配任何內存)。外部分配內存的垃圾回收(例如存儲在原生內存或 NIO 字節緩衝區中的像素數據)。

釋放量

今後次垃圾回收中回收的內存量。

堆統計數據

堆的可用空間百分比與(活動對象數量)/(堆總大小)。

外部內存統計數據

API 級別 10 及更低級別的外部分配內存(已分配內存量)/(發生回收的限值)。

暫停時間

堆越大,暫停時間越長。併發暫停時間顯示了兩個暫停:一個出如今回收開始時,另外一個出如今回收快要完成時。

在這些日誌消息積聚時,請注意堆統計數據的增大(上面示例中的 3571K/9991K 值)。若是此值繼續增大,可能會出現內存泄漏。

ART 日誌消息

與 Dalvik 不一樣,ART 不會爲未明確請求的垃圾回收記錄消息。只有在認爲垃圾回收速度較慢時纔會打印垃圾回收。更確切地說,僅在垃圾回收暫停時間超過 5ms 或垃圾回收持續時間超過 100ms 時。若是應用未處於可察覺的暫停進程狀態,那麼其垃圾回收不會被視爲較慢。始終會記錄顯式垃圾回收。

ART 會在其垃圾回收日誌消息中包含如下信息:

I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
複製代碼
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms
複製代碼

垃圾回收緣由

什麼觸發了垃圾回收以及是哪一種回收。可能出現的緣由包括:

  • Concurrent 不會暫停應用線程的併發垃圾回收。此垃圾回收在後臺線程中運行,並且不會阻止分配。
  • Alloc 您的應用在堆已滿時嘗試分配內存引發的垃圾回收。在這種狀況下,分配線程中發生了垃圾回收。
  • Explicit 由應用明確請求的垃圾回收,例如,經過調用 gc() 或 gc()。與 Dalvik 相同,在 ART 中,最佳作法是您應信任垃圾回收並避免請求顯式垃圾回收(若是可能)。不建議使用顯式垃圾回收,由於它們會阻止分配線程並沒必要要地浪費 CPU 週期。若是顯式垃圾回收致使其餘線程被搶佔,那麼它們也可能會致使卡頓(應用中出現間斷、抖動或暫停)。
  • NativeAlloc 原生分配(如位圖或 RenderScript 分配對象)致使出現原生內存壓力,進而引發的回收。
  • CollectorTransition 由堆轉換引發的回收;此回收由運行時切換垃圾回收引發。回收器轉換包括將全部對象從空閒列表空間複製到碰撞指針空間(反之亦然)。當前,回收器轉換僅在如下狀況下出現:在 RAM 較小的設備上,應用將進程狀態從可察覺的暫停狀態變動爲可察覺的非暫停狀態(反之亦然)。
  • HomogeneousSpaceCompact 齊性空間壓縮是空閒列表空間到空閒列表空間壓縮,一般在應用進入到可察覺的暫停進程狀態時發生。這樣作的主要緣由是減小 RAM 使用量並對堆進行碎片整理。
  • DisableMovingGc 這不是真正的垃圾回收緣由,但請注意,發生併發堆壓縮時,因爲使用了 GetPrimitiveArrayCritical,回收遭到阻止。通常狀況下,強烈建議不要使用 GetPrimitiveArrayCritical,由於它在移動回收器方面具備限制。
  • HeapTrim 這不是垃圾回收緣由,但請注意,堆修剪完成以前回收會一直受到阻止。

垃圾回收名稱

ART 具備能夠運行的多種不一樣的垃圾回收。

Concurrent mark sweep (CMS)

整個堆回收器,會釋放和回收映像空間之外的全部其餘空間。

Concurrent partial mark sweep

幾乎整個堆回收器,會回收除了映像空間和 zygote 空間之外的全部其餘空間。

Concurrent sticky mark sweep

生成回收器,只能釋放自上次垃圾回收以來分配的對象。此垃圾回收比完整或部分標記清除運行得更頻繁,由於它更快速且暫停時間更短。

Marksweep + semispace

非併發、複製垃圾回收,用於堆轉換以及齊性空間壓縮(對堆進行碎片整理)。

釋放的對象

這次垃圾回收從非大型對象空間回收的對象數量。

釋放的大小

這次垃圾回收從非大型對象空間回收的字節數量。

釋放的大型對象

這次垃圾回收從大型對象空間回收的對象數量。

釋放的大型對象大小

這次垃圾回收從大型對象空間回收的字節數量。

堆統計數據

空閒百分比與(活動對象數量)/(堆總大小)。

暫停時間

一般狀況下,暫停時間與垃圾回收運行時修改的對象引用數量成正比。當前,ART CMS 垃圾回收僅在垃圾回收即將完成時暫停一次。移動的垃圾回收暫停時間較長,會在大部分垃圾回收期間持續出現。

若是您在 logcat 中看到大量的垃圾回收,請注意堆統計數據的增大(上面示例中的 25MB/38MB 值)。若是此值繼續增大,且始終沒有變小的趨勢,則可能會出現內存泄漏。或者,若是您看到緣由爲「Alloc」的垃圾回收,那麼您的操做已經快要達到堆容量,而且將很快出現 OOM 異常。

總體內存分配

使用下面的 adb 命令觀察應用內存在不一樣類型的 RAM 分配之間的劃分狀況:

adb shell dumpsys meminfo <package_name|pid> [-d]
複製代碼

-d 標誌會打印與 Dalvik 和 ART 內存使用狀況相關的更多信息。

輸出列出了應用的全部當前分配,單位爲千字節。

檢查此信息時,您應熟悉下列類型的分配:

私有(乾淨和髒)RAM

這是僅由您的進程使用的內存。這是您的應用進程被破壞時系統能夠回收的 RAM 量。一般狀況下,最重要的部分是私有髒 RAM,它的開銷最大,由於只有您的進程使用它,並且其內容僅存在於 RAM 中,因此沒法被分頁以進行存儲(由於 Android 不使用交換)。全部的 Dalvik 和您進行的原生堆分配都將是私有髒 RAM;您與 Zygote 進程共享的 Dalvik 和原生分配是共享的髒 RAM。

按比例分配佔用內存 (PSS)

這表示您的應用的 RAM 使用狀況,考慮了在各進程之間共享 RAM 頁的狀況。您的進程獨有的任何 RAM 頁會直接影響其 PSS 值,而與其餘進程共享的 RAM 頁僅影響與共享量成比例的 PSS 值。例如,兩個進程之間共享的 RAM 頁會將其一半的大小貢獻給每一個進程的 PSS。

PSS 結果一個比較好的特性是,您能夠將全部進程的 PSS 相加來肯定全部進程正在使用的實際內存。這意味着 PSS 適合測定進程的實際 RAM 比重和比較其餘進程的 RAM 使用狀況與可用總 RAM。

例如,下面是 Nexus 5 設備上地圖進程的輸出。此處信息較多,但討論的關鍵點以下所示。

adb shell dumpsys meminfo com.google.android.apps.maps -d
複製代碼
** MEMINFO in pid 18227 [com.google.android.apps.maps] **
                   Pss  Private  Private  Swapped     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap    10468    10408        0        0    20480    14462     6017
  Dalvik Heap    34340    33816        0        0    62436    53883     8553
 Dalvik Other      972      972        0        0
        Stack     1144     1144        0        0
      Gfx dev    35300    35300        0        0
    Other dev        5        0        4        0
     .so mmap     1943      504      188        0
    .apk mmap      598        0      136        0
    .ttf mmap      134        0       68        0
    .dex mmap     3908        0     3904        0
    .oat mmap     1344        0       56        0
    .art mmap     2037     1784       28        0
   Other mmap       30        4        0        0
   EGL mtrack    73072    73072        0        0
    GL mtrack    51044    51044        0        0
      Unknown      185      184        0        0
        TOTAL   216524   208232     4384        0    82916    68345    14570

 Dalvik Details
        .Heap     6568     6568        0        0
         .LOS    24771    24404        0        0
          .GC      500      500        0        0
    .JITCache      428      428        0        0
      .Zygote     1093      936        0        0
   .NonMoving     1908     1908        0        0
 .IndirectRef       44       44        0        0

 Objects
               Views:       90         ViewRootImpl:        1
         AppContexts:        4           Activities:        1
              Assets:        2        AssetManagers:        2
       Local Binders:       21        Proxy Binders:       28
       Parcel memory:       18         Parcel count:       74
    Death Recipients:        2      OpenSSL Sockets:        2
複製代碼

下面是 Gmail 應用的 Dalvik 上一個較舊版本的 dumpsys:

** MEMINFO in pid 9953 [com.google.android.gm] **
                 Pss     Pss  Shared Private  Shared Private    Heap    Heap    Heap
               Total   Clean   Dirty   Dirty   Clean   Clean    Size   Alloc    Free
              ------  ------  ------  ------  ------  ------  ------  ------  ------
  Native Heap      0       0       0       0       0       0    7800    7637(6)  126
  Dalvik Heap   5110(3)    0    4136    4988(3)    0       0    9168    8958(6)  210
 Dalvik Other   2850       0    2684    2772       0       0
        Stack     36       0       8      36       0       0
       Cursor    136       0       0     136       0       0
       Ashmem     12       0      28       0       0       0
    Other dev    380       0      24     376       0       4
     .so mmap   5443(5) 1996    2584    2664(5) 5788    1996(5)
    .apk mmap    235      32       0       0    1252      32
    .ttf mmap     36      12       0       0      88      12
    .dex mmap   3019(5) 2148       0       0    8936    2148(5)
   Other mmap    107       0       8       8     324      68
      Unknown   6994(4)    0     252    6992(4)    0       0
        TOTAL  24358(1) 4188    9724   17972(2)16388    4260(2)16968   16595     336

 Objects
               Views:    426         ViewRootImpl:        3(8)
         AppContexts:      6(7)        Activities:        2(7)
              Assets:      2        AssetManagers:        2
       Local Binders:     64        Proxy Binders:       34
    Death Recipients:      0
     OpenSSL Sockets:      1

 SQL
         MEMORY_USED:   1739
  PAGECACHE_OVERFLOW:   1164          MALLOC_SIZE:       62
複製代碼

一般狀況下,僅需關注 Pss Total 和 Private Dirty 列。一些狀況下,Private Clean 和 Heap Alloc 列提供的數據也須要關注。您須要關注的不一樣內存分配(各行)的詳細信息以下:

Dalvik Heap

您的應用中 Dalvik 分配佔用的 RAM。Pss Total 包括全部 Zygote 分配(如上述 PSS 定義所述,經過進程之間的共享內存量來衡量)。Private Dirty 數值是僅分配到您應用的堆的實際 RAM,由您本身的分配和任何 Zygote 分配頁組成,這些分配頁自從 Zygote 派生應用進程以來已被修改。 注:在包含 Dalvik Other 部分的更新的平臺版本上,Dalvik 堆的 Pss Total 和 Private Dirty 數值不包括 Dalvik 開銷(例如即時 (JIT) 編譯和垃圾回收記錄),而較舊的版本會在 Dalvik 中將其一併列出。

Heap Alloc 是 Dalvik 和原生堆分配器爲您的應用跟蹤的內存量。此值大於 Pss Total 和 Private Dirty,由於您的進程從 Zygote 派生,且包含您的進程與全部其餘進程共享的分配。

.so mmap 和 .dex mmap

映射的 .so(原生)和 .dex(Dalvik 或 ART)代碼佔用的 RAM。Pss Total 數值包括應用之間共享的平臺代碼;Private Clean 是您的應用本身的代碼。一般狀況下,實際映射的內存更大 - 此處的 RAM 僅爲應用執行的代碼當前所需的 RAM。不過,.so mmap 具備較大的私有髒 RAM,由於在加載到其最終地址時對原生代碼進行了修改。

.oat mmap

這是代碼映像佔用的 RAM 量,根據多個應用一般使用的預加載類計算。此映像在全部應用之間共享,不受特定應用影響。

.art mmap

這是堆映像佔用的 RAM 量,根據多個應用一般使用的預加載類計算。此映像在全部應用之間共享,不受特定應用影響。儘管 ART 映像包含 Object 實例,它仍然不會計入您的堆大小。

.Heap(僅帶有 -d 標誌)

這是您的應用的堆內存量。不包括映像中的對象和大型對象空間,但包括 zygote 空間和非移動空間。

.LOS(僅帶有 -d 標誌)

這是由 ART 大型對象空間佔用的 RAM 量。包括 zygote 大型對象。大型對象是全部大於 12KB 的原語數組分配。

.GC(僅帶有 -d 標誌)

這是內部垃圾回收量(考慮了應用開銷)。真的沒有任何辦法減小這一開銷。

.JITCache(僅帶有 -d 標誌)

這是 JIT 數據和代碼緩存佔用的內存量。一般狀況下爲 0,由於全部的應用都會在安裝時編譯。

.Zygote(僅帶有 -d 標誌)

這是 zygote 空間佔用的內存量。zygote 空間在設備啓動時建立且永遠不會被分配。

.NonMoving(僅帶有 -d 標誌)

這是由 ART 非移動空間佔用的 RAM 量。非移動空間包含特殊的不可移動對象,例如字段和方法。您能夠經過在應用中使用更少的字段和方法來減小這一部分。

.IndirectRef(僅帶有 -d 標誌)

這是由 ART 間接引用表佔用的 RAM 量。一般狀況下,此量較小,但若是很高,能夠經過減小使用的本地和全局 JNI 引用數量來減小此 RAM 量。

Unknown

系統沒法將其分類到其餘更具體的一個項中的任何 RAM 頁。當前,此類 RAM 頁主要包含原生分配,因爲地址空間佈局隨機化 (ASLR) 而沒法在收集此數據時經過工具識別。與 Dalvik 堆相同,Unknown 的 Pss Total 考慮了與 Zygote 的共享,且 Private Dirty 是僅由您的應用佔有的未知 RAM。

TOTAL

您的進程佔用的按比例分配佔用內存 (PSS) 總量。等於上方全部 PSS 字段的總和。表示您的進程佔用的內存量佔總體內存的比重,能夠直接與其餘進程和可用總 RAM 比較。 Private Dirty 和 Private Clean 是您的進程中的總分配,未與其餘進程共享。它們(尤爲是 Private Dirty)等於您的進程被破壞後將釋放回系統中的 RAM 量。髒 RAM 是由於已被修改而必須保持在 RAM 中的 RAM 頁(由於沒有交換);乾淨 RAM 是已從某個持久性文件(例如正在執行的代碼)映射的 RAM 頁,若是一段時間不用,能夠移出分頁。

ViewRootImpl

您的進程中當前活動的根視圖數量。每一個根視圖都與一個窗口關聯,所以有助於您肯定涉及對話框或其餘窗口的內存泄漏。

AppContexts 和 Activities

您的進程中當前活動的應用 Context 和 Activity 對象數量。這能夠幫助您快速肯定因爲存在靜態引用(比較常見)而沒法進行垃圾回收的已泄漏 Activity 對象。這些對象常常擁有不少關聯的其餘分配,所以成爲跟蹤大型內存泄漏的一種不錯的方式。

注:View 或 Drawable 對象也會保持對其源 Activity 的引用,所以保持 View 或 Drawable 對象也會致使您的應用泄漏 Activity。

Memory Profiler

Memory Profiler 是 Android Profiler 中的一個組件,可幫助您識別致使應用卡頓、凍結甚至崩潰的內存泄漏和流失[memory leaks and memory churn]。它顯示一個應用內存使用量的實時圖表[It shows a realtime graph of your app's memory use],讓您能夠捕獲堆轉儲[capture a heap dump]、強制執行垃圾回收[force garbage collections]以及跟蹤內存分配[track memory allocations]。

要打開 Memory Profiler,請按如下步驟操做:

點擊 View > Tool Windows > Android Profiler(也能夠點擊工具欄中的 Android Profiler )。 從 Android Profiler 工具欄中選擇您想要分析的設備和應用進程。 點擊 MEMORY 時間線中的任意位置可打開 Memory Profiler。

Android 提供一個 託管內存環境(managed memory environment) —當它肯定您的應用再也不使用某些對象時,垃圾回收器會將未使用的內存釋放回堆中。 雖然 Android 查找未使用內存的方式在不斷改進,但對於全部 Android 版本,系統都必須在某個時間點短暫地暫停您的代碼。 大多數狀況下,這些暫停難以察覺。 不過,若是您的應用分配內存的速度比系統回收內存的速度快,則當收集器釋放足夠的內存以知足您的分配須要時,您的應用可能會延遲。 此延遲可能會致使您的應用跳幀[skip frames],並使系統明顯變慢。

儘管您的應用不會表現出變慢,但若是存在內存泄漏,則即便應用在後臺運行也會保留該內存。 此行爲會強制執行沒必要要的垃圾回收 Event,於是拖慢系統的內存性能。 最後,系統被迫終止您的應用進程以回收內存。 而後,當用戶返回您的應用時,它必須徹底重啓。

爲幫助防止這些問題,您應使用 Memory Profiler 執行如下操做:

在時間線[timeline]中查找可能會致使性能問題的不理想的內存分配模式[undesirable memory allocation patterns]。 轉儲 Java 堆以查看在任何給定時間哪些對象耗盡了使用內存。 長時間進行多個堆轉儲可幫助識別內存泄漏。 記錄正經常使用戶交互和極端用戶交互期間的內存分配以準確識別您的代碼在何處短期分配了過多對象,或分配了泄漏的對象[allocating objects that become leaked]。

Memory Profiler 概覽

當您首次打開 Memory Profiler 時,您將看到一條表示應用內存使用量的詳細時間線,並可訪問用於強制執行垃圾回收、捕捉堆轉儲和記錄內存分配的各類工具。

如圖 1 所示,Memory Profiler 的默認視圖包括如下各項:

  1. 用於強制執行垃圾回收 Event 的按鈕。
  2. 用於捕獲堆轉儲的按鈕。
  3. 用於記錄內存分配狀況的按鈕。 此按鈕僅在鏈接至運行 Android 7.1 或更低版本的設備時纔會顯示。
  4. 用於放大/縮小時間線的按鈕。
  5. 用於跳轉至實時內存數據的按鈕。
  6. Event 時間線,其顯示 Activity 狀態、用戶輸入 Event 和屏幕旋轉 Event。
  7. 內存使用量時間線,其包含如下內容:
  • 一個顯示每一個內存類別使用多少內存的堆疊圖表[stacked graph],如左側的 y 軸以及頂部的彩色鍵所示。
  • 虛線表示分配的對象數,如右側的 y 軸所示。
  • 用於表示每一個垃圾回收 Event 的圖標。

不過,若是您使用的是運行 Android 7.1 或更低版本的設備,則默認狀況下,並非全部分析數據都可見。 若是您看到一條消息,其顯示「Advanced profiling is unavailable for the selected process」,則須要 啓用高級分析 以查看下列內容:

  • Event 時間線
  • 分配的對象數
  • 垃圾回收 Event

在 Android 8.0 及更高版本上,始終爲可調試應用啓用高級分析。

如何計算內存

您在 Memory Profiler(圖 2)頂部看到的數字取決於您的應用根據 Android 系統機制所提交的全部私有內存頁面數[private memory pages]。 此計數不包含與系統或其餘應用共享的頁面。

內存計數中的類別以下所示:

  • Java:從 Java 或 Kotlin 代碼分配的對象內存。
  • Native:從 C 或 C++ 代碼分配的對象內存。 即便您的應用中不使用 C++,您也可能會看到此處使用的一些原生內存,由於 Android 框架使用原生內存表明您處理各類任務[handle various tasks on your behalf],如處理圖像資源和其餘圖形時,即便您編寫的代碼採用 Java 或 Kotlin 語言。
  • Graphics:圖形緩衝區隊列向屏幕顯示像素(包括 GL 表面、GL 紋理等等)所使用的內存。 (請注意,這是與 CPU 共享的內存,不是 GPU 專用內存。)
  • Stack: 您的應用中的原生堆棧和 Java 堆棧使用的內存。 這一般與您的應用運行多少線程有關。
  • Code:您的應用用於處理代碼和資源(如 dex 字節碼、已優化或已編譯的 dex 碼、.so 庫和字體)的內存。
  • Other:您的應用使用的系統不肯定如何分類的內存。
  • Allocated:您的應用分配的 Java/Kotlin 對象數。 它沒有計入 C 或 C++ 中分配的對象。 (當鏈接至運行 Android 7.1 及更低版本的設備時,此分配僅在 Memory Profiler 鏈接至您運行的應用時纔開始計數。 所以,您開始分析以前分配的任何對象都不會被計入。 不過,Android 8.0 附帶一個設備內置分析工具,該工具可記錄全部分配,所以,在 Android 8.0 及更高版本上,此數字始終表示您的應用中待處理的 Java 對象總數。)

與之前的 Android Monitor 工具中的內存計數相比,新的 Memory Profiler 以不一樣的方式記錄您的內存,所以,您的內存使用量如今看上去可能會更高些。 Memory Profiler 監控的類別更多,這會增長總的內存使用量,但若是您僅關心 Java 堆內存,則「Java」項的數字應與之前工具中的數值類似。

然而,Java 數字可能與您在 Android Monitor 中看到的數字並不是徹底相同,這是由於應用的 Java 堆是從 Zygote 啓動的,而新數字則計入了爲它分配的全部物理內存頁面。 所以,它能夠準確反映您的應用實際使用了多少物理內存。

注:目前,Memory Profiler 還會顯示應用中的一些誤報的原生內存使用量,而這些內存其實是分析工具使用的。 對於大約 100000 個對象,最多會使報告的內存使用量增長 10MB。 在這些工具的將來版本中,這些數字將從您的數據中過濾掉。

查看內存分配

內存分配顯示內存中每一個對象是_如何_分配的。 具體而言,Memory Profiler 可爲您顯示有關對象分配的如下信息:

  • 分配哪些類型的對象以及它們使用多少空間。
  • 每一個分配的堆疊追蹤[stack trace],包括在哪一個線程中。
  • 對象在什麼時候_被取消分配_(僅當使用運行 Android 8.0 或更高版本的設備時)。

若是您的設備運行 Android 8.0 或更高版本,您能夠隨時按照下述方法查看您的對象分配: 只需點擊並按住時間線,並拖動選擇您想要查看分配的區域(如視頻 1 中所示)。 不須要開始記錄會話,由於 Android 8.0 及更高版本附帶設備內置分析工具,可持續跟蹤您的應用分配。

若是您的設備運行 Android 7.1 或更低版本,則在 Memory Profiler 工具欄中點擊 Record memory allocations 。 記錄時,Android Monitor 將跟蹤您的應用中進行的全部分配。 操做完成後,點擊 Stop recording (同一個按鈕;請參閱視頻 2)以查看分配。

在選擇一個時間線區域後(或當您使用運行 Android 7.1 或更低版本的設備完成記錄會話時),已分配對象的列表將顯示在時間線下方,按類名稱[class name]進行分組,並按其堆計數[heap count]排序。

注:在 Android 7.1 及更低版本上,您最多能夠記錄 65535 個分配。 若是您的記錄會話超出此限值,則記錄中僅保存最新的 65535 個分配。 (在 Android 8.0 及更高版本中,則沒有實際的限制。)

要檢查分配記錄,請按如下步驟操做:

  • 瀏覽列表以查找堆計數異常大且可能存在泄漏的對象。 - 爲幫助查找已知類,點擊 Class Name 列標題以按字母順序排序。 而後點擊一個類名稱。 此時在右側將出現 Instance View 窗格,顯示該類的每一個實例,如圖 3 中所示。
  • 在 Instance View 窗格中,點擊一個實例。 此時下方將出現 Call Stack 標籤,顯示該實例被分配到何處以及哪一個線程中。
  • 在 Call Stack 標籤中,點擊任意行以在編輯器中跳轉到該代碼。

默認狀況下,左側的分配列表按類名稱排列。 在列表頂部,您能夠使用右側的下拉列表在如下排列方式之間進行切換:

  • Arrange by class:基於類名稱對全部分配進行分組。
  • Arrange by package:基於軟件包名稱對全部分配進行分組。
  • Arrange by callstack:將全部分配分組到其對應的調用堆棧[Groups all allocations into their corresponding call stack]。

在分析時提升應用程序性能

爲了在分析時提升應用程序性能,內存分析器默認狀況下會按期對內存分配進行採樣[samples ]。 在運行API級別26或更高級別的設備上進行測試時,能夠使用「Allocation Tracking」下拉列表更改此行爲。

可用選項以下:

  • Full:捕獲內存中的全部對象分配。 這是Android Studio 3.2及更早版本中的默認行爲。 若是您有一個分配了大量對象的應用程序,您可能會在分析時觀察到應用程序的可見速度降低[observe visible slowdowns]。
  • Sampled:按期在內存中採樣對象分配。 這是默認選項,在分析時對應用程序性能的影響較小。 在很短的時間內分配大量對象的應用程序仍然可能會出現明顯的減速。
  • Off:中止跟蹤應用的內存分配。

注意:默認狀況下,Android Studio會在執行CPU錄製時中止跟蹤實時分配,並在CPU錄製完成後從新打開。 您能夠在CPU錄製配置對話框中更改此行爲。

查看全局JNI引用

Java Native Interface(JNI)是一個容許Java代碼和 native code 相互調用的框架。

JNI引用由 native code 手動管理,所以 native code 使用的Java對象可能會保持活動太長時間。若是在沒有先明確刪除[first being explicitly deleted]的狀況下丟棄JNI引用,Java堆上的某些對象可能沒法訪問。此外,可能耗盡[exhaust]全局JNI引用限制。

要解決此類問題,請使用Memory Profiler中的 JNI heap 視圖瀏覽全部全局JNI引用,並按Java類型和本機調用堆棧對其進行過濾。經過此信息,您能夠找到建立和刪除全局JNI引用的時間和位置。

在您的應用程序運行時,選擇要檢查的時間軸的一部分,而後從 class list 上方的下拉菜單中選擇JNI堆。而後,您能夠像往常同樣檢查堆中的對象,而後雙擊 Allocation Call Stack 選項卡中的對象,以查看在代碼中分配和釋放JNI引用的位置,如圖4所示。

要檢查應用程序的JNI代碼的內存分配,您必須將應用程序部署到運行Android 8.0或更高版本的設備。

捕獲堆轉儲

堆轉儲顯示在您捕獲堆轉儲時您的應用中哪些對象正在使用內存。 特別是在長時間的用戶會話後,堆轉儲會顯示您認爲不該再位於內存中卻仍在內存中的對象,從而幫助識別內存泄漏。 在捕獲堆轉儲後,您能夠查看如下信息:

您的應用已分配哪些類型的對象,以及每一個類型分配多少。 每一個對象正在使用多少內存。 在代碼中的何處仍在引用每一個對象。 對象所分配到的調用堆棧。(目前,若是您在記錄分配時捕獲堆轉儲,則只有在 Android 7.1 及更低版本中,堆轉儲才能使用調用堆棧。)

在類列表中,您能夠查看如下信息:

Allocations: 堆中分配數 Native Size: 此對象類型使用的native內存總量。 此列僅適用於Android 7.0及更高版本。您將在這裏看到一些用Java分配內存的對象,由於Android使用native內存來處理某些框架類,例如Bitmap。 Shallow Size: 此對象類型使用的Java內存總量 Retained Size: 所以類的全部實例而保留的內存總大小

您能夠使用已分配對象列表上方的兩個菜單來選擇要檢查的堆轉儲以及如何組織數據。

從左側的菜單中,選擇要檢查的堆:

Default heap:系統未指定堆時。 App heap:您的應用在其中分配內存的主堆[primary heap]。 Image heap:系統啓動映像[system boot image],包含啓動期間預加載[preloaded]的類。 此處的分配保證毫不會移動或消失。 Zygote heap:copy-on-write heap,其中的應用進程是從 Android 系統中派生[forked]的。 從右側菜單中選擇如何排列分配:

Arrange by class:基於類名稱對全部分配進行分組。 Arrange by package:基於軟件包名稱對全部分配進行分組。 Arrange by callstack:將全部分配分組到其對應的調用堆棧。此選項僅在記錄分配[recording allocations]期間捕獲堆轉儲[capture the heap dump]時纔有效。即便如此,堆中的對象也極可能是在您開始記錄以前分配的,所以這些分配會首先顯示,且只按類名稱列出。 默認狀況下,此列表按 Retained Size 列排序。 您能夠點擊任意列標題以更改列表的排序方式。

在 Instance View 中,每一個實例都包含如下信息:

Depth:從任意 GC root 到所選實例的最短 hops 數。 Native Size: native內存中此實例的大小。此列僅適用於Android 7.0及更高版本。 Shallow Size:此實例Java內存的大小。 Retained Size:此實例支配[dominator]的內存大小(根據 [dominator 樹](en.wikipedia.org/wiki/Domina…

要檢查您的堆,請按如下步驟操做:

一、瀏覽列表以查找堆計數[heap counts]異常大且可能存在泄漏的對象。 爲幫助查找已知類,點擊 Class Name 列標題以按字母順序排序。 而後點擊一個類名稱。 此時在右側將出現 Instance View 窗格,顯示該類的每一個實例,如圖 5 中所示。

或者,您能夠經過單擊 Filter 或按 Control + F 並在搜索字段中輸入類名或包名來快速定位對象。 也能夠從下拉菜單中選擇 Arrange by callstack 來按方法名稱搜索。若是要使用正則表達式,請選中Regex旁邊的框。若是您的搜索查詢區分大小寫,請選中匹配大小寫旁邊的框。

二、在 Instance View 窗格中,點擊一個實例。此時下方將出現 References,顯示該對象的每一個引用。或者,點擊實例名稱旁的箭頭以查看其全部字段,而後點擊一個字段名稱查看其全部引用。 若是您要查看某個字段的實例詳情,右鍵點擊該字段並選擇 Go to Instance。

三、在 References 標籤中,若是您發現某個引用可能在泄漏內存,則右鍵點擊它並選擇 Go to Instance。 這將從堆轉儲中選擇對應的實例,顯示您本身的實例數據。

在您的堆轉儲中,請注意由下列任意狀況引發的內存泄漏:

長時間引用 Activity、Context、View、Drawable 和其餘對象,可能會保持對 Activity 或 Context容器的引用。 能夠保持 Activity 實例的非靜態內部類,如 Runnable。 對象保持時間超出所需時間的緩存。

將堆轉儲另存爲 HPROF

在捕獲堆轉儲後,僅當分析器運行時才能在 Memory Profiler 中查看數據。 當您退出分析會話時,您將丟失堆轉儲。 所以,若是您要保存堆轉儲以供往後查看,可經過點擊時間線下方工具欄中的 Export heap dump as HPROF file,將堆轉儲導出到一個 HPROF 文件中。 在顯示的對話框中,確保使用 .hprof 後綴保存文件。

而後,經過將此文件拖到一個空的編輯器窗口(或將其拖到文件標籤欄中),您能夠在 Android Studio 中從新打開該文件。

要使用其餘 HPROF 分析器(如 jhat),您須要將 HPROF 文件從 Android 格式轉換爲 Java SE HPROF 格式。 您能夠使用 android_sdk/platform-tools/ 目錄中提供的 hprof-conv 工具執行此操做。 運行包括如下兩個參數的 hprof-conv 命令:原始 HPROF 文件和轉換後 HPROF 文件的寫入位置。 例如:

hprof-conv heap-original.hprof heap-converted.hprof

導入堆轉儲文件 要導入HPROF(.hprof)文件,請單擊 Sessions 窗格中 Load from file,而後從文件瀏覽器中選擇該文件。

您還能夠經過將 HPROF 文件從文件瀏覽器拖到編輯器窗口中來導入HPROF文件。

分析內存的技巧

使用 Memory Profiler 時,您應對應用代碼施加壓力[stress your app code]並嘗試強制內存泄漏。在應用中引起內存泄漏的一種方式是,先讓其運行一段時間,而後再檢查堆。泄漏在堆中可能逐漸匯聚到分配頂部[Leaks might trickle up to the top of the allocations in the heap]。不過,泄漏越小,您越須要運行更長時間才能看到泄漏。

您還能夠經過如下方式之一觸發內存泄漏:

將設備從縱向旋轉爲橫向,而後在不一樣的 Activity 狀態下反覆操做屢次。 旋轉設備常常會致使應用泄漏 Activity、Context、View 對象,由於系統會從新建立 Activity,而若是您的應用在其餘地方保持對這些對象之一的引用,系統將沒法對其進行垃圾回收。 處於不一樣的 Activity 狀態時,在您的應用與另外一個應用之間切換(導航到主屏幕,而後返回到您的應用)。

MAT

MAT,Memory Analyzer Tool,它能夠幫助咱們查找內存泄漏和查看內存消耗狀況。使用內存分析工具從衆多的對象中進行分析,快速的計算出在內存中對象的佔用大小,看看是誰阻止了垃圾收集器的回收工做,並能夠經過報表直觀的查看到可能形成這種結果的對象。

能夠在應用內使用 android.os.Debug.dumpHprofData(String fileName) 接口主動抓取應用的內存堆棧。只須要在程序運行時調用 dumpHprofData(String fileName) 靜態接口便可將應用內存導出到文件。以下是一段在應用異常退出時抓取內存的示例代碼僅供參考:

public class CaptureHeapDumpsApplication extends Application {
    private static final String FILE_NAME = "/data/local/tmp/heap-dump.hprof";

    @Override
    public void onCreate() {
        super.onCreate();
        Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                String absolutePath = new File(FILE_NAME).getAbsolutePath();
                try {
                    Debug.dumpHprofData(absolutePath);
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        });
    }
}
複製代碼

獲取及打開 .hprof 文件

使用MAT既能夠打開一個已有的堆快照,也能夠經過MAT直接從活動Java程序中導出堆快照。

HPROF文件是MAT能識別的文件,HPROF文件存儲的是特定時間點,java進程的內存快照。有不一樣的格式來存儲這些數據,總的來講包含了快照被觸發時java對象和類在heap中的狀況。因爲快照只是一瞬間的事情,因此heap dump中沒法包含一個對象在什麼時候、何地(哪一個方法中)被分配這樣的信息。

若是HPROF文件是經過AndroidStudio的profile工具導出的,因爲這個不是 mat 工具用到的標準文件,咱們須要使用 sdk 自帶的platform-tools/hprof-conv.exe工具進行轉換,命令爲:

hprof-conv -z 1.hprof 1_mat.hprof

注意:最好將.hprof文件放在一個單獨的文件夾內打開,由於你在操做過程當中,會生成大量的臨時文件。

工具欄

Overview:主界面

Histogram:直方圖

Dominator Tree:支配樹

OQL:Object Query Language studio

Thread OvewView:查看這個應用全部的Thread信息

Run Expert System Test:運行專家系統測試

Query Browser:查詢瀏覽器

Find Object By Address

Group:在Histogram和Domiantor Tree界面,能夠選擇將結果用另外一種Group的方式顯示(默認是Group by Object),切換到Group by package能夠更好地查看具體是哪一個包裏的類佔用內存大,也很容易定位到本身的應用程序。

Calculate Retained Size:點擊後,會出現Retained Size這一列

主界面 Overview

咱們須要關注的是下面Actions區域,介紹4種分析方法:

Histogram: Lists number of instances per class 列出內存中的對象,對象的個數以及大小

Dominator Tree: List the biggest objects and what they keep alive. 列出最大的對象以及其依賴存活的Object,大小是以Retained Heap爲標準排序的

Top Consumers: Print the most expensive objects grouped by class and by package. 經過圖形列出最大的object

Duplicate Classes: Detect classes loaded by multiple class loaders. 經過MAT自動分析泄漏的緣由

default_report 窗口

該窗口列出了可能有問題的代碼片斷。點擊每一個問題中的Details能夠查看相關的詳情。

詳情頁面包含以下內容

Description:問題簡要描述 Shortest Paths To the Accumulation Point:在此列表中,咱們能夠追溯到問題代碼的類樹的結構,並找到本身代碼中的類。 Accumulated Objects in Dominator Tree:在此列表中,咱們能夠看見建立的大量的對象 Accumulated Objects by Class in Dominator Tree:在此列表中,咱們能看見建立大量對象相關的類。 All Accumulated Objects by Class:在此列表中,會按類別劃分的全部累計對象。

兩個重要概念

Shallow heap:自己佔用內存 Shallow size就是對象自己佔用內存的大小,不包含其引用的對象。

常規對象(非數組)的Shallow size由其成員變量的數量和類型決定 數組類型的對象的shallow size由數組元素的類型(對象類型、基本類型)和數組長度決定 注意:由於不像c++的對象自己能夠存放大量內存,java的對象成員都是些引用。真正的內存都在堆上,看起來是一堆原生的byte[]、char[]、int[],因此咱們若是隻看對象自己的內存,那麼數量都很小。因此咱們看到 Histogram 圖是以Shallow size進行排序的,排在第一位的通常都是byte[]。

Retained Heap:引用佔用內存 Retained Heap的概念,它表示若是一個對象被釋放掉,那麼該對象引用的全部對象,包括被遞歸引用的對象,被釋放的內存。

例如,若是一個對象的某個成員new了一大塊int數組,那這個int數組也能夠計算到這個對象中。與shallow heap比較,Retained heap能夠更精確的反映一個對象實際佔用的大小,由於若是該對象釋放,retained heap均可以被釋放。

可是,Retained Heap並不老是那麼有效。 例如,我在A裏new了一塊內存,賦值給A的一個成員變量,同時我讓B也指向這塊內存。此時,由於A和B都引用到這塊內存,因此A釋放時,該內存不會被釋放。因此這塊內存不會被計算到A或者B的Retained Heap中。

爲了糾正這點,MAT中的 Leading Object(例如A或者B)不必定只是一個對象,也能夠是多個對象。此時,(A,B)這個組合的Retained Set就包含那塊大內存了。對應到MAT的UI中,在Histogram中,能夠選擇 Group By class, superclass or package來選擇這個組。

爲了計算Retained Memory,MAT引入了Dominator Tree。

例如,對象A引用B和C,B和C又都引用到D,計算Retained Memory時:

A的包括A自己和B,C,D。 B和C由於共同引用D,因此B,C 的Retained Memory都只是他們自己。 D固然也只是本身。 在這裏例子中,樹根是A,而B,C,D是他的三個兒子,B,C,D再也不有相互關係。

我以爲是爲了加快計算的速度,MAT將對象引用圖轉換成對象引用樹。把引用圖變成引用樹後,計算Retained Heap就會很是方便,顯示也很是方便。對應到 MAT UI 上,在 dominator tree 這個view中,顯示了每一個對象的 shallow heap 和 retained heap。而後能夠以該節點爲樹根,一步步的細化看看 retained heap 究竟是用在什麼地方了。

這種從圖到樹的轉換確實方便了內存分析,但有時候會讓人有些疑惑。原本對象B是對象A的一個成員,但由於B還被C引用,因此B在樹中並不在A下面,而極可能是平級。

爲了糾正這點,MAT中點擊右鍵,能夠 List objects 中選擇 with outgoing references 和 with incoming references。這是個真正的引用圖的概念,

outgoing references :表示該對象的出節點(被該對象引用的對象) incoming references :表示該對象的入節點(引用到該對象的對象) 爲了更好地理解 Retained Heap,下面引用一個例子來講明:

把內存中的對象當作下圖中的節點,而且對象和對象之間互相引用。這裏有一個特殊的節點GC Roots,這就是reference chain(引用鏈)的起點:

上圖中藍色節點表明僅僅只有經過obj1才能直接或間接訪問的對象。由於能夠經過GC Roots訪問,因此上圖的obj3不是藍色節點。所以上圖中obj1的retained size是obj一、obj二、obj4的shallow size總和。

上圖obj1的retained size是obj一、obj二、obj三、obj4的shallow size總和。而obj2的retained size是obj三、obj4的shallow size總和。

Histogram 和 Dominator Tree

Histogram的主要做用是查看一個instance的數量,通常用來查看本身建立的類的實例的個數。

能夠很容易的找出佔用內存最多的幾個對象,根據百分比(Percentage)來排序。

能夠分不一樣維度來查看對象的Dominator Tree視圖,Group by class、Group by class loader、Group by package

Dominator Tree和Histogram的區別是站的角度不同,Histogram是站在類的角度上去看,Dominator Tree是站的對象實例的角度上看,Dominator Tree能夠更方便的看出其引用關係。

經過查看Object的個數,結合代碼就能夠找出存在內存泄露的類(便可達可是無用的對象,或者是能夠重用可是從新建立的對象)

Histogram中還能夠對對象進行Group,更方便查看本身Package中的對象信息。

右鍵菜單:Query Browser

Search Queries:搜索列出全部Queries選項的具體含義,包含搜索區、輸入關鍵字後匹配的Queries選項列表區,點擊選項後的具體含義解釋區。

能夠看到,全部的命令其實就是配置不一樣的SQL查詢語句,好比咱們最經常使用的:

List objects -> with incoming references:查看這個對象持有的外部對象引用 List objects -> with outcoming references:查看這個對象被哪些外部對象引用 Path To GC Roots -> exclude all phantim/weak/soft etc. references:查看這個對象的GC Root,不包含虛、弱引用、軟引用,剩下的就是強引用。從GC上說,除了強引用外,其餘的引用在JVM須要的狀況下是均可以 被GC掉的,若是一個對象始終沒法被GC,就是由於強引用的存在,從而致使在GC的過程當中一直得不到回收,所以就內存溢出了。 Merge Shortest path to GC root:找到從GC根節點到一個對象或一組對象的共同路徑

systrace

systrace 命令能夠容許咱們在系統層面上,對設備裏全部進程的耗時信息進行收集和調研。systrace 結合的分析數據來源於 Android 內核,如 CPU 調度器、磁盤活動和 App 的線程等,會生成一份網頁報告以下所示:

這是一份簡單的 systrace 網頁報告,顯示了和 App 5 秒鐘的交互,報告中高亮的幀是 systrace 認爲沒有合理渲染的地方。

事實上,生成的報告在給定的時間區間,描述了 Android 設備系統進程的全局圖。同時,報告檢查了抓到的 tracing 信息,並高亮化其觀察到的問題,如展現動做或動畫時的 UI 卡頓,會提供一些如何修復的建議。此外,systrace 也有它自身的侷限性,其沒法收集到 App 進程內的代碼執行信息。爲了獲取更多的詳細信息,如 App 運行時正在執行的方法、App 使用的 CPU 資源等,使用 Android Studio 內置的 CPU profiler,或生成的 trace 日誌,而後使用 Traceview 查看。

具體使用

用戶界面調用

打開Android studio,Tools -- Android -- Android Device Monitor,或則直接在sdk中的tools中找到Android Device Monitor打開

在彈出的Android Device Monitor中,左側Devices選項卡的下面一排,點擊下圖中圈出的位置

配置trace信息,點擊OK後,會在對應目錄下生成html文件

命令

爲了生成所須要的網頁報告,須要在命令行使用下面的命令運行 systrace,即進入android-sdk/platform-tools/systrace/後,執行:

python systrace.py [options] [categories]
複製代碼

舉個例子,下面運行 systrace 來記錄 10 秒期間設備的進程,包括圖像進程,而後生成一份命名爲 mynewtrace 的網頁報告,執行:

python systrace.py --time=10 -o mynewtrace.html gfx
複製代碼

若沒指定任何類別或者選項,systrace 會生成一份報告,包括全部可用的種類,同時使用默認的設置。

通用的選項

命令和命令選項

-e <DEVICE_SERIAL> 或 --serial=<DEVICE_SERIAL> 指定設備的序列號

-d 或 --disk 追蹤activity disk的輸入和輸出,須要root設備

-i 或 --cpu-idle 追蹤CPU的idel事件

-l 或 --cpu-load 追蹤CPU的加載

-s或--no-cpu-sched 防止CPU調度的追蹤,經過下降trace buffer的速率達到加長的trace時長的目的

-u 或 --bus-utilization 追蹤bus的使用,須要root設備

-w 或 --workqueue 追蹤work queue,須要root設備

對於上面的--set-tags配置,可選項以下:

gfx 圖形圖像

input 輸入

view 視圖

webview

wm Window Manager

am Activity Manager

sync Synchronization Manager

audio

video

camera

注意:設置tag後,須要重啓framework(’adb shell stop;adb shell start‘)暴躁配置生效。

調研 UI 的性能問題

systrace 尤爲在調研 App 的 UI 性能上表現突出,由於其能夠分析代碼和幀率,來斷定問題出現的區域,並給出可能的建議。大體步驟以下:

連上設備,運行 App

在android-sdk/platform-tools/systrace/路徑下,執行跑 systrace 的命令如:

python systrace.py view --time=10
複製代碼

手動與 App 交互,如滑動等。10 秒後,systrace 會生成一份網頁報告

使用瀏覽器打開生成的網頁報告

能夠點擊報告,來查看記錄期間設備 CPU 的使用。下面給出怎樣在報告中調研信息,來找到並修復 UI 的性能問題。

審查幀和警告

以下圖所示,報告中列出了渲染 UI 幀的每一個進程,顯示了時間線裏每幅渲染的幀。綠色幀圈的代表,在要求的 16.6 毫秒內保持穩定的 60 幀/秒渲染幀像;黃色或紅色幀圈的代表,渲染幀像時花費了超過 16.6 毫秒。

注意,在 Android 5.0 (API level 21) 及更高的設備上,渲染幀像的工做被分隔於 UI 主線程和渲染線程。之前的版本,建立幀像的工做都在 UI 主線程完成。

點擊幀圈能夠高亮化,提供系統完成渲染幀像額外的信息,包括警告。它也展現渲染幀像時系統正在執行的方法,所以能夠根據這些方法來找出 UI 卡頓的緣由。選擇有問題的幀,trace 報告下面會展現問題詳情的一個 alert。

一目瞭然,trace 中會給出相關事件的連接,來解釋在此期間系統運行的詳情。

要看 trace 中工具發現的 alert,以及設備觸發每一個警告的次數,能夠點擊右上邊上的 Alerts tab。Alerts 欄能夠顯示 trace 裏發生的每個問題和它們致使卡頓的頻次。考慮到欄目裏一系列待修復的 bugs,一片區域裏,一個微小的變化或改進,能夠忽略 App 中整個類的警告。

若看到 UI 主線程中作了太多的工做,須要咱們本身找出消耗 CPU 時間的那些方法,方法之一是在認爲致使性能瓶頸的地方,添加 trace 標記,來查看 trace 中出現的調用方法。若不肯定哪些方法或許致使 UI 主線程的瓶頸,使用 Android Studio 內置的 CPU profiler,或生成的 trace 日誌,而後使用 Traceview 查看。

植入你 App 的代碼

因爲 systrace 只在系統層級顯示進程的信息,所以很難經過網頁報告知道給定的時間內,App 究竟執行了哪些方法。在 Android 4.3 (API level 18) 及以上,能夠在代碼中使用 Trace 類來在網頁報告中標記執行的事件。事實上,不須要植入代碼中,用 systrace 記錄 traces,這樣作能幫助咱們定位,代碼的哪一塊也許是形成線程阻塞或 UI 卡頓的緣由。這個方法和 Debug 類不一樣,Trace 類只是簡單地在 systrace 報告中添加標記,而 Debug 類經過生成 .trace 文件幫助咱們審查使用 App 時 CPU 詳細的使用信息。

注意,爲了生成包括 trace 事件的 systrace 網頁報告,須要指定 -a 或 - -app 的命令來運行 systrace,這樣來指定 App 的包名。

下面給出如何使用 Trace 類來標記執行方法的示例,包括兩個嵌套的代碼塊:

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
    ...
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Trace.beginSection("MyAdapter.onCreateViewHolder");
        MyViewHolder myViewHolder;
        try {
            myViewHolder = MyViewHolder.newInstance(parent);
        } finally {
			// try...catch 語句中, 老是要調用 endSection(), 在 finally 語句
          	// 中調用以確保即便拋出異常時 endSection() 也能執行
            Trace.endSection();
        }
        return myViewHolder;
    }

   @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Trace.beginSection("MyAdapter.onBindViewHolder");
        try {
            try {
                Trace.beginSection("MyAdapter.queryDatabase");
                RowItem rowItem = queryDatabase(position);
                mDataset.add(rowItem);
            } finally {
                Trace.endSection();
            }
            holder.bind(mDataset.get(position));
        } finally {
            Trace.endSection();
        }
    }
...
}
複製代碼

注意,當屢次調用 beginSection() 時,調用 endSection() 只終止最近一次調用的 beginSection() 方法。所以,對於嵌套調用,如上面的代碼所示,確保每次調用 beginSection(),一樣恰當地調用到 endSection()。此外,不能在一個線程調用 beginSection(),而後從另外一個線程終止,而應該在相同的線程調用到 endSection() 方法。

瞭解 systrace

在開發應用時,一般使用60fps的幀率來檢測交互是否流暢,若是中途出錯了,或者發生了掉幀,解決這個問題的第一步應當是搞清楚當前系統在作什麼。

systrace工具能夠在程序運行的時候收集實時的信息,記錄時間以及CPU的分配狀況,記錄每一個線程和進程在任意時間的運行狀況,能夠自動分析出一些重要的緣由,而且給出建議。

預覽

Systrace能夠幫助你分析應用是如何設備上運行起來的,它將系統和應用程序線程集中在一個共同的時間軸上,分析systrace的第一步須要在程序運行的時間段中抓取trace log,在抓取到的trace文件中,包含了這段時間中你想要的關鍵信息,交互狀況。

圖1顯示的是當一個app在滑動時出現了卡頓的現象,默認的界面下,橫軸是時間,縱向爲trace event,trace event 先按進程分組,而後再按線程分組.

從上到下的信息分別爲Kernel,SurfaceFlinger,應用包名。經過配置trace的分類,能夠根據配置狀況記錄每一個應用程序的全部線程信息以及trace event的層次結構信息。

systrace 的工做原理

systrace是一個分析android性能問題的基礎工具,但本質上是其餘某些工具的封裝,包括:PC端的atrace,設備端的可執行文件(用於控制用戶控件的追蹤以及配置ftrace,即Linux內核中的主要跟蹤機制)。Systrace使用atrace開啓追蹤,而後讀取ftrace的緩存,而且把它從新轉換成HTML格式。

ftrace相比與systrace和atrace具備更多的功能,而且包含一些對調試性能問題相當重要的高級功能。 (這些功能須要root權限)

運行systrace

./systrace.py sched freq idle am wm gfx view sync binder_driver irq workq input -b 96000
複製代碼

當systrace與GPU和顯示管道活動所需的附加跟蹤點結合使用時,您能夠跟蹤全部從用戶輸入到屏幕顯示的幀。設置大的緩衝區能夠避免事件的丟失(一般表現爲某些CPU在跟蹤中的某個點以後沒有任何事件)。

當使用systrace時,注意每一個事件都是在CPU上觸發的。

注意:硬件中斷不受CPU控制也不會在ftrace中觸發事件,實際提交到跟蹤日誌是由中斷處理程序完成的,但一些損壞的驅動會形成中斷的延遲,所以最關鍵點因素仍是CPU自己。

由於systrace構建在ftrace之上,ftrace在CPU上運行,因此硬件改變log必然會寫入到的ftrace緩衝區。這就意味着若是你好奇爲何顯示欄改變了,那麼你能夠看CPU在該轉換點上的運行內容(CPU上運行的事件均被保存到log中)。這個概念是使用systrace分析性能的基礎。

例子:working frame

這是一個描述正常UI管道過程的systrace,請事先下載好zip文件,點擊下載zip文件,解壓並在瀏覽器中打開systrace_tutorial.html,注意:這個文件要比通常的html文件大得多。

對於一個持續的按期的工做負載,例如TouchLatency,UI管道,一般包含如下階段:

SurfaceFlinger中的EventThread喚醒了應用程序UI線程,代表如今是渲染新幀的時候了。

應用程序使用CPU和GPU資源在UI線程,RenderThread和hwuiTasks中渲染幀。這部分暫UI的大部分。

應用程序經過binder將繪製好的幀發送到SurfaceFlinger並進入睡眠狀態。

SurfaceFlinger中的第二個EventThread喚醒SurfaceFlinger來觸發組合和顯示輸出。若是SurfaceFlinger肯定沒有任何工做要完成,它將返回睡眠狀態。

SurfaceFlinger經過HWC / HWC2或GL處理組合。 HWC / HWC2組合更快,更低的功耗,但會受到SOC的限制。這一步一般須要4-6ms,可是 能夠與步驟2重疊,由於Android應用程序老是三重緩衝。 (雖然應用程序老是三重緩衝,但在SurfaceFlinger中只能有一個待處理幀,所以和雙重緩存差很少。)

SurfaceFlinger經過供應商驅動程序調度最終輸出,並返回睡眠狀態,等待EventThread喚醒。

讓咱們從15409ms開始查看幀:

正常的UI以及EventThread

圖1是正常的幀(對應階段1),要了解UI管道如何工做這是一個很好的示範。 TouchLatency的UI線程行在不一樣時間包含不一樣的顏色。 不一樣的線表明線程的不一樣狀態:

灰色: 睡眠。

藍色: 能夠運行(它能夠運行,但還未被調度運行)。

綠色: 正在運行(調度程序認爲它正在運行)。

注意:中斷處理程序沒有CPU時間軸中顯示,所以在線程運行的過程當中雖然沒有顯示中斷,但實際上你可能已經執行了中斷或者softirqs,最終須要經過檢查trace(進程0)來判斷中斷是否發生。

紅色: 不間斷的睡眠(一般發生在內核鎖上), 指出I / O負載,對於性能問題的調試很是有用。

橙色: 因爲I / O負載致使的不間斷睡眠。 要查看不間斷睡眠的緣由(可從sched_blocked_reason跟蹤點獲取),請選擇紅色不間斷睡眠切片。

當EventThread正在運行時,TouchLatency的UI Thread就變成了能夠運行的藍色。 要查看是什麼,請點擊藍色部分:

TouchLatency的UI線程

圖2(對應階段1) 顯示的是EventThread運行後,TouchLatency的tid6843喚醒UIThread爲其工做,這一個信息能夠從下方的「wakeup from tid:6843」看出

(對應階段2)UI線程被喚醒,開始渲染幀,而後插入SurfaceFlinger的繪製幀隊列

若是'binder_drivier'tag被開啓後,你能夠選擇binder transaction來查看整個過程。

對應階段3) Binder transaction

圖4(對應階段3) Binder transaction 如圖4所示,在15423ms處,SurfaceFlinger的Binder:6832_1因爲pid9579的調用變爲了可運行狀態。在binder tracsaction的兩側你也能夠看到緩衝隊列。 在SurfaceFlinger的queueBuffer中,TouchLtency的待繪製幀數量從1變爲了2.

對應階段3) 待處理幀從1到2

圖5顯示了三重緩衝,其中有兩個完整的幀,應用程序將很快開始渲染第三個幀。 這是由於咱們已經刪除了一些幀,因此應用程序會保留兩個掛起的幀而不是一個幀,以免跳幀。

隨後,SurfaceFlinger的主線程被第二個EventThread喚醒,所以它能夠將的待處理幀輸出到顯示器:

階段4) SurfaceFlinger的主線程由第二個EventThread喚醒

SurfaceFlinger首先鎖定較早的待繪製緩衝區,這將致使掛起的緩衝區數從2減爲1:

(階段4)SurfaceFlinger繪製最先的待繪製緩衝幀

鎖定緩衝區後,SurfaceFlinger進行組裝並顯示新幀。

階段5). SurfaceFlinger進行組裝並提交最終的框架

接下來,'mdss_fb0'在CPU 0上喚醒。'mdss_fb0'是顯示管道的內核線程,用於將渲染的幀輸出到顯示器。 咱們能夠看到’mdss_fb0‘的信息(向下滾動查看)。

(階段6)'mdss_fb0'喚醒CPU0

使用 Systrace

在介紹使用以前,先簡單說明一下Systrace的原理:它的思想很樸素,在系統的一些關鍵鏈路(好比System Service,虛擬機,Binder驅動)插入一些信息(我這裏稱之爲Label),經過Label的開始和結束來肯定某個核心過程的執行時間,而後把這些Label信息收集起來獲得系統關鍵路徑的運行時間信息,進而獲得整個系統的運行性能信息。Android Framework裏面一些重要的模塊都插入了Label信息(Java層的經過android.os.Trace類完成,native層經過ATrace宏完成),用戶App中能夠添加自定義的Label,這樣就組成了一個完成的性能分析系統。

TraceView試圖收集某個階段全部函數的運行信息(sampling的也是基於此思路),它但願在你並不知道哪一個函數有問題的時候直接定位到關鍵函數;但惋惜的是,收集全部信息 這個是不現實的,它的運行時開銷嚴重干擾了運行環境;一我的犯罪了,你要把全國人民都抓起來審問一遍嗎?Systrace的思路是反過來的,在不清楚問題的狀況下,你壓根兒沒法下手,只有掌握了一些基本的信息,經過假設-分析-驗證 的過程一步一步找出問題的緣由;TraceView那種一招吃遍天下鮮的方式,講道理是不符合科學依據的(固然在特定的場合TraceView有他的用途)。

首先,在手機端準備好你須要分析的過程的環境;好比假設你要分析App的冷啓動過程,那就先把App進程殺掉,切換到Launcher中有你的App 圖標的那個頁面,隨時準備點擊圖標啓動進程;假設你要分析某個Activity的卡頓狀況,那就先在手機上進入到上一個Activity,隨時準備點按鈕切換到待分析的Activity中。

由於Systrace沒辦法自由滴控制開始和結束(下面有一個辦法能夠緩解),而trace獲得的數據有可能很是多,所以咱們須要手工縮小須要分析的數據集合;否則你可能被一堆眼花繚亂的數據和圖像弄得暈頭轉向,而後什麼有用的結論也分析不出來。記住哦,手動縮小範圍,會幫助你加速收斂問題的分析過程,進而快速地定位和解決問題。

./systrace.py -t 10 sched gfx view wm am app webview -a <package-name>
複製代碼

這樣,systrace.py 這個腳本就經過adb給手機發送了收集trace的通知;與此同時,切換到手機上進行你須要分析的操做,好比點擊Launcher中App的Icon啓動App,或者進入某個Activity開始滑動ListView/RecyclerView。通過你指定的時間以後(以上是10s),就會有trace數據生成在當前目錄,默認是 trace.html;用Chrome瀏覽器打開便可。

如上文所述,systrace沒有辦法在代碼中控制Trace運行的開始和結束;那麼,若是咱們要分析App的啓動性能,我點了桌面圖標,把Trace時間設置爲10s,我怎麼知道這10s中,哪段時間是我App的啓動過程?若是不知道咱們須要分析的時間段,那後續不是扯淡麼?

咱們能夠用自定義Trace Label解決;Android SDK中提供了android.os.Trace#beginSectionandroid.os.Trace#endSection 這兩個接口;咱們能夠在代碼中插入這些代碼來分析某個特定的過程;好比咱們以爲Fragment的onCreateView過程有問題,那就在onCreateView 中加上代碼:

public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
            Bundle savedInstanceState) {
    Trace.beginSection("Fragement_onCreateView");
    // .. 其餘代碼
    // ...
    // .. 結束處
    Trace.endSection();
}
複製代碼

這樣,在Trace的分析結果中就會帶上Fragement_onCreateView 這個過程的運行時間段信息(固然你得開啓 -a 選項!),以下:

咱們能夠在任意本身感興趣的地方添加自定義的Label;通常來講,分析過程就是,你懷疑哪裏有問題,就在那那個函數加上Label,運行一遍抓一個Trace,看看本身的猜想對不對;若是猜想正確,進一步加Label縮小範圍,定位到具體的自定義函數,函數最終調用到系統內部,那就開啓系統相關模塊的Trace,繼續定位;若是猜想錯誤,那就轉移目標,一步步縮小範圍,直至問題收斂。

回到正題,咱們如何控制Systace的開始和結束?事實上這是無法辦到的,不過控制開始和結束的目的是什麼?其實就是獲得開始和結束這段時間內的Trace信息。要達到這個目的,咱們只須要在指望開始和結束的地方加上自定義的Label就能夠了。好比你要分析App的冷啓動過程,那就在Application類的attachBaseContext調用Trace.beginSection("Boot Procedure"),而後在App首頁的onWindowFocusChanged 或者你認爲別的合適的啓動結束點調用Trace.endSection就能夠到啓動過程的信息;好比下圖是個人Label:

從bindApplication到activityStart,到Phase2,Phase3;這幾個過程組合就是我感興趣的啓動過程。從圖中能夠直觀地看出來,從Application到activityStart佔用了啓動一半的時間,activityStart下面那有一大段空白是在幹什麼?這是個問題。

systrace官方文檔說待trace的App必須是debuggable的,可是官方又說,debuggable的App與非debuggable的性能有較大差異;由於系統爲了支持debug開啓了一些列功能而且關閉掉了某些重要的優化。

若是咱們想要待分析的App儘量接近真實狀況,那麼必需要在非Debug的App中能啓用systrace功能;由於相同狀況下Debug的App性能比非Debuggable的差,你沒法確保在debuggable版本上分析出來的結論能準確推廣到非debuggable的版本上。

分析systrace源碼以後 ,發現這個條件只是個障眼法而已;咱們能夠手動開啓App的自定義Label的Trace功能,方法也很簡單,調用一個函數便可;可是這個函數是SDK @hide的,咱們須要反射調用:

Class<?> trace = Class.forName("android.os.Trace");
Method setAppTracingAllowed = trace.getDeclaredMethod("setAppTracingAllowed", boolean.class);
setAppTracingAllowed.invoke(null, true);
複製代碼

把這段代碼放在Application的attachBaseContext 中,這樣就能夠手動開啓App自定義Label的Trace功能,在非debuggable的版本中也適用!

MAT 使用

內存優化

  1. UI 不可見時釋放資源
  • 在 onStop 中關閉網絡鏈接、註銷廣播接收器、釋放傳感器等資源;
  • 在 onTrimMemory() 回調方法中監聽 TRIM_MEMORY_UI_HIDDEN 級別的信號,此時可在 Activity 中釋放 UI 使用的資源,大符減小應用佔用的內存,從而避免被系統清除出內存。
  1. 內存緊張時釋放資源

運行中的程序,若是內存緊張,會在 onTrimMemory(int level) 回調方法中接收到如下級別的信號:

  • TRIM_MEMORY_RUNNING_MODERATE:系統可用內存較低,正在殺掉 LRU 緩存中的進程。你的進程正在運行,沒有被殺掉的危險。
  • TRIM_MEMORY_RUNNING_LOW:系統可用內存更加緊張,程序雖然暫沒有被殺死的危險,可是應該儘可能釋放一些資源,以提高系統的性能(這也會直接影響你程序的性能)。
  • TRIM_MEMORY_RUNNING_CRITICAL:系統內存極度緊張,而 LRU 緩存中的大部分進程已被殺死,若是仍然沒法得到足夠的資源的話,接下來會清理掉 LRU 中的全部進程,而且開始殺死一些系統一般會保留的進程,好比後臺運行的服務等。關於進程的優先級,參考這裏(developer.android.com/guide/compo…

當程序未在運行,保留在 LRU 緩存中時, onTrimMemory(int level) 中會返回如下級別的信號:

  • TRIM_MEMORY_BACKGROUND:系統可用內存低,而你的程序處在 LRU 的頂端,所以暫時不會被殺死,可是此時應釋放一些程序再次打開時比較容易恢復的 UI 資源。
  • TRIM_MEMORY_MODERATE:系統可用內存低,程序處於 LRU 的中部位置,若是內存狀態得不到緩解,程序會有被殺死的可能。
  • TRIM_MEMORY_COMPLETE:系統可用內存低,你的程序處於 LRU 尾部,若是系統仍然沒法回收足夠的內存資源,你的程序將首先被殺死。此時應釋放無助於恢復程序狀態的全部資源。

注:該 API 在版本 14 中加入。舊版本的 onLowMemory() 方法,大體至關於 onTrimMemory(int level) 中接收到 TRIM_MEMORY_COMPLETE 級別的信號。

另:儘管系統主要按照 LRU 中順序來殺進程,不過系統也會考慮程序佔用的內存多少,那些佔用內存高的進程有更高的可能性會被首先殺死。

  1. 肯定你的程序應該佔用多少內存

能夠經過 getMemoryClass()來獲取你的程序被分配的可用內存,以 M 爲單位。

你能夠經過在 標籤下將 largeHeap 屬性設爲 true 來要求更多的內存,這時經過 getLargeMemoryClass() 方法來獲取可用內存。

大部分應用程序不須要使用此功能,所以使用該標籤前,確認你的程序是否真的須要更多內存。使用更多內存會對整個系統的性能產生影響,並且當程序進入 LRU 時會更容易首先被系統清理掉。

  1. 正確使用 Bipmap,避免浪費內存

若是你的 ImageViwe 的尺寸只有 100 * 100,那麼沒有必要將一張 2560 * 1600 的圖片整個加載入內存。關於如何加載圖片,參考這裏(developer.android.com/topic/perfo…)。

  1. 使用 Android 提供的優化過的數據結構

如 SparseArray, SparseBooleanArray, LongSparseArray 等,相比 Java 提供的 HashMap,這些結構更節省內存。

  1. 始終對內存使用狀況保持關注
  • 枚舉類型 Enum 會比靜態常量佔用更多的內存;
  • Java 中每一個類(包括匿名內部類)都佔用至少 500 字節左右的代碼;
  • 每一個類的實例會在 RAM 中佔用大約 12 ~ 16 字節的內存;
  • 每向 HashMap 中添加一個 Entry 時,新生成的 Entry 佔用大約 32 個字節。
相關文章
相關標籤/搜索