Android 記憶體洩漏

孟芳芳發表於2020-11-19

記憶體洩漏(Memory Leak)是指程式中已動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰(OOM)等嚴重後果。

一、記憶體洩漏檢查工具LeakCanary

LeakCanary是Square公司為Android開發者提供的一個自動檢測記憶體洩漏的工具,LeakCanary本質上是一個基於MAT進行Android應用程式記憶體洩漏自動化檢測的的開源工具,我們可以通過整合LeakCanary提供的jar包到自己的工程中,一旦檢測到記憶體洩漏,LeakCanary就好dump Memory資訊,並通過另一個程式分析記憶體洩漏的資訊並展示出來,隨時發現和定位記憶體洩漏問題,而不用每次在開發流程中都抽出專人來進行記憶體洩漏問題檢測,極大地方便了Android應用程式的開發。

LeakCanary顯示記憶體洩漏的頁面:
在這裡插入圖片描述

LeakCanary的Android Studio整合

一、 在build.gradle中新增LeakCanary的依賴包,截止目前leakcanary的最新版本是1.6.1:

dependencies {
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.6.1'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
}

在開發中一般同時整合debug和release版本,其中:

  • com.squareup.leakcanary:leakcanary-android:1.6.1 是debug版本,在你的app編譯的是debug版本,載入的是該jar包,一旦出現記憶體洩漏會在通知欄中通知開發者產生了記憶體洩漏;

  • com.squareup.leakcanary:leakcanary-android-no-op:1.6.1 是release版本,如果你的app編譯的是release版本時,載入的是該jar包,no-op是指No Operation Performed,代表不會做任何操作,不會干擾正式使用者的使用;

二、 在我們自定義Application的onCreate方法中註冊LeakCanary

@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);
}
  • 解釋一下為什麼要先判斷LeakCanary.isInAnalyzerProcess(this)

         在註冊之前先判斷LeakCanary是否已經執行在手機上,比如你同時有多個APP整合了LeakCanary,其他app已經執行了LeakCanary則不需要重新install。

  • 正常情況下Application呼叫LeakCanary.install(this)後就可以正常監聽該app程式的記憶體洩漏;

LeakCanary監聽指定物件的記憶體洩漏

如果想讓LeakCanary監聽指定物件的記憶體洩漏,我們就需要使用到RefWatcherwatch功能,使用方式如下:

  • ApplicationonCreate中呼叫install方法,並獲取RefWatcher物件:
private static RefWatcher sRefWatcher;

@Override
public void onCreate() {
    super.onCreate();
    sRefWatcher = LeakCanary.install(this);
}

public static RefWatcher getRefWatcher() {
    return sRefWatcher;
}

注意:因為這時候需要獲取sRefWatcher物件,所以sRefWatcher = LeakCanary.install(this)一定需要執行,不需要判斷LeakCanary.isInAnalyzerProcess(this)。

 

為了方便演示使用LeakCanary獲取和解決記憶體洩漏的問題,我們先寫一個記憶體洩漏的場景,我們知道最常見的記憶體洩漏是單列模式使用ActivityContext場景,所以我們也用單列模式來演示:

public class Singleton {
    private static Singleton singleton;
    private Context context;
    private Singleton(Context context) {
        this.context = context;
    }

    public static Singleton newInstance(Context context) {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null){//雙重檢查鎖定
                    singleton = new Singleton(context);
                }
            }
        }
        return singleton;
    }
}

在需要監聽的物件中呼叫RefWatcherwatch方法進行監聽,比如我想監聽一個Activity,我們可以在該AcitivityonCreate方法中新增DemoApp.getRefWatcher().watch(this);

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    DemoApp.getRefWatcher().watch(this);
    setContentView(R.layout.activity_second);
    Singleton singleton = Singleton.newInstance(this);
}

LeakCanary記憶體洩漏展示頁面

同時上面的步驟,當我們在執行app程式的時候,出現記憶體洩漏後,過一小段時間後就會在通知欄中通知出現記憶體洩漏的情況:

同時會在桌面上生成一個Leaks的圖示,這個就是展示記憶體洩漏列表的,記憶體洩漏列表頁面如下:
在這裡插入圖片描述
這是一個記憶體洩漏的列表,我們可以通過點選進入檢視洩漏的內容
在這裡插入圖片描述
還可以通過點選右邊的“+”號檢視更詳細的資訊,內容太長就不截圖了,內部有詳細介紹呼叫的流程;

二、Android發生記憶體洩漏的常見情況

1、靜態變數

靜態變數的生命週期和應用的生命週期一樣長。如果靜態變數持有某個Activity的context,則會引發對應Activity無法釋放,導致記憶體洩漏。如果持有application的context,就沒有問題。

常見的有:

  • 單例模式:內部實現是靜態變數和方法
  • 靜態的View:view預設持有Activity的context
  • 靜態Activity
public class MainActivity extends AppCompatActivity {


private static Context StaticVarible;

private Handler mHandler;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

btn = (Button)findViewById(R.id.button);

btn.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

MainActivity.this.finish();

}

});

