1. 程式人生 > >Android進階——效能優化之記憶體洩漏和記憶體抖動的檢測及優化措施總結(七)

Android進階——效能優化之記憶體洩漏和記憶體抖動的檢測及優化措施總結(七)

上一篇Android進階——效能優化之記憶體管理機制和垃圾回收機制(六)簡述了Java記憶體管理模型、記憶體分配、記憶體回收的機制的相關知識,相信對於記憶體溢位也有了稍深的瞭解和體會,這一篇將從檢測、解決記憶體洩漏進行總結。

一、Java的引用概述
通過A能呼叫並訪問到B,那就說明A持有B的引用,或A就是B的引用。比如 Object obj = new Object();通過obj能操作Object物件,因此obj是Object的引用;假如obj是類Test中的一個成員變數,因此我們可以使用test.obj的方式來訪問Object類物件的成員Test持有一個Object物件的引用。GC過程與物件的引用型別是密切相關的,Java1.2對引用的分類Strong reference(強引用), SoftReference(軟引用), WeakReference(弱引用), PhatomReference(虛引用)。


軟/弱引用技術可以用來實現高速緩衝器:首先定義一個HashMap,儲存軟引用物件。

private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再來定義一個方法,儲存Bitmap的軟引用到HashMap。

public void static main(String args[]){
       public void addition_isCorrect() throws Exception {
        assertEquals(4, 2 + 2);
        //軟引用
        Object softObj = new Object();
        ReferenceQueue<Object> objectReferenceQueue = new ReferenceQueue<>();
        SoftReference<Object> softReference = new SoftReference<>(softObj,objectReferenceQueue);//通過這個ReferenceQueue可以監聽到GC回收
        //引用佇列
        System.out.println("soft:"+softReference.get());
        System.out.println("soft queue:"+objectReferenceQueue.poll());
        //請求gc
        softObj = null;
        System.gc();

        Thread.sleep(2_000);
        //沒有被回收 因為軟引用 在記憶體不足 回收
        System.out.println("soft:"+softReference.get());
        System.out.println("soft queue:"+objectReferenceQueue.poll());


        Object wakeObj = new Object();
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        WeakReference<Object> weakReference = new WeakReference<>(wakeObj,queue);
        //引用佇列
        System.out.println("weak:"+weakReference.get());
        System.out.println("weak queue:"+queue.poll());
        //請求gc
        wakeObj = null;
        System.gc();

        Thread.sleep(2_000);
        //沒有被回收 因為軟引用 在記憶體不足 回收
        System.out.println("weak:"+weakReference.get());
        System.out.println("weak queue:"+queue.poll());

    }
}

對於軟引用和弱引用的選擇,如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的效能更在意,想盡快回收一些佔用記憶體比較大的物件,則可以使用弱引用。另外可以根據物件是否經常使用來判斷選擇軟引用還是弱引用。如果該物件可能會經常使用的,就儘量用軟引用。如果該物件不被使用的可能性更大些,就可以用弱引用。另外軟/弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。利用這個佇列可以得知被回收的軟/弱引用的物件列表,從而為緩衝器清除已失效的軟/弱引用。

二、記憶體洩漏的檢測
記憶體洩漏的原因很很多種,僅僅依靠開發人員的技術經驗無法準確定位到造成記憶體洩漏的罪魁禍首,何況有些記憶體發生在系統層或者第三方SDK中,幸好我們可以藉助專業的工具來進行檢測,在使用工具檢測前,我們可以藉助自動化測試手段或者其他手段進行初步測試,從前面的文章我們知道發生記憶體洩漏的時候,記憶體是會變大的,比如說在android中我們執行一段程式碼進入了一個新的Activity,這時候我們的記憶體使用肯定比在前一個頁面大,而在介面finish返回後,如果記憶體沒有回落,那麼很有可能就是出現了記憶體洩漏。

1、OOM
通俗來說OOM就是申請的記憶體超過了Heap的最大值,OOM的產生不一定是一次申請的記憶體就超過了最大值,導致OOM的原因基本上都是因為我們的不良程式碼平時”積累”下來的。而Android應用的程序都是從一個叫做Zygote的程序fork出來的,Android 會對每個應用進行記憶體限制(通過ActivityManager例項的getMemoryClass()檢視),也可以檢視/system/build.prop中的對應欄位來檢視App的最大允許申請記憶體。

