Android中常見的內存泄漏

轉載請注明出處:【huachao1001的簡書:http://www.lxweimin.com/users/0a7e42698e4b/latest_articles】

我們經常會在不經意間寫出造成內存泄漏的代碼,往往在代碼上很難查出來。但是我們可以通過一些輔助工具來檢測是否存在內存泄漏,比如通過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);
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • ###集合類泄漏 集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被占用。如果這個集合類是全局性的變...
    RunningTeemo閱讀 581評論 0 0
  • 寫在前面 雖然現在手機的內存不斷增大,但Android為了實現不同應用間運行隔離,不至于相互影響,所以對單個應用最...
    CPPAlien閱讀 5,546評論 7 31
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,033評論 25 708
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    _痞子閱讀 1,652評論 0 8
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    apkcore閱讀 1,237評論 2 7