1. 程式人生 > >循序漸進學用MAT排查Android Activity記憶體洩露

循序漸進學用MAT排查Android Activity記憶體洩露

一、先磨刀再砍柴,記憶體洩露相關介紹

  我們先來簡單重溫一下Java GC 的概念:GC即為Garbage Collection,垃圾回收機制。Java GC實質上也就是一個執行在Java虛擬機器(JVM)上的一個程式,它自動地管理著記憶體的使用,在適當的時機釋放並回收無用的記憶體分配。使得我們不用像寫C++那樣手動釋放記憶體,從而幫助我們釋放雙手。那它是如何知道哪些記憶體分配是無用的,而哪些是有用的呢?借用一下2011年Google I/O的一張圖:

GC

  垃圾回收機制設定了一個根節點GC Roots,然後從根節點開始往下進行遍歷,凡是能被直接或間接訪問到的子節點,都認為其是仍然有用的,不應該被回收的,也就是圖中的黃色節點。而無法被遍歷到的藍色節點,即被認為是無用的,應該被回收的記憶體區域。
  那什麼是記憶體洩露呢?簡單來說就是:不應該被GC Roots訪問到到的記憶體,仍能被訪問到,GC Roots誤以為這塊記憶體區域並不是垃圾,導致該回收的記憶體沒被回收。久而久之,記憶體洩露越來越嚴重,舊的垃圾記憶體得不到回收,新的垃圾記憶體不斷增加,可用的記憶體也就越來越少。JVM為了申請新的記憶體空間,頻繁觸發GC,程式執行效率將會受到影響,程式甚至直接丟擲Out Of Memory Exception異常退出。

  下面我們來熟悉一下Android開發中容易出現的記憶體洩漏場景:

1、靜態變數引用Activity

  靜態變數是駐紮在JVM的方法區,因此,靜態變數引用的物件是不會被GC回收的,因為它們所引用的物件本身就是GC ROOT。如果實在要用這樣彆扭的寫法,記得在onDestroy()裡把mActivity置為null。

public class TopicDetailActivity extends AppCompatActivity {
    private static Activity mActivity;

    public static void enterActivity
(Activity activity) { mActivity = activity; Intent intent = new Intent(activity, TopicDetailActivity.class); activity.startActivity(intent); } }

2、非靜態內部類

  非靜態內部類(包括匿名內部類)預設會持有外部類例項,如果內部類的生命週期長於外部類,則可能導致記憶體洩漏。可以考慮改成靜態內部類,或者新建立一個檔案,把內部類挪過去。

public class MainActivity
extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyThread myThread = new MyThread(); myThread.start(); } class MyThread extends Thread { @Override public void run() { while (true) { try { Thread.sleep(65536); } catch (InterruptedException e) { e.printStackTrace(); } } } } }

3、Activity內View不被釋放

  一旦View attach到我們的Window上,就會持有一個Context(即Activity)的引用。如果View無法被釋放,Activity也無法被釋放。

public class MainActivity extends AppCompatActivity {

    private static TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = (TextView) findViewById(R.id.tv);
        tv.setText("Hello world");
    }
}

4、Handler

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {

            }
        };
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        }, Integer.MAX_VALUE);
    }
}

  同第二點,程式碼中new了一個Handler的匿名內部類,預設持有Activity。我們知道,主執行緒的Looper物件不斷從訊息佇列中取出訊息,然後再交給Handler處理。而每個Message物件是持有Handler的引用的(Message物件的target屬性持有Handler引用),從而導致Message間接引用到了Activity。如果在Activty destroy之後,訊息佇列中還有Message物件,Activty是不會被回收的。同樣是,可以把匿名內部類改成靜態內部類,或挪到另一個檔案中去。還可以在onDestroy()時,removeCallbacks或removeCallBacks來清除MessageQueue殘留的Message,已保證Activity不會被Message hold住。

5、監聽器

  在Activity裡註冊BroadcastReceiver或EventBus等忘記反註冊。

二、步入正題,學用MAT

MAT是一種快速,功能豐富的Java堆分析工具,用於分析hprof檔案,幫助你查詢記憶體洩漏和減少記憶體消耗。

1、獲取hprof檔案

  首先,我們開啟Android Studio,除錯好ADB相關,並run起我們的工程,這裡確保一下我們的工程存在記憶體洩漏的Activity(- -因為如果沒有記憶體洩漏存在,那就沒得分析啦,巧婦難為無米之炊啊)。我們不停地在頁面間進行跳轉,最後回到起始的MainActivity。然後,在AS的左下角可以找到一個名為”Android Monitor”的欄目,點開之後再從我們常用的”logcat”切換到”Monitors”下,此時可以看見已連線手機的記憶體、CPU、GPU等使用情況。點選”Memory”項裡紅框選中的按鈕”Dump Java Heap”。

