[譯]內存泄露的八種花樣

這是好久之前發佈在簡書平臺上的一篇有關內存泄漏的譯文。
這篇文章說起的8種內存泄漏的場景,如今來看依舊很經典。爲了不內存泄漏,開發過程當中須要謹慎謹慎再謹慎。同時,保持良好的開發習慣也相當重要。html

具備垃圾回收特性的語言(如Java)的優勢在於,它使得開發者不須要顯式的對內存的分配和回收進行管理。這個特性下降引起段錯誤引起應用崩潰的風險,避免沒有釋放的內存長期佔據堆內存,從而編寫出更加安全的代碼。惋惜這並非銀彈,在Java裏仍是有其餘方式致使內存泄露,這意味着咱們的Android App依然存在浪費沒必要要的內存,最終因爲內存不足(OOM)致使Crash的可能性。原文連接java

傳統的內存泄露方式是:在全部相關的引用離開做用域後,沒有釋放以前申請的內存空間。邏輯上的內存泄露,是沒有釋放再也不須要的對象的引用的結果。若是一個對象的強引用依然存在,垃圾回收器就不能把這個對象從內存裏回收。在Android開發裏,Context上下文的泄露就一般就屬於這種泄露。由於Context對象如Activity一般引用了一大堆內存,如View的層級和其餘資源。若是泄露了Context對象,一般意味着它所引用的全部對象也跟着泄露。Android應用運行在內存受限的設備上,若是有多處地方泄露的話,應用很容易耗光全部的可用內存。android

若是對象沒有明確的生命週期,那麼檢測邏輯上的內存泄露更像是一個主觀的問題。幸運的是,Activity擁有明肯定義的生命週期,所以咱們能明確的知道一個Activity實例是否已經泄露。Activity的onDestroy()方法在Activity的生命週期的最後被調用,意味着它在編程意圖上或Android系統調度上須要進行一些內存的回收。若是這個方法調用完畢後,Activity實例依舊能從堆的根經過強引用鏈被訪問到,垃圾回收器也就沒法將它標記爲可從內存回收——儘管從本來的意圖是將它從內存中刪除。所以,咱們能夠將一個在生命週期結束後依舊存在的Activity對象標記爲被泄露。git

Activity是一個很重的對象,所以你不該該選擇干預Android框架對它們的調度處理。然而,依舊有方法不經意的致使Activity泄露。在Android上,全部致使內存泄露的陷阱都離不開兩個基礎場景。第一個內存泄露的類別是進程級別的全局共享靜態變量,它們的存在狀態不取決於應用的狀態,同時還持有指向Activity的引用鏈。另外一個內存泄露類別是由於線程的運行時間比Activity的生命週期還長,忽視了清除一個指向Activity的強引用鏈。下面咱們來看下幾種可能會遇到的內存泄露的狀況。github

1. 靜態Activity

最容易泄露Activity的方式莫過於定義一個類,類的內部經過靜態變量的方式持有Activity,而後在運行中,將Activity實例賦值給這個變量。若是這個靜態變量的引用在Activity的生命週期結束前沒有置空的話,Activity實例就泄露了。由於被靜態變量持有的對象,它將會被保持在內存中,在App的運行過程當中一直存在。若是有一個靜態變量持有了Activity的引用,那麼這個Activity就沒法被垃圾回收器回收。完整代碼編程

void setStaticActivity() {
  activity = this;
}

View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
  }
});複製代碼

Activity內存泄露
Activity內存泄露

2. 靜態View

另外一個相似的場景:若是一個Activity須要常常被訪問,那麼咱們可能會選擇使用單例模式,保持一個實例在內存裏,以便它能夠被快速的使用到。然而,若前所述,干預Activity的生命週期並將它保持在內存裏是一件很危險也沒有必要的事情,應該儘量的避免這麼作。安全