StaticVarible = this;

LeakCanary.install(getApplication());

}


}

解決辦法:

在Activity Destory時將靜態變數置空即可

​
@Override

protected void onDestroy() {

StaticVarible = null;

super.onDestroy();

}

​

2、匿名內部類或者非靜態內部類

非靜態的內部類和匿名內部類都會隱式地持有其外部類的引用(否則怎麼訪問外部類的非靜態成員呢?),靜態的內部類不會持有外部類的引用。

java允許我們在一個類裡面定義靜態類。比如內部類(nested class)。把nested class封閉起來的類叫外部類。在java中,我們不能用static修飾頂級類(top level class)。只有內部類可以為static。
靜態內部類和非靜態內部類之間到底有什麼不同呢?下面是兩者間主要的不同。
(1)內部靜態類不需要有指向外部類的引用。但非靜態內部類需要持有對外部類的引用。
(2)非靜態內部類能夠訪問外部類的靜態和非靜態成員。靜態類不能訪問外部類的非靜態成員。他只能訪問外部類的靜態成員。
(3)一個非靜態內部類不能脫離外部類實體被建立,一個非靜態內部類可以訪問外部類的資料和方法,因為他就在外部類裡面。

常見的有:

Handler,AsyncTask,TimerTask等,一般在處理多執行緒任務的時候。

public class Activity6 extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_6);
 
        findViewById( R.id.finish).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {<br>                    //模擬耗時操作
                    Thread.sleep( 15000 );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
 
    }
}

執行上面的程式碼後,點選finish按鈕,過一會兒發生了記憶體洩漏的問題。

 為什麼Activity6會發生記憶體洩漏?

進入Activity6 介面,然後點選finish按鈕,Activity6銷燬,但是Activity6裡面的執行緒還在執行,匿名內部類Runnable物件引用了Activity6的例項,導致Activity6所佔用的記憶體不能被GC及時回收。

解決辦法:

 1.使用靜態內部類      2.銷燬前及時處理非靜態內部類

如上面的例子中Runnable改為靜態非匿名內部類即可:

public class Activity6 extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_6);
 
        findViewById( R.id.finish).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
 
        new Thread( new MyRunnable()).start();
 
    }
 
    private static class MyRunnable implements Runnable {
 
        @Override
        public void run() {
            try {
                Thread.sleep( 15000 );
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
     
}

上面這個程式碼已經有效的解決了Handler,Runnable 引用Activity例項從而導致記憶體洩漏的問題,但是這不夠。因為記憶體洩漏的核心原因就是這個某個物件應該被系統回收記憶體的時候,卻被其他物件引用,造成該記憶體無法回收。所以我們在寫程式碼的時候,要始終繃著這個弦。再回到上面這個問題,噹噹前Activity呼叫finish銷燬的時候,在這個Activity裡面所有執行緒是不是應該在OnDestory()方法裡,取消執行緒。當然是否取消非同步任務,要看專案具體的需求,比如在Activity銷燬的時候,啟動一個執行緒,非同步寫log日誌到本地磁碟,針對這個需求卻需要在OnDestory()方法裡開啟執行緒。所以根據當前環境做出選擇才是正解。

所以我們還可以修改程式碼為:在onDestroy() 裡面移除所有的callback 和 Message 。

@Override
    protected void onDestroy() {
        super.onDestroy();
 
       //如果引數為null的話,會將所有的Callbacks和Messages全部清除掉。
        handler.removeCallbacksAndMessages( null );
    }

3、 資源未關閉或監聽器未移除(登出)

在開發中,如果使用了BraodcastReceiverContentObserverFileCursorStreamBitmap自定義屬性attributeattr、感測器等資源,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,從而造成記憶體洩漏。比如:

// 使用感測器等資源,需要登出
SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
sensorManager.unregisterListener(listener);
// 使用BraodcastReceiver,需要登出
Myreceiver recevier = new Myreceiver();
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
registerReceiver(recevier,intentFilter);
unRegisterReceiver(recevier);
// 自定義屬性,需要recycle
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AttrDeclareView);
int color = a.getColor(R.styleable.AttrDeclareView_background_color, 0);
a.recycle();

除了上述常見的記憶體洩漏外,還有包括無限迴圈動畫使用ListView使用集合容器以及使用WebView也會造成記憶體洩漏,其中,無限迴圈動畫造成洩漏的原因是沒有在Activity的onDestory中停止動畫;使用ListView造成洩漏的原因是構造Adapter時沒有使用快取的convertView;使用集合容器造成洩漏的原因是在不使用相關物件時,沒有清理掉集合中儲存的物件引用。在優化時,在退出程式之前將集合中的元素(引用)全部清理掉,再置為null;使用WebView造成洩漏的原因是在不使用WebView時沒有呼叫其destory方法來銷燬它,導致其長期佔用記憶體且不能被回收。在優化時,可以為WebView開啟另外一個程式,通過AIDL與主執行緒進行通訊,便於WebVIew所在的程式可以根據業務需要選擇合適的時機進行銷燬。

 

 

 

相關文章