Android學習筆記19-內存泄漏分析

今天來簡單的介紹下怎麼分析android應用的內存泄漏問題。

首先我們要明白爲什麼會有內存泄漏,主要有2種情況:
a.全局進程(process-global)的static變量。這個無視應用的狀態,持有Activity的強引用的怪物。
b.活在Activity生命週期之外的線程。沒有清空對Activity的強引用。

1、瞭解下4種級別對象的引用:

從JDK 1.2版本開始,把對象的引用分爲4種級別,從而使程序能更加靈活地控制對象的生命週期。這4種級別由高到低依次爲:強引用、軟引用、弱引用和虛引用。
    強引用(Strong reference):
        常見形式如:A a = new A();等
    軟引用(Soft Reference):
        A a = new A();
        SoftReference<A> srA = new SoftReference<A>(a);
        軟引用所指示的對象進行垃圾回收需要滿足如下兩個條件:
        1.當其指示的對象沒有任何強引用對象指向它;
        2.當虛擬機內存不足時。
    弱引用(Weak Reference):
        A a = new A();
        WeakReference<A> wrA = new WeakReference<A>(a);
        WeakReference不改變原有強引用對象的垃圾回收時機,一旦其指示對象沒有任何強引用對象時,此對象即進入正常的垃圾回收流程。
    虛引用(Phantom Reference):

2、瞭解下內存的知識:

JAVA是在JVM所虛擬出的內存環境中運行的,JVM的內存可分爲三個區:堆(heap)、棧(stack)和方法區(method)。
    棧(stack):棧最顯著的特徵是:LIFO(Last In, First Out, 後進先出),棧中只存放基本類型和對象的引用(不是對象)。
    堆(heap):堆內存用於存放由new創建的對象和數組。
        在堆中分配的內存,由java虛擬機自動垃圾回收器來管理。JVM只有一個堆區(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身。
    方法區(method):又叫靜態區,跟堆一樣,被所有的線程共享。方法區包含所有的class和static變量。

查看設備內存的方法:
    a.查看meminfo文件
        cat /proc/meminfo
        cat /proc/meminfo |grep Mem
    b.更詳細的信息可以使用dumpsys命令
        dumpsys meminfo  查看總的內存使用
        dumpsys meminfo com.example.testleakmemory  查看單個應用的內存使用情況
        dumpsys meminfo 27606  通過pid查看單個應用的內存使用情況

3、工具準備:

工欲善其事,必先利器。我們要使用的是DDMS+MAT(Memory Analysis Tool)來分析。
DDMS是ADT自帶的調試工具。
MAT的下載網址:http://www.eclipse.org/mat/downloads.php

這裏寫圖片描述
我自己在線安裝一直都不成功,各種錯誤。後面直接在下載(Stand-alone Eclipse RCP Applications)版本。
開始的時候下載了64位的版本,發現運行exe文件的時候老是報 “failed to load the jni shared jvm.dll” 這個錯誤。
後面又下載32位了發現可以運行。查看本地的java版本,發現是32位的(我電腦是64位的),這就可以解釋通。
D:\android-sdk-windows-4.3\platform-tools>java -version
java version “1.7.0_51”
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) Client VM (build 24.51-b03, mixed mode, sharing)