dump

  稍等片刻,我們就會得到一個“不完整”的hprof檔案,AS會自動幫我們開啟這個檔案。(注意,這個hprof還不能用MAT開啟,強行用MAT開啟會報錯!)

before analyze

  點選右邊的Analyzer Tasks,會有意外的驚喜喲~沒錯!AS居然自帶了方便我們排查Activity記憶體洩露的工具。

2、意外的收穫,AS自帶的分析工具

  勾選”Detect Leaked Activities”,然後點選綠色的”>”按鈕,分析結果就出來了。展開分析結果裡的”Leaked Activities”就可以看到哪些Activity存在記憶體洩漏問題。這裡可以看出TopicDetailActivity明顯就洩漏了,因為我dump hprof檔案時,已經回到起始的MainActivity了且Activity棧不存在TopicDetailActivity的例項了,所以記憶體中不應該有那麼多TopicDetailActivity的例項:@601142272,@603487232等。這些例項都應該被GC回收,而沒有被回收!查詢記憶體洩漏是不是So easy啊~。嗯(⊙_⊙),雖然每天用AS都卡得飛起,但是這個分析工具還是值得肯定的,方便,準確!

analyze

  好了,我們現在知道,哪些Activity被洩漏了,但是我們仍然不知道它們為什麼會被洩漏。剛開篇的時候,我們說到,能被GC Roots直接或間接訪問到的例項是不會被回收的。Activity洩漏和其他記憶體洩漏一樣,都是因為被某個長時間不會被銷燬的傢伙給引用了,而這個傢伙能被GC Roots訪問到。接著,我們就來看看是誰hold住了Activity又不肯放手。

reference tree

  選中某個已知洩漏的Activity,可以檢視其Reference Tree。這裡列出了所有引用TopicDetailActivity的物件,一眼望過去,圖中引用TopicDetailActivity例項的物件有View類的,比如TextView,也有非View類,比如ContextImpl。這裡注意一下,AS會把重點懷疑物件用藍色字型標示出來,這裡標出來的是CursorWatcherEditTextView(這裡宣告一下,它是EditText的子類),洩漏的原因八九不離十就是這個自定義View。那又是什麼導致CursorWatcherEditTextView Hold住了Activity呢。這裡再繼續展開Reference Tree也不容易看出個所以然來。要想進一步深入還得使用MAT。
  我們來匯出一個”完整版”的hprof。點選AS左上角的歪脖”Captures”,然後按圖示”Export to standard.hprof”,選好匯出位置再匯出就好了

standard-hprof

3、主角登場,MAT使用簡介

  接下來我們用MAT開啟hprof檔案:

overview

  注意紅框裡圈出的兩個Action,分別是用兩種不同角度檢視記憶體的使用情況。
  Histogram根據以class為基本單位,來列舉每個類各有多少個例項(Objects),Shallow Heap代表這個類的所有例項佔有的記憶體大小。然後也沒啥好看的了,我們再去看看Dominator Tree。

Histogram

  Dominator Tree會從大到小列舉出一些當前記憶體中佔用最大記憶體的物件。Shallow Heap的概念與Histogram裡的一樣。Retained Heap代表如果這個物件被回收,有多少記憶體能被真正釋放,其中包括了Shallow Heap(自身佔有的記憶體) + 只被這個物件Hold住的其他物件。Percentage則表示佔比,這很簡單。

dominator-tree

  剛才我們已經通過AS分析工具,得知TopicDetailActivity這個頁面存在記憶體洩漏,故在搜尋框裡輸入TopicDetailActivity。在以上兩種Tab下,選中某一欄右鍵都能看到一系列的操作。

right-click

  這裡我們只介紹一下”List Object”和”Merge Shortest Paths to GC Roots”,其他的大家自己去摸索哈。
  List Object也有兩個可選的“with outgoing reference”:檢視這個物件它引用了誰,”with incoming reference”:檢視是誰引用了這個物件,又是兩種不同的角度。排查記憶體洩漏,我們肯定是看incoming,因為只有被引用才可能被hold住,才可能記憶體洩漏。放上incoming的圖,大家自行感受一下。

incoming

  由上圖可見,總共有306個例項引用了TopicDetailActivity,一個個排查過去不得爆炸..所以我們化繁為簡,用”Merge Shortest Paths to GC Roots”->”exclude weak/soft references”(排除掉弱引用和軟引用,因為他們不會造成記憶體洩漏):從GC Roots開始找一條能到達Activity的最短的路徑,這往往就是記憶體洩漏的真正原因。

