天天看點

Android 記憶體優化-常見記憶體洩露

作為Android開發人員,我們或多或少都聽說過記憶體洩漏。那麼何為記憶體洩漏,Android中的記憶體洩漏又是什麼樣子的呢,本文将簡單概括的進行一些總結。

關于記憶體洩露的定義,我可以了解成這樣

沒有用的對象無法回收的現象就是記憶體洩露

如果程式發生了記憶體洩露,則會帶來如下的問題

  • 應用可用的記憶體減少,增加了堆記憶體的壓力
  • 降低了應用的性能,比如會觸犯更頻繁的GC
  • 嚴重的時候可能會導緻記憶體溢出錯誤,即OOM Error

在正式介紹記憶體洩露之前,我們有必要介紹一些必要的預備知識。

預備知識1: Java中的對象

  • 當我們使用

    new

    指令生成對象時,堆記憶體将會為此開辟一份空間存放該對象
  • 建立的對象可以被局部變量,執行個體變量和類變量引用。
  • 通常情況下,類變量持有的對象生命周期最長,執行個體變量次之,局部變量最短。
  • 垃圾回收器回收非存活的對象,并釋放對應的記憶體空間。

預備知識2:Java中的GC

  • 和C++不同,對象的釋放不需要手動完成,而是由垃圾回收器自動完成。
  • 垃圾回收器運作在JVM中
  • 通常GC有兩種算法:引用計數和GC根節點周遊

引用計數

  • 每個對象有對應的引用計數器
  • 當一個對象被引用(被複制給變量,傳入方法中),引用計數器加1
  • 當一個對象不被引用(離開變量作用域),引用計數器就會減1
  • 基于這種算法的垃圾回收器效率較高
  • 循環引用的問題引用計數算法的垃圾回收器無法解決。
  • 主流的JVM很少使用基于這種算法的垃圾回收器實作。

GC根節點周遊

  • 識别對象為垃圾從被稱為GC 根節點出發
  • 每一個被周遊的強引用可到達對象,都會被标記為存活
  • 在周遊結束後,沒有被标記為存活的對象都被視為垃圾,需要後續進行回收處理
  • 主流的JVM一般都采用這種算法的垃圾回收器實作
Android 記憶體優化-常見記憶體洩露

以上圖為例,我們可以知道

  • 最下層的兩個節點為GC Roots,即GC Tracing的起點
  • 中間的一層的對象,可以強引用到達GC根節點,是以被标記為存活
  • 最上層的三個對象,無法強引用達到GC根節點,是以無法标記為存活,也就是所謂的垃圾,需要被後續回收掉。

上面的垃圾回收中,我們提到的兩個概念,一個是GC根節點,另一個是強引用

在Java中,可以作為GC 根節點的有

  • 類,由系統類加載器加載的類。這些類從不會被解除安裝,它們可以通過靜态屬性的方式持有對象的引用。注意,一般情況下由自定義的類加載器加載的類不能成為GC Roots
  • 線程,存活的線程
  • Java方法棧中的局部變量或者參數
  • JNI方法棧中的局部變量或者參數
  • JNI全局引用
  • 用做同步監控的對象
  • 被JVM持有的對象,這些對象由于特殊的目的不被GC回收。這些對象可能是系統的類加載器,一些重要的異常處理類,一些為處理異常預留的對象,以及一些正在執行類加載的自定義的類加載器。但是具體有哪些前面提到的對象依賴于具體的JVM實作。

提到強引用,有必要系統說一下Java中的引用類型。Java中的引用類型可以分為一下四種:

  • 強引用: 預設的引用類型,例如

    StringBuffer buffer = new StringBuffer();

    就是buffer變量持有的為StringBuilder的強引用類型。
  • 軟引用:即SoftReference,其指向的對象隻有在記憶體不足的時候進行回收。
  • 弱引用:即WeakReference,其指向的對象在GC執行時會被回收。
  • 虛引用:即PhantomReference,與ReferenceQueue結合,用作記錄該引用指向的對象已被銷毀。

補充了預備知識,我們就需要具體講一講Android中的記憶體洩漏了。

Android中的記憶體洩漏

歸納而言,Android中的記憶體洩漏有以下幾個特點:

  • 相對而言,Android中的記憶體洩漏更加容易出現。
  • 由于Android系統為每個App配置設定的記憶體空間有限,在一個記憶體洩漏嚴重的App中,很容易導緻OOM,即記憶體溢出錯誤。
  • 記憶體洩漏會随着App的推出而消失(即程序結束)。