-dalvik.vm.heapstartsize—— 堆分配的初始大小
-dalvik.vm.heapgrowthlimit —— 正常情況下dvm heap的大小是不會超過dalvik.vm.heapgrowthlimit的值。
-dalvik.vm.heapsize ——manifest中指定android:largeHeap為true的極限堆大小,這個就是堆的預設最大值
2、Android Studio 的Profiler初步定位記憶體洩漏可疑點
Profiler是Android Sutdio內建的一個檢測記憶體洩漏的工具,使用Profiler第一步就是通過“Profiler app”執行APP 
 
然後首先看到如下介面 

點選Memory之後 


強制執行垃圾收集事件的按鈕。
捕獲堆轉儲的按鈕,用於捕獲堆記憶體快照hprof檔案。
記錄記憶體分配的按鈕,點選一次記錄記憶體的建立情況再點選一次停止記錄。
放大時間線的按鈕。
跳轉到實時記憶體資料的按鈕。
事件時間線顯示活動狀態、使用者輸入事件和螢幕旋轉事件。
記憶體使用時間表,其中包括以下內容:

•   每個記憶體類別使用多少記憶體的堆疊圖,如左邊的y軸和頂部的顏色鍵所示。
•   虛線表示已分配物件的數量,如右側y軸所示。
•   每個垃圾收集事件的圖示。

啟動APP之後,我們在執行一些操作之後(一些可以初步判斷記憶體洩漏的操作),然後開始捕獲hprof檔案,首先得先點選請求執行GC按鈕——>點選Dump java heap按鈕 捕獲hprof日誌檔案稍等片刻即可成功捕獲日誌(當然一次dump可能並不能發現記憶體洩漏,可能每次我們dump的結果都不同,那麼就需要多試幾次,然後結合程式碼來排查),然後直接把這個XXX.hprof檔案拖到Android Studio就可解析到如下資訊: 

通過上圖可以得知記憶體中物件的個數(通常大於1就有可能是記憶體洩漏了需要結合自身的情況)、所佔空間大小、引用組佔的記憶體大小等基本資訊,點選具體某個節點,比如說此處點選MainActivity下,選中某個任務然後點選自動分析任務按鈕,還可以得到 

通過Android Profiler可以初步定位到能記憶體洩漏的地方,不過這可能需要重複去測試捕獲hprof檔案,再去分析,不過效能優化永遠不是一蹴而就的事情,也沒有任何墨守成規的步驟,除了藉助hprif檔案之外必須結合到實際的程式碼中去體會。

3、使用Memory Analyzer Tool精確定位記憶體洩漏之處
在Android Studio 的Profiler 上發現為何會記憶體洩漏相對於MAT來說麻煩些,所以MAT更容易精確定位到記憶體洩漏的地方及原因,MAT 是基於Eclipse的一個檢測記憶體洩漏的最專業的工具,也可以單獨下載安裝MAT,在使用MAT之前我們需要把Android Studio捕獲的hprof檔案轉換一下,使用SDK路徑下的platform-tools資料夾下hprof-conv 的工具就可以轉成MAT 需要的格式。

//-z選項是為了排除不屬於app的記憶體,比如Zygote
hprof-conv -z xxxx.hprof xxxx.hprof

執行上面那句簡單的命令之後就可以得到MAT支援的格式,用MAT開啟後 

還可以切換為直方圖顯示形式(這裡會顯示所有物件的資訊),假如說我們知道了可能是MainActivity引起的洩漏,這裡可以直接通過搜尋欄直接過濾(往往這也是在做記憶體洩漏檢測比較難的地方,這需要耐心還有運氣) 

然後想選中的物件上右鍵選擇 


彈出的對話方塊還可以顯示很多資訊,這裡不一一介紹,這裡只使用“Merge Shortest Path GC Roots”這個功能可以顯示出物件的引用鏈(因為發生記憶體洩漏是因為物件還是GC Roots可達,所以需要分析引用鏈),然後可以直接選擇“exclude all phantom/weak/soft ect references ” 排除掉軟弱虛引用,接著就可以看到完整的引用鏈(下層物件被上層引用) 

- shallow heap——指的是某一個物件所佔記憶體大小。 
- retained heap——指的是一個物件與所包含物件所佔記憶體的總大小。 
- out檢視這個物件持有的外部物件引用 
- incoming檢視這個物件被哪些外部物件引用 
在分析引用鏈的時候也需要逐層去結合程式碼排查,這一步也是個體力活,比如說上例就是逐步排查之後定位到的是網易IM 的SDK一個叫做e的物件引用了(其中Xxx$Xx的寫法代表的是Xxx中的一個內部類Xx),至此就可以精確定位完畢記憶體洩漏的,結合程式碼分析(結合程式碼分析也是體力活和技術活,需要耐心和細心)