shortest-gc-path

  然後逐級展開到底,我們能看到TopicDetailActivity例項。然後,簡單介紹一下如何看這個圖。我們從下往上看,Activity被CursorWatcherEditText hold住了,而Activity的引用被賦值在CursorWatcherEditText的mContext變數域裡,同理,CursorWatcherEditText被Editor hold住了,CursorWatcherEditText的引用被賦值在Editor的mTextView變數域裡,以此類推。那這個記憶體洩漏到底是為什麼呢?這個問題可能比較複雜,我們慢慢理清邏輯。

  CursorWatcherEditText持有TopicDetailActivity這很正常,因為我們之前說過,一旦View attach到我們的Window上,就會持有其Activity的引用。我們再往上找原因。因為CursorWatcherEditText繼承自EditText,EditText繼承自TextView,通過看TextView.java的原始碼可知,在new 成員變數mEditor的時候,會把TextView例項自身傳給mEditor,然後被儲存在Editor例項的mTextView成員變數域內,這依舊沒啥問題。

textview

Editor

  然後我們再往上看,噫,Editor被他自己的內部類Blink hold住了,找到Blink類,這貨居然不是靜態的內部類,還是個Handler,還實現了Runnable,極有可能就是這個導致的記憶體洩露。

Blink

  我們最後往上看一次- -,一眼看完,Path上端還真的有Message,MessageQueue,就是我們之前說的因為仍有殘留的Message hold住了Handler導致的記憶體洩漏。好了,這條Path總算是摸清了,但是新的問題又出現了,為什麼會有殘留的Message,是不是我們用EditText的姿勢不對或者說是Extends的時候出了什麼問題。雖然不太明白,但是我們進一步閱讀原始碼後發現,Blink與Cursor以及Focus息息相關,並且TextView#setEnabled(boolean enabled)裡有這麼一行,會嘗試呼叫blink.removeCallbacks:
makeBlink,在onDestroy()裡呼叫editReply.setEnabled(false);,記憶體洩漏得到解決

相關推薦

循序漸進MAT排查Android Activity記憶體洩露

一、先磨刀再砍柴,記憶體洩露相關介紹   我們先來簡單重溫一下Java GC 的概念:GC即為Garbage Collection,垃圾回收機制。Java GC實質上也就是一個執行在Java虛擬機器(JVM)上的一個程式,它自動地管理著記憶體的使用,在適當的時

利用Android Studio、MATAndroid進行記憶體洩漏檢測

專案進入維護階段時才有時間測試分析app的記憶體問題,這時就要用到測試工具了,可以使用Android Studio、MAT互相結合進行測試, 但是對於複雜的,這兩者很難分析出來,但這兩測試工具也是必須掌握的,感覺網上大多文章講得不怎麼細緻,所以想寫篇文章記錄下,剛好看到本文

在Eclipse中使用MAT分析Android程式記憶體使用狀況(轉)

對於Android這種手持裝置來說,通常不會帶有太大的記憶體,而且一般使用者都是長時間不重啟手機,所以編寫程式的時候必須要非常小心的使用記憶體,儘量避免有記憶體洩露的問題出現。通常分析程式中潛在記憶體洩露的問題是一件很有難度的工作,一般都是由團隊中的資深工程師負責,而且隨著程式程式碼量的提高,難度還會逐步加大

Android Native記憶體洩露檢測(針對Android7.0)

1. 需要合入一個 Patch 2. 執行指令 adb root adb shell setprop libc.debug.malloc.program cameraserver adb shell setprop libc.debug.malloc.options “backt

Android studio記憶體洩露分析工具使用

什麼是記憶體洩漏 Android虛擬機器的垃圾回收採用的是根搜尋演算法。GC會從根節點(GC Roots)開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是需要回收的垃圾,會被GC回收掉。而記憶體洩漏出現的原因就是存在了無

Android native memory leak detect (Android native記憶體洩露檢測)

簡介 Android應用中,經常會有業務需要使用到Native實現。比如加密,音視訊播放等。也就是常見的二進位制檔案xxx.so 這部分程式碼,申請的記憶體不走Java Heap管理。那麼一旦發生記憶體洩露,無法使用匯出MAT來進行檢視。 本篇文章將講解如

Android應用記憶體洩露分析、改善經驗總結