在Android中的記憶體洩漏場景有很多,按照類型劃分可以歸納為

  • 長期持有(Activity)Context導緻的
  • 忘記登出監聽器或者觀察者
  • 由非靜态内部類導緻的

此外,如果按照洩漏的程度,可以分為

  • 長時間洩漏,即洩漏隻能等待程序退出才消失
  • 短時間洩漏,被洩漏的對象後續會被回收掉。

長時間持有Activity執行個體

在Android中,Activity是我們常用的元件,通常情況下,一個Activity會包含了一些複雜的UI視圖,而視圖中如果含有ImageView,則有可能會使用比較大的Bitmap對象。因而一個Activity持有的記憶體會相對很多,如果造成了Activity的洩漏,勢必造成一大塊記憶體無法回收,發生洩漏。

這裡舉個簡單的例子,說明Activity的記憶體洩漏,比如我們有一個叫做AppSettings的類,它是一個單例模式的應用。

1
2
3
4
5
6
7
8
9
10
11
12
13
      
public class AppSettings {
    private Context mAppContext;
    private static AppSettings sInstance = new AppSettings();

    //some other codes
    public static AppSettings getInstance() {
      return sInstance;
    }

    public final void setup(Context context) {
        mAppContext = context;
    }
}

           

當我們傳入Activity作為Context參數時,則AppSettings執行個體會持有這個Activity的執行個體。

當我們旋轉裝置時,Android系統會銷毀目前的Activity,建立新的Activity來加載合适的布局。如果出現Activity被單例執行個體持有,那麼旋轉過程中的舊Activity無法被銷毀掉。就發生了我們所說的記憶體洩漏。

想要解決這個問題也不難,那就是使用Application的Context對象,因為它和AppSettings執行個體具有相同的生命周期。這裡是通過使用

Context.getApplicationContext()

方法來實作。是以修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
      
public class AppSettings {
    private Context mAppContext;
    private static AppSettings sInstance = new AppSettings();

    //some other codes
    public static AppSettings getInstance() {
      return sInstance;
    }

    public final void setup(Context context) {
        mAppContext = context.getApplicationContext();
    }
}

           

忘記反注冊監聽器

在Android中我們會使用很多listener,observer。這些都是作為觀察者模式的實作。當我們注冊一個listener時,這個listener的執行個體會被主題所引用。如果主題的生命周期要明顯大于listener,那麼就有可能發生記憶體洩漏。

以下面的代碼為例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
      
public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }
}

           

上述代碼處理的業務,可以了解為

  • AppCompatActivity實作了OnNetworkChangedListener接口,用來監聽網絡的可用性變化
  • NetworkManager為單例模式實作,其registerListener接收了MainActivity執行個體

又是單例模式,可知NetworkManager會持有MainActivity的執行個體引用,因而螢幕旋轉時,MainActivity同樣無法被回收,進而造成了記憶體洩漏。

對于這種類型的記憶體洩漏,解決方法是這樣的。即在MainActivity的onDestroy方法中加入反登出的方法調用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
      
public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        NetworkManager.getInstance().unregisterListener(this);
    }

}

           

非靜态内部類導緻的記憶體洩漏

在Java中,非靜态内部類會隐式持有外部類的執行個體引用。想要了解更多,可以參考這篇文章細話Java:”失效”的private修飾符

通常情況下,我們會書寫類似這樣的代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
      
public class SensorListenerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SensorManager sensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
        sensorManager.registerListener(new SensorListener() {
            @Override
            public void onSensorChanged(int sensor, float[] values) {

            }

            @Override
            public void onAccuracyChanged(int sensor, int accuracy) {

            }
        }, SensorManager.SENSOR_ALL);
    }
}

           

其中上面的SensorListner執行個體是一個匿名内部類的執行個體,也是非靜态内部類的一種。是以SensorListner也會持有外部SensorListenerActivity的執行個體引用。

而SensorManager作為單例模式實作,其生命周期與Application相同,和SensorListner對象生命周期不同,有可能間接導緻SensorListenerActivity發生記憶體洩漏。

解決這種問題的方法可以是

  • 使用執行個體變量存儲SensonListener執行個體,在Activity的onDestroy方法進行反注冊。
  • 如果registerListener方法可以修改,可以使用弱引用或者WeakHashMap來解決。