4、構造一個有內存泄漏的代碼:

    MainActivity.java:
        public class MainActivity extends Activity {
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
            }
            public void eventClick(View v) {//點擊跳轉到第二個頁面
                Intent intent = new Intent(this, Second.class);
                startActivity(intent);
            }
        }

    Second.java:
        public class Second extends Activity {
            private List<String> list = new ArrayList<String>();//全局的list
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_second);
                // 模擬Activity一些其他的對象
                for (int i = 0; i < 200000; i++) {
                    list.add(new String("hello world!"));
                }
                new MyThread().start();// 開啓線程
            }

            public class MyThread extends Thread {
                @Override
                public void run() {
                    super.run();
                    try {// 模擬耗時操作,線程開啓10分鐘
                        Thread.sleep(1000*60*10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }   

5、獲取hprof文件:

mat分析需要導出hprof文件。可以先導出泄漏之前的hprof文件,在操作app,導出泄漏之後的hprof。

這裏寫圖片描述

操作之後的內存圖:
這裏寫圖片描述

dump hprof文件
這裏寫圖片描述

6、轉化hprof文件:

cmd到當前的目錄下,把你需要轉化的hprof文件也可以拷貝到當前的目錄
hprof-conv 1.hprof before.hprof

這裏寫圖片描述

7、使用mat打開文件開始分析-直方圖:

這個時候我們獲取了操作之前的before.hprof文件 和 操作之後的 after.hprof文件。
打開mat工具,導入(Open Heap Dump ...)hprof開始分析。

主界面
這裏寫圖片描述

我們分析最常用到的就是Histogram(直方圖)和 Dorminator Tree(支配樹)
a.打開Histogram(直方圖)他列舉了每個對象的統計。它可以列出任意一個類的實例數。
    比如你需要查看MainActivity,可以使用正則表達式 .*Main.*  (注意大小寫)
    可以列出與MainActivity相關的類

這裏寫圖片描述

b.選中com.example.testleakmemory.Second,右擊,選擇“Merge Shortest Paths to GC Roots”,
    再選擇選擇“exclude all phantom/weak/soft etc.references”
    (排查虛引用/弱引用/軟引用等)因爲被虛引用/弱引用/軟引用的對象可以直接被GC給回收.
    在JAVA中是通過可達性(Reachability Analysis)來判斷對象是否存活,這個算法的基本思想是通過一系列的稱謂"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索所走得路徑稱爲引用鏈

這裏寫圖片描述

c.如果存在GC Roots鏈,即存在內存泄露問題。
    除了使用Merge Shortest Paths to GC Roots 我們還可以使用
    List object - With outgoing References   顯示選中對象持有那些對象
    List object - With incoming References  顯示選中對象被那些外部對象所持有
    Show object by class - With outgoing References  顯示選中對象持有哪些對象, 這些對象按類合併在一起排序
    Show object by class - With incoming References  顯示選中對象被哪些外部對象持有, 這些對象按類合併在一起排序

這裏寫圖片描述

8、使用mat打開文件開始分析-Dorminator Tree支配樹):

 a.通過 Dorminator Tree(支配樹),可以直觀地反映一個對象的retained heap,根據retained heap進行排序.
    shallow heap:指的是某一個對象所佔內存大小。
    retained heap:指的是一個對象的retained set所包含對象所佔內存的總大小。
    它主要可以用於診斷一個對象所佔內存爲什麼會不斷膨脹,一個對象膨脹,就說明它對應到支配樹中的子樹就越來越龐大。

這裏寫圖片描述

 b.通過dominator_tree查看單一對象所持有的對象
    右擊對象--Java Basics -- Open In Dominator Tree
    很實用 

 注意:MAT工具上顯示的size的大小單位是: Bytes 。例如 21414072 = 20.4M

這裏寫圖片描述

 在MAT中可以查看到有類似如下的顯示
    com.example.main.MainActivity$1
    com.example.main.MainActivity$2
    這個 $1 表示第一個匿名類的大小。
    $2、$3這樣排下去。

9、使用的小技巧1:

a.操作前後兩個hprof的對比:
    dump出前後2個hprof文件
    使用mat工具打開,在Navigation History這個視圖中,右擊histogram--Add to Compare Basket
    Window -- Compare Basket,裏面會有2個histogram。點擊對比就可以了。
    上面這個對比結果不利於查找差異,還可以調整對比選項
    Difference from base Table

這裏寫圖片描述

10、使用的小技巧2:

我們使用對比發現了操作後的內存中多了很多 char[] (我們構造的20萬個String未釋放)。
在after.hprof的直方圖視圖中
    a.可以右擊 -- Immediate dominators(查看引用者) -- 可以發現Second這個activity持有20萬個,基本可以判斷Second這個頁面有內存泄露。
        我們再查看那個變量持有了這麼多。

這裏寫圖片描述

    b.右擊這個Second -- 選擇 Dominated Objects(注意一定是這個不要選擇Objects裏面的) -- Merge Shortest Paths to GC Roots -- 
        exclude all phantom/weak/soft etc.references

這裏寫圖片描述

        c.一步步看誰持有了這個Second不釋放

這裏寫圖片描述

    備註:把這個問題解決了之後,再分析發現還有個輸入法問題。
        android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper 
            .
            .
            list
        可以確定是輸入法持有了activity的引用,導致Second activity裏面list這個變量未正常釋放。
    (輸入法是單例的,只會持有一個Second的,所以不管你打開多少次這個頁面,泄露的大小是固定的。網上查閱這個是系統的一個bug,
        如果頁面佔用內存不是太多,主要是全局變量,那麼泄露的就可以接受)

11、實際中遇到的內存泄漏問題:

在實際的應用中遇到了以下一些的內存泄露問題:
a.將一些View定義爲static,例如窗口顯示:我在其它的類裏面直接調用靜態的方法來設置靜態的View上面的文本。
    因爲static的生命週期很長,導致這個View持有activity的引用不釋放,導致activity銷燬了,資源還得不到釋放(如果有圖片資源泄露的更多)。
    解決的方法:
    將View不要定義爲static,Handler類定義爲靜態的,使用弱引用來持有activity的引用。

b.在工具類Information類中,定義了一個靜態的WiFiManager變量wm
    在使用的時候,直接將context傳到這個工具類中,wm使用到了這個context獲取服務,activity銷燬了,wm不會銷燬,導致activity資源不釋放,內存泄露。
    解決的方法:
    在工具類中不用傳遞進來的context,直接獲取全局的Context
    或者context.getApplicationContext();來獲取

c.將context傳進了測試類中,類裏面又將context定義爲static,導致activity銷燬內存不釋放。(和上面的情況類似)
    解決方式:
    測試完成,將靜態的context設置爲null,斷掉GC Root鏈路

d.使用第三方的jar包,顯示gif圖像導致的內存泄露:
    我們啓動了gif,但是jar提供的停止的方法中不能停止它裏面的子線程。
    使用mat分析到時jar包裏面的子線程引用了activity,這樣子線程不結束,activity就釋放不了
    解決方法:
    我們沒有jar的源碼,通過查看class顯示的私有屬性有個isRunning,但是沒有提供方法設置。我通過反射去設置了這個變量的屬性。問題解決,可以正常釋放了。
    if(null != gv_toolbox_distribute_gif){
        try {
            Class<? extends GifView> clazz = gv_toolbox_distribute_gif.getClass();
            Field f = clazz.getDeclaredField("isRun");
            f.setAccessible(true);
            f.set(gv_toolbox_distribute_gif, false); 
        } catch (Exception e) {
            LogUtils.d(TAG, "失敗:" + e.getMessage());
        }
    }

e.自定義一個WaveCircle動畫水波紋,裏面使用了postDelay方法。導致內存泄露(網上有比較多的這個分析案例)
    通過mat工具分析發現,有個ThreadLocal老是持有activity,繼續往下看,在getRunQueue裏面有2個Runnable未執行。
    後面解決方法:
    1.在調用stop動畫的時候睡500ms,測試有效。
    2.我使用發消息的方法stop動畫,測試也有效(暫未研究源碼,也許是消息執行需要等待一段時間)

f.有個等待的狀態Dialog沒有正常關閉
    通過mat分析到時有個WindowManger持有了activity的引用不釋放。後面查代碼發現dialog根本就沒有關閉。
    解決方法:
    在dialog顯示完成結束的時候,需要手動dismiss掉這個dialog,實際測試OK

g.使用了幀動畫,沒有正常結束
    通過mat分析到有很大的Bitmap未釋放,把原圖導出來看下,發現是幀動畫的原圖,檢查代碼發現,原來開啓了動畫,當時沒有結束動畫。
    解決方法:
    在動畫顯示完成的時候stop掉就OK了。

**總結:
以上只是分析了一個簡單的例子,基本的流程就是這樣,以後遇到了複雜的問題,按照這個思路來分析也能逐步的找到問題的所在。
平時我們寫代碼的時候要多注意這些性能的問題。能避免的儘量避免,不然遇到了泄漏的問題後面來分析花費的代價會更大。
好的習慣很重要!!!**

發佈了108 篇原創文章 · 獲贊 39 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章