4、LeakCanary
LeakCanary是Square開源一個檢測記憶體洩漏的框架,使用起來很簡單,只需要兩步:

在build.gradle中引入庫
dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

然後在Application中進行初始化即可,當可能導致記憶體洩漏的時候會自動提示對應的洩漏點
public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

三、記憶體洩漏的常見情形及解決辦法
1、 靜態變數引起的記憶體洩漏
在java中靜態變數的生命週期是在類載入時開始,類解除安裝時結束,Static成員作為GC Roots,如果一個物件被static宣告,這個物件會一直存活直到程式程序停止。即在android中其生命週期是在程序啟動時開始,程序死亡時結束。所以在程式的執行期間,如果程序沒有被殺死,靜態變數就會一直存在,不會被回收掉。那麼靜態變數強引用了某個Activity中變數,那麼這個Activity就同樣也不會被釋放,即便是該Activity執行了onDestroy(不要將執行onDestroy和被回收劃等號)。

1.1、單例模式需要持有上下文的引用的時,傳入短生命週期的上下文物件,引起的Context記憶體洩漏
public class Singleton {
    private Context mContext;
    private volatile  static Singleton mInstance;

    public static Singleton getInstance(Context mContext) {
        if (mInstance == null) {
            synchronized (Singleton.class) {
                if (mInstance == null)
                    mInstance = new Singleton(mContext);
            }
        }
        return mInstance;
    }

    //當呼叫getInstance時,如果傳入的context是Activity的context。只要這個單例沒有被釋放,這個Activity也不會被釋放,就很可能導致記憶體洩漏
    private Singleton(Context mContext) {
        this.mContext = mContext;
    }
}


解決這類問題的思路有二:尋找與該靜態變數生命週期差不多的替代物件和將強引用方式改成弱(軟)引用

public class Singleton {
    private Context mContext;
    private volatile  static Singleton mInstance;

    public static Singleton getInstance(Context mContext) {
        if (mInstance == null) {
            synchronized (Singleton.class) {
                if (mInstance == null)
                    mInstance = new Singleton(mContext.getApplicationContext());//將傳入的mContext轉換成Application的context   
            }
        }
        return mInstance;
    }

    //當呼叫getInstance時,如果傳入的context是Activity的context。只要這個單例沒有被釋放,這個Activity也不會被釋放。
    private Singleton(Context mContext) {
        this.mContext = mContext;
    }
}

Application 的 context 不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的Context的應用場景如下 


1.2、非靜態內部類預設持有外部類例項的強引用引起的記憶體洩漏
內部類(包含非靜態內部類 和 匿名類) 都會預設持有外部類例項的強引用,因此可以隨意訪問外部類。但如果這個非靜態內部類例項做了一些耗時的操作或者聲明瞭一個靜態型別的變數,就會造成外圍物件不會被回收,從而導致記憶體洩漏。通常這類問題的解決思路有:

將內部類變成靜態內部類
如果有強引用Activity中的屬性,則將該屬性的引用方式改為弱引用。
在業務允許的情況下,及時回收,比如當Activity執行onStop、onDestory時,結束這些耗時任務。
1.2.1、匿名內部執行緒執行耗時操作引起的記憶體洩漏
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        test();
    }
    public void test() {
        //匿名內部類會引用其外圍例項MainActivity.this,所以會導致記憶體洩漏    
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1_000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

改為靜態內部類即可

public static void test() {
        //靜態內部類不會持有外部類例項的引用
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1_000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

1.2.2、Handler引起的記憶體洩漏
mHandler 為匿名內部類例項,會引用外圍物件MainActivity .this,若該Handler在Activity退出時依然還有訊息需要處理,那麼這個Activity就不會被回收,尤其是延遲處理時mHandler.postDelayed更甚。

public class MainActivity extends Activity {

    private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            ...
        };
    };
    ...
}

針對Handler引起的記憶體洩漏,可以把Handler改為靜態內部類,對於外部Activity的引用改為弱引用方式,並且在相關生命週期方法中及時移除掉未處理的Message和回撥

public class MainActivity extends Activity {
    private void doOnHandleMessage(){}

    //1、將Handler改成靜態內部類。   
    private static class MyHandler extends Handler {
        //2將需要引用Activity的地方,改成弱引用。     
        private WeakReference<MainActivity> mInstance;