前言   通過這幾天對好幾個應用的記憶體洩露檢測和改善,效果明顯: 完全退出應用時,手動觸發GC,從原來佔有記憶體100多M降到低於20M; 手動觸發GC後,通過adb shell dumpsys meminfo packagename -d檢視Activity和View的數量也趨近於0了(沒有做到歸零

Android NDK 記憶體洩露檢測

前言最近寫C++程式碼,老是擔心程式碼存在記憶體洩露,膽戰心驚的,Andorid中Java層程式碼記憶體洩露可以藉助leakcanary進行檢測;找了一番,找到了PC上C++上的記憶體洩露檢測庫LeakTracer,於是再找了下,找到了Android上的移植版。首先建立一個專案,在根目錄下建立thirdpar

Android記憶體洩露記憶體溢位、記憶體抖動分析

記憶體 JAVA是在JVM所虛擬出的記憶體環境中執行的,記憶體分為三個區:堆、棧和方法區。 棧(stack):是簡單的資料結構,程式執行時系統自動分配,使用完畢後自動釋放。優點:速度快。 堆(heap):用於存放由new建立的物件和陣列。在堆中分配的記憶體,一方面由jav

TextWatcher引起activity記憶體洩露的問題

TextWatcher會引起activity記憶體洩露。 EditText設定了addTextChangedListener的介面,要在onDestroy裡呼叫removeTextChangedListener釋放掉。

Android應用記憶體洩露分析以及優化方案

文章轉載http://blog.csdn.net/Soiol/article/details/52486871        本篇部落格是介紹Android記憶體優化方面的知識,在讀本篇部落格之前需要你熟練掌握Java 基礎知識(例如,靜態變數的生命週期,匿名內部類的使用,匿名物件等),並且具有一定的Andr

android開發記憶體洩露分析

記憶體洩露分析: 在android開發的專案中,記憶體洩露是影響產品穩定性的最大誘因,目前總結到的記憶體洩露的2大源頭如下: 1.Bitmap記憶體未被釋放 Bitmap記憶體洩露是所有原因中最嚴重的一個,因為Bitmap物件引用的記憶體分JAVA層和C層,而GC機制在一

androidMAT、DDMS 等記憶體檢視工具

三、記憶體監測工具 DDMS --> Heap     無論怎麼小心,想完全避免bad code是不可能的,此時就需要一些工具來幫助我們檢查程式碼中是否存在會造成記憶體洩漏的地方。Android tools中的DDMS就帶有一個很不錯的記憶體監測工具Heap(這裡我使用eclipse的ADT外掛,

使用Android studio 的Analyzer Task分析解決activity記憶體洩漏問題

最近接手的一個老專案,功能比較繁雜,是執行在安卓pos機上面的收銀系統,基本需要應用整天在前臺使用,對系統穩定性要求較高,不同於一般app。 今天做了記憶體方面的檢測 首先我們用電腦連線裝置,用cmd執行adb的記憶體檢測的命令 adb shell dumpsys mem

android java反射修改Activity的元件view的佈局或者屬性

原因:正所謂技術來源於需求,同時推動需求 , 研究的出發點是,PM發現app的某一個view的元件有些問題,view座標或者顏色、字型大小等,需要rd去修改,但是呢這個元件是第三方的sdk中內建,並沒有提供 對應的介面或者方法,怎麼辦? 以前是這樣的: 方法:反射

android 應用記憶體分析MAT結合LeakCanary的分析OOM異常

朋友們在開發應用過程中,可能會碰到OOM異常,通常造成的原理是物件沒有及時釋放,或者載入Bitmap過多過大導致的。 一、匯入LeakCanary 使用的方法也十分的簡單 在Gradle檔案中加入

Kotlin/DSL(Anko),原汁原味Kotlin開發Android---Activity Fragment與AnkoUI分離,強大的複,更加便捷的開發

/寫在前面翻開自己的CSDN,已經很久很久沒有活動了,最近的關於PDF簽章的部落格還是16年寫的。將近年關,工作內容階段性告一段落,終於有時間寫一下自己的東西。廢話少說,說說Kotlin。kotlin開發者給kotlin的定位---不是用來取代任何一種語言,而是 讓開發者有更

Android Studio + MAT 給你看真實專案實戰的記憶體洩露分析

先貼出我要進行實戰的專案背景;專案已經出爐快半年時間了,現在要對它進行效能上的優化,這時候就要使用到 MAT 。然後隨便記錄一下我的分析歷程。 首先要了解兩個概念:記憶體溢位和記憶體洩漏 記憶體溢位

android ion 記憶體洩漏排查

1.檢視各個程序的ION :/sys/kernel/debug/ion/heaps # cat system-heap cat system-heap           client              pid             size ----------

[Android Memory] 記憶體分析工具 MAT 的使用

Dalvik Debug Monitor Server (DDMS) 是 ADT外掛的一部分,其中有兩項功能可用於記憶體檢查 : ·    heap 檢視堆的分配情況 ·    allocation tracker跟蹤記憶體分配情況 DDMS 這兩項功能有助於找到記憶體洩漏的操作行為。 Eclipse M