Android進階——性能優化以內存泄漏和內存抖動的檢測及優化措施總結(七)

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

1、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(虛引用)。android


軟/弱引用技術能夠用來實現高速緩衝器:首先定義一個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();app

        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();ide

        Thread.sleep(2_000);
        //沒有被回收 由於軟引用 在內存不足 回收
        System.out.println("weak:"+weakReference.get());
        System.out.println("weak queue:"+queue.poll());函數

    }
}

對於軟引用和弱引用的選擇,若是隻是想避免OutOfMemory異常的發生,則可使用軟引用。若是對於應用的性能更在乎,想盡快回收一些佔用內存比較大的對象,則可使用弱引用。另外能夠根據對象是否常用來判斷選擇軟引用仍是弱引用。若是該對象可能會常用的,就儘可能用軟引用。若是該對象不被使用的可能性更大些,就能夠用弱引用。另外軟/弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。利用這個隊列能夠得知被回收的軟/弱引用的對象列表,從而爲緩衝器清除已失效的軟/弱引用。工具

2、內存泄漏的檢測
內存泄漏的緣由很不少種,僅僅依靠開發人員的技術經驗沒法準肯定位到形成內存泄漏的罪魁禍首,況且有些內存發生在系統層或者第三方SDK中,幸虧咱們能夠藉助專業的工具來進行檢測,在使用工具檢測前,咱們能夠藉助自動化測試手段或者其餘手段進行初步測試,從前面的文章咱們知道發生內存泄漏的時候,內存是會變大的,好比說在android中咱們執行一段代碼進入了一個新的Activity,這時候咱們的內存使用確定比在前一個頁面大,而在界面finish返回後,若是內存沒有回落,那麼頗有可能就是出現了內存泄漏。post

一、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的極限堆大小,這個就是堆的默認最大值
二、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文件以外必須結合到實際的代碼中去體會。

三、使用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),至此就能夠精肯定位完畢內存泄漏的,結合代碼分析(結合代碼分析也是體力活和技術活,須要耐心和細心)

四、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...
  }
}

3、內存泄漏的常見情形及解決辦法
一、 靜態變量引發的內存泄漏
在java中靜態變量的生命週期是在類加載時開始,類卸載時結束,Static成員做爲GC Roots,若是一個對象被static聲明,這個對象會一直存活直到程序進程中止。即在android中其生命週期是在進程啓動時開始,進程死亡時結束。因此在程序的運行期間,若是進程沒有被殺死,靜態變量就會一直存在,不會被回收掉。那麼靜態變量強引用了某個Activity中變量,那麼這個Activity就一樣也不會被釋放,即使是該Activity執行了onDestroy(不要將執行onDestroy和被回收劃等號)。

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.二、非靜態內部類默認持有外部類實例的強引用引發的內存泄漏
內部類(包含非靜態內部類 和 匿名類) 都會默認持有外部類實例的強引用,所以能夠隨意訪問外部類。但若是這個非靜態內部類實例作了一些耗時的操做或者聲明瞭一個靜態類型的變量,就會形成外圍對象不會被回收,從而致使內存泄漏。一般這類問題的解決思路有:

將內部類變成靜態內部類
若是有強引用Activity中的屬性,則將該屬性的引用方式改成弱引用。
在業務容許的狀況下,及時回收,好比當Activity執行onStop、onDestory時,結束這些耗時任務。
1.2.一、匿名內部線程執行耗時操做引發的內存泄漏
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.二、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(){}

    //一、將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);
    }

}

二、集合類中只執行添加操做,而沒有對應的移除操做
集合類若是僅僅有添加元素的方法,而沒有相應的刪除機制,致使內存被佔用。若是這個集合類是全局性的變量 (好比類中的靜態屬性,全局性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,極可能致使集合所佔用的內存只增不減。好比ButterKnife中的LinkedHashmap就存在這個問題(但實際上是一種妥協,爲了不建立重複的XXActivity$$ViewInjector對象)

三、資源未關閉引發的內存泄漏
當使用了IO資源、BraodcastReceiver、Cursor、Bitmap、自定義屬性attr等資源時,當不須要使用時,須要及時釋放掉,若沒有釋放,則會引發內存泄漏

四、註冊和反註冊沒有成對使用引發的內存泄漏
好比說調用了View.getViewTreeObserver().addOnXXXListener ,而沒有調用View.getViewTreeObserver().removeXXXListener。

五、無限循環動畫沒有及時中止引發的內存泄漏
在Activity中播放屬性動畫中的一類無限循環動畫,沒有在ondestory中中止動畫,Activity會被動畫持有而沒法釋放

六、某些Android 系統自身目前存在的Bug
6.一、輸入法引發的內存泄漏

如上圖所示啓動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();
        }

4、內存抖動及修復措施
從上篇文章Android進階——性能優化以內存管理機制和垃圾回收機制(六)咱們得知在Android5.0以後默認採用ART模式,採用的垃圾收集器是使用標記–清除算法的CMS 收集器,同時這也是產生內存抖動的根本緣由。

一、內存抖動Memory Churn
內存抖動是指在短期內有大量的對象被建立或被回收的現象,致使頻繁GC,而開發時因爲不注意,頻繁在循環裏建立局部對象會致使大量對象在短期內被建立和回收,若是頻繁程度不夠嚴重的話,不會形成內存抖動;若是內存抖動的特別頻繁,會致使短期內產生大量對象,須要大量內存,並且還頻繁回收建立。總之,頻繁GC會致使內存抖動。 

如上圖所示,發生內存抖動時候,表現出的狀況就是上下起伏,相似心電圖同樣(正常的內存表現應該是平坦的)

二、內存抖動的檢測
經過Alloctions Tracker就能夠進行排查內存抖動的問題,在Android Studio中點擊Memory Profiler中的紅點錄製一段時間的內存申請狀況,再點擊結束,而後獲得如下圖片,而後再參照內存泄漏的步驟使用Profiler結合本身的代碼進行分析。 


三、內存抖動的優化
儘可能避免在循環體或者頻繁調用的函數內建立對象,應該把對象建立移到循環體外。總之就是儘可能避免頻繁GC。

小結 性能優化之路,歷來都不是一蹴而就的,準確地來講也沒有任何技巧這兩篇也僅僅是分享了一些常規的步驟,懂得了一些背後的故事,可是在實際開發中須要耐心和細心結合本身的代碼區逐步完成優化工做。  

相關文章
相關標籤/搜索