但若是咱們有一個View對象,須要花費很大的代價去建立它,而它在Activity的不一樣的生命週期裏保持不變,那麼咱們能不能把在這個實例存在靜態變量裏,再講他附加到View的層級結構裏去?讓咱們來看下。完整代碼當咱們的Activity被回收的時候,大部分的內存能夠被回收。bash

void setStaticView() {
  view = findViewById(R.id.sv_button);
}

View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticView();
    nextActivity();
  }
});複製代碼

靜態View內存泄露
靜態View內存泄露

等下!看到沒。你知道一個attach了的view內部會持有一個指向Context的引用,換句話說,那就是咱們的Activity。經過吧一個View設爲靜態變量,咱們建立了一個能長期持有Activity的引用鏈,致使Activity被泄露了。千萬不要把attach的view設爲靜態變量,若是實在必須這麼作,至少保證在Activity的生命週期結束前把它從View的層級結構裏detach)掉。app

3. 內部類

除了這,讓咱們在咱們的Activity類裏在定義一個類,也就是內部類。爲了提升代碼的可讀性和健壯性,封裝程序邏輯,咱們可能會這麼作。若是咱們建立了一個這樣的內部類的實例,並經過靜態變量持有了它,會怎樣呢?你應該能猜到這又是一個內存泄露的點。框架

void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});複製代碼

內部類致使的內存泄露
內部類致使的內存泄露

不幸的是,因爲內部類能夠直接訪問到它的外部類的變量,這個特性意味着內部類會隱式的持有一個對它的外部類的引用,這間接致使了咱們不當心又泄露了Activity。

4. 匿名類

一樣的,匿名類也持有一個指向它申明的地方所在的類的引用。若是你在Activity內定義和實例化一個AsyncTask匿名類,那也可能發生內存泄露

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});複製代碼

AsyncTask的內存泄露
AsyncTask的內存泄露

5. Handler

一樣的原則也適用於後臺任務:定義一個匿名的Runnable,而後將它加入Handler的處理隊列裏。這個Runnable對象會隱含的持有一個指向它定義的時候所在的Activity的引用,而後它會做爲一個消息對象加入到Handler的消息隊列裏去。在Activity生命週期結束以後,只要這個消息還沒被Activity處理,那就有一條引用鏈指向咱們的Activity對象,使得Activity對象沒法被回收,進而泄露。

void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}


View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});複製代碼

Handler致使的內存泄露
Handler致使的內存泄露

6. 線程

相似的問題咱們能夠在線程定時任務(TimerTask)裏發現。

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

View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});複製代碼

線程使用不當致使內存泄露
線程使用不當致使內存泄露

7. 定時任務

只要它們是經過匿名類的方式定義和實例化的,即使是工做在另外的線程,依舊會在Activity被destroy以後,存在一條指向Activity的引用鏈,致使Activity泄露。

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

View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }
});複製代碼

TimerTask致使的內存泄露
TimerTask致使的內存泄露

8. 系統服務

最後,還有一些系統服務能夠經過上下文Context對象上的getSystemService)方法獲取到。這些服務運行在他們各自的進程裏,協助應用執行某種類型的的後臺任務,或者和設備的硬件進行交互。若是Context對象須要系統服務內的某個事件發生的時候通知到這個Context,那麼它須要把自身做爲一個監聽器註冊給系統服務。系統服務也由此持有了一個對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);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});複製代碼

傳感器管理器致使的內存泄露
傳感器管理器致使的內存泄露


咱們已經見識到了一系列內存泄露,也知道他們是多麼容易不當心就泄露一堆的內存。記住,儘管最壞的可能性也就是致使你的應用由於內存不足而崩潰,也不必定會一直這樣。可是它會吃掉你應用內的一大部分沒必要要的內存。在這個時候,你的應用將會缺乏內存來生成別的對象,進而致使垃圾回收器頻繁的執行,以便釋放內存給新的對象使用。垃圾回收是一個很是昂貴(耗時)的操做,還會產生用戶可感知的卡頓。所以,須要對可能存在的內存泄露保持警戒,並時常對內存泄露進行測試。

相關文章
相關標籤/搜索