除了上面的三種場景外,Android的記憶體洩漏還有可能出現在以下情況

  • 使用

    Activity.getSystemService()

    使用不當,也會導緻記憶體洩漏。
  • 資源未關閉也會造成記憶體洩漏
  • Handler使用不當也可以造成記憶體洩漏的發生
  • 延遲的任務也可能導緻記憶體洩漏

解決記憶體洩漏

想要解決記憶體洩漏無非如下兩種方法

  • 手動解除不必要的強引用關系
  • 使用弱引用或者軟引用替換強引用關系

下面會簡單介紹一些記憶體洩漏檢測和解決的工具

Strictmode

  • StrictMode,嚴格模式,是Android中的一種檢測VM和線程違例的工具。
  • 使用

    detectAll()

    或者

    detectActivityLeaks()

    可以檢測Activity的記憶體洩漏
  • 使用

    setClassInstanceLimit()

    可以限定類的執行個體個數,可以輔助判斷某些類是否發生了記憶體洩漏
  • 但是StrictMode隻能檢測出現象,并不能提供更多具體的資訊。
  • 了解更多關于StrictMode,請通路Android性能調優利器StrictMode

Android Memory Monitors

Android Memory Monitor内置于Android Studio中,用于展示應用記憶體的使用和釋放情況。它大緻長成這個樣子

Android 記憶體優化-常見記憶體洩露

當你的App占用的記憶體持續增加,而且你同時出發GC,也沒有進行釋放,那麼你的App很有可能發生了記憶體洩漏問題。

LeakCanary

  • LeakCanary是一個檢測Java和Android記憶體洩漏的庫
  • 由Square公司開發
  • 內建LeakCanary之後,隻需要等待記憶體洩漏出現就可以了,無需認為進行主動檢測。
  • 關于如何使用LeakCanary,可以參考這篇文章 Android記憶體洩漏檢測利器:LeakCanary

Heap Dump

  • 一個Heap dump就是某一時間點的記憶體快照
  • 它包含了某個時間點的Java對象和類資訊。
  • 我們可以通上述提到的Android Heap Monitor進行Heap Dump,當然LeakCanary也會生成Heap Dump檔案。
  • 生成的Heap Dump檔案擴充名為.hprof 即Heap Profile.
  • 通常情況下,一個heap profile需要轉換後才能被MAT使用分析。

Shallow Heap VS Retained Heap

  • Shallow Heap 指的是對象自身的占用的記憶體大小。
  • 對象x的Retained Set指的是如果對象x被GC移除,可以釋放總的對象的集合。
  • 對象x的Retained Heap指的就是上述x的Retained Set的占用記憶體大小。
Android 記憶體優化-常見記憶體洩露

以上圖做個例子,進行分析

  • A,B,C,D四個對象的Shallow Heap均為1M
  • B,C,D的Retained Heap均為1M
  • A的Retained Heap為4M

真實情況下如何計算洩漏記憶體大小

上述的Retained Heap的大小擷取是基于假設的,而現實在進行分析中不可能基于這種方法,那麼實際上計算洩漏記憶體的大小的方法其實是這樣的。

這裡我們需要一個概念,就是Dominator Tree(統治者樹)。

  • 如果對象x統治對象y,那麼每條從GC根節點到y對象的路徑都會經過x,即x是GC根節點到y的必經之路。
  • 上述情況下,我們可以說x是y的統治者
  • 最近統治者指的是離對象y最近的統治者。
Android 記憶體優化-常見記憶體洩露

上圖中

  • A和B都不無法統治C對象,即C對象被A和B的父對象統治
  • H不受F,G,D,E統治,但是受C統治
  • F和D是循環引用,但是按照路徑的方向(從根節點到對象),D統治F

記憶體洩漏與OOM

  • OOM全稱Out Of Memory Error 記憶體溢出錯誤
  • OOM發生在,當我們嘗試進行建立對象,但是堆記憶體無法通過GC釋放足夠的空間,堆記憶體也無法在繼續增長,進而完成對象建立請求,是以發生了OOM
  • OOM發生很有可能是記憶體洩漏導緻
  • 但是并非所有的OOM都是由記憶體洩漏引起
  • 記憶體洩漏也并不一定引起OOM

聲明

  • 其中第一張圖檔GC回收圖來自Patrick Dubroy在Google IO的演講Keynote
  • 最後一張Dorminator Tree來自MAT官方網站

繼續閱讀