Android中常見的內存泄漏

轉載請註明出處:【huachao1001的專欄:http://blog.csdn.net/huachao1001】

我們經常會在不經意間寫出造成內存泄漏的代碼,往往在代碼上很難查出來。但是我們可以通過一些輔助工具來檢測是否存在內存泄漏,比如通過AndroidStudio的monitors來查看內存的變化情況,或者是通過開源框架《LeakCanary》來檢測。本文主要是從網絡中搜索彙總一些常見的內存泄漏,一方面自己應對校招,另一方面以後自己寫代碼時也會注意這些問題。當然了,還有一方面就是方便大家~

1 Activity對象未被回收

本節是從《Eight Ways Your Android App Can Leak Memory》中學習並總結。

1.1 靜態變量引用Activity對象

通過靜態變量引用Activty對象時,會導致Activty對象所佔內存內漏。主要是因爲,靜態變量是駐紮在JVM的方法區,因此,靜態變量引用的對象是不會被GC回收的,因爲它們所引用的對象本身就是GC ROOT(這塊不清楚的請參考我的另一篇文章《JVM理解其實並不難! 》)。即最終導致Activity對象不被回收,從而也就造成內存泄漏。

看個簡單例子,比如說,你應用啓動Activty的場景很多,你希望定義一個工具類Util.java,在這個類中,定義一個啓動Activty的方法startActivity(Class nextActivity);以此來簡化啓動Activty的代碼。另外,加入你當前的Activty啓動另一個Activty的代碼使用率也特別高。爲了使得參數儘可能的少,你提供setFirstActivty,保存當前的Activty。代碼如下:

Util.java

/**
 * Created by HuaChao on 2016/8/13.
 */
public class Util {

    private static Activity sActivity;

    public static void setActivity(Activity activity) {
        sActivity = activity;
    }

    public static void startActivity(Class nextActivity) {
        Intent intent = new Intent(sActivity, nextActivity);
        sActivity.startActivity(intent);
    }
}

在當前的Activty中,只需在onCreate中調用Util.setFirstActivity(this);,在需要啓動另一個Activty處調用Util.startActivity(SecondActivity.class);

在上面代碼中,如果當前的Activty不再使用且Util中的sActivity對象沒有更改,會導致當前Activty一直駐留在內存中。

1.2 靜態View

有時,當一個Activity經常啓動,但是對應的View讀取非常耗時,我們可以通過靜態View變量來保持對該Activity的rootView引用。這樣就可以不用每次啓動Activity都去讀取並渲染View了。這確實是一個提高Activity啓動速度的好方法!但是要注意,一旦View attach到我們的Window上,就會持有一個Context(即Activity)的引用。而我們的View有事一個靜態變量,所以導致Activity不被回收。當然了,也不是說不能使用靜態View,但是在使用靜態View時,需要確保在資源回收時,將靜態View detach掉。

1.3 內部類

我們知道,非靜態內部類持有外部類的一個引用。因此,如果我們在一個外部類中定義一個靜態變量,這個靜態變量是引用內部類對象。將會導致內存泄漏!因爲這相當於間接導致靜態引用外部類。

static InnerClass innerClass;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main); 
    innerClass = this.new InnerClass(); 
}

class InnerClass { 
}

1.4 匿名類

與內部類一樣,匿名類也會持有外部類的引用。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);


    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            //另一個線程中持有Activity的引用,並且不釋放
            while (true) ;
        }
    }.execute();

}

1.5 Handler

我們知道,主線程的Looper對象不斷從消息隊列中取出消息,然後再交給Handler處理。如果在Activity中定義Handler對象,那麼Handler肯定是持有Activty的引用。而每個Message對象是持有Handler的引用的(Message對象的target屬性持有Handler引用),從而導致Message間接引用到了Activity。如果在Activty destroy之後,消息隊列中還有Message對象,Activty是不會被回收的。當然了,如果消息正在準備(處於延時入隊期間)放入到消息隊列中也是一樣的。

private final Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {

    }
};


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);


    handler.postDelayed(new Runnable() {
        @Override
        public void run() { /* ... */ }
    }, Integer.MAX_VALUE); 
}

解決辦法就是,將Handler放入單獨的類或者將Handler放入到靜態內部類中(靜態內部類不會持有外部類的引用)。如果想要在handler內部去調用所在的外部類Activity,可以在handler內部使用弱引用的方式指向所在Activity,這樣不會導致內存泄漏。

1.6 Threads和TimerTask

Threads和Timer導致內存泄漏的原因跟內部類一樣。雖然在新的線程中創建匿名類,但是只要是匿名類/內部類,它都會持有外部類引用。

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}

1.7 監聽器

當我們需要使用系統服務時,比如執行某些後臺任務、爲硬件訪問提供接口等等系統服務。我們需要把自己註冊到服務的監聽器中。然而,這會讓服務持有 activity 的引用,如果程序員忘記在 activity 銷燬時取消註冊,那就會導致 activity 泄漏了。

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

2 集合對象造成的泄漏

當我們定義一個靜態的集合類時,請注意,這可能會導致內存泄漏!前面我們提到過,靜態變量所引用的對象是不會被回收掉的。而我的靜態集合類中,包含有大量的對象,這些對象不會被回收。另外,如果集合中保存的對象又引用到了其他的大對象,如超長字符串、Bitmap、大數組等,很容易造成OOM。

3 資源對象沒關閉造成內存泄漏

當我們打開資源時,一般都會使用緩存。比如讀寫文件資源、打開數據庫資源、使用Bitmap資源等等。當我們不再使用時,應該關閉它們,使得緩存內存區域及時回收。雖然有些對象,如果我們不去關閉,它自己在finalize()函數中會自行關閉。但是這得等到GC回收時才關閉,這樣會導致緩存駐留一段時間。如果我們頻繁的打開資源,內存泄漏帶來的影響就比較明顯了。

4 使用對象池避免頻繁創建對象

在我們需要頻繁創建使用某個類時,或者是在for循環裏面創建新的對象時,導致JVM不斷創建同一個類。我們知道,在使用Message對象時,不是直接new出來的,而是通過obtain方法獲取,以及recycle方法回收。這是典型的享元模式(不熟悉的同學參考《從Android代碼中來記憶23種設計模式 》)。我們可以通過使用對象池來實現.

import android.support.v4.util.Pools;

/**
 * Created by HuaChao on 2016/8/13.
 */
public class MyObject {
    private static final Pools.SynchronizedPool<MyObject> MY_POOLS = new Pools.SynchronizedPool<>(10);

    public static MyObject obtain() {
        MyObject object = MY_POOLS.acquire();
        if (object == null)
            object = new MyObject();
        return object;
    }

    public void recycle() {
        MY_POOLS.release(this);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章