        public MyHandler(MainActivity activity) {
            this.mInstance = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity activity = mInstance == null ? null : mInstance.get();
            //如果Activity被釋放回收了,則不處理這些訊息       
            if (activity == null || activity.isFinishing()) {
                return;
            }
            activity.doOnHandleMessage();
        }
    }

    @Override
    protected void onDestroy() {
        //3在Activity退出的時候移除回撥     
        super.onDestroy();
        handler.removeCallbacksAndMessages(null);
    }

}

2、集合類中只執行新增操作,而沒有對應的移除操作
集合類如果僅僅有新增元素的方法,而沒有相應的刪除機制,導致記憶體被佔用。如果這個集合類是全域性性的變數 (比如類中的靜態屬性,全域性性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,很可能導致集合所佔用的記憶體只增不減。比如ButterKnife中的LinkedHashmap就存在這個問題(但其實是一種妥協,為了避免建立重複的XXActivity$$ViewInjector物件)

3、資源未關閉引起的記憶體洩漏
當使用了IO資源、BraodcastReceiver、Cursor、Bitmap、自定義屬性attr等資源時,當不需要使用時,需要及時釋放掉,若沒有釋放,則會引起記憶體洩漏

4、註冊和反註冊沒有成對使用引起的記憶體洩漏
比如說呼叫了View.getViewTreeObserver().addOnXXXListener ,而沒有呼叫View.getViewTreeObserver().removeXXXListener。

5、無限迴圈動畫沒有及時停止引起的記憶體洩漏
在Activity中播放屬性動畫中的一類無限迴圈動畫,沒有在ondestory中停止動畫,Activity會被動畫持有而無法釋放

6、某些Android 系統自身目前存在的Bug
6.1、輸入法引起的記憶體洩漏

如上圖所示啟動Activity的時候InputMethodManager中的DecorView型別的變數mCurRootView/mServedView/mNextServedView會自動持有相應Activity例項的強引用,而InputMethodManager可以作為GC Root就有可能導致Activity沒有被及時回收導致記憶體洩漏。 
要處理這類問題,唯一的思路就是破壞其引用鏈即把對應的物件置為null即可,又由於不能直接訪問到,只能通過反射來置為null。

InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        try {
            Field mCurRootViewField = InputMethodManager.class.getDeclaredField("mCurRootView");
            mCurRootViewField.setAccessible(true);
            Object mCurRootView = mCurRootViewField.get(im);
            if (null != mCurRootView){
                Context context = ((View) mCurRootView).getContext();
                if (context == this){
                    //置為null
                    mCurRootViewField.set(im,null);
                }
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

四、記憶體抖動及修復措施
從上篇文章Android進階——效能優化之記憶體管理機制和垃圾回收機制(六)我們得知在Android5.0之後預設採用ART模式,採用的垃圾收集器是使用標記–清除演算法的CMS 收集器,同時這也是產生記憶體抖動的根本原因。

1、記憶體抖動Memory Churn
記憶體抖動是指在短時間內有大量的物件被建立或被回收的現象,導致頻繁GC,而開發時由於不注意,頻繁在迴圈裡建立區域性物件會導致大量物件在短時間內被建立和回收,如果頻繁程度不夠嚴重的話,不會造成記憶體抖動;如果記憶體抖動的特別頻繁,會導致短時間內產生大量物件,需要大量記憶體,而且還頻繁回收建立。總之,頻繁GC會導致記憶體抖動。 

如上圖所示,發生記憶體抖動時候,表現出的情況就是上下起伏,類似心電圖一樣(正常的記憶體表現應該是平坦的)

2、記憶體抖動的檢測
通過Alloctions Tracker就可以進行排查記憶體抖動的問題,在Android Studio中點選Memory Profiler中的紅點錄製一段時間的記憶體申請情況,再點選結束,然後得到以下圖片,然後再參照記憶體洩漏的步驟使用Profiler結合自己的程式碼進行分析。 


3、記憶體抖動的優化
儘量避免在迴圈體或者頻繁呼叫的函式內建立物件,應該把物件建立移到迴圈體外。總之就是儘量避免頻繁GC。

小結
效能優化之路,從來都不是一蹴而就的,準確地來說也沒有任何技巧這兩篇也僅僅是分享了一些常規的步驟,懂得了一些背後的故事,但是在實際開發中需要耐心和細心結合自己的程式碼區逐步完成優