常見的八種導致 APP 內存泄漏的問題

常見的八種導致 APP 內存泄漏的問題

像 Java 這樣具有垃圾回收功能的語言的好處之一,就是程序員無需手動管理內存分配。這減少了段錯誤(segmentation fault)導致的閃退,也減少了內存泄漏導致的堆空間膨脹,讓編寫的代碼更加安全。然而,Java 中依然有可能發生內存泄漏。所以你的安卓 APP 依然有可能浪費了大量的內存,甚至由於內存耗盡(OOM)導致閃退。

傳統的內存泄漏是由忘記釋放分配的內存導致的,而邏輯上的內存泄漏則是由於忘記在對象不再被使用的時候釋放對其的引用導致的。如果一個對象仍然存在強引用,垃圾回收器就無法對其進行垃圾回收。在安卓平臺,泄漏 Context 對象問題尤其嚴重。這是因爲像 Activity 這樣的 Context 對象會引用大量很佔用內存的對象,例如 View 層級,以及其他的資源。如果 Context 對象發生了內存泄漏,那它引用的所有對象都被泄漏了。安卓設備大多內存有限,如果發生了大量這樣的內存泄漏,那內存將很快耗盡。

如果一個對象的合理生命週期沒有清晰的定義,那判斷邏輯上的內存泄漏將是一個見仁見智的問題。幸運的是,activity 有清晰的生命週期定義,使得我們可以很明確地判斷 activity 對象是否被內存泄漏。onDestroy() 函數將在 activity 被銷燬時調用,無論是程序員主動銷燬 activity,還是系統爲了回收內存而將其銷燬。如果 onDestroy 執行完畢之後,activity 對象仍被 heap root 強引用,那垃圾回收器就無法將其回收。所以我們可以把生命週期結束之後仍被引用的 activity 定義爲被泄漏的 activity。

Activity 是非常重量級的對象,所以我們應該極力避免妨礙系統對其進行回收。然而有多種方式會讓我們無意間就泄露了 activity 對象。我們把可能導致 activity 泄漏的情況分爲兩類,一類是使用了進程全局(process-global)的靜態變量,無論 APP 處於什麼狀態,都會一直存在,它們持有了對 activity 的強引用進而導致內存泄漏,另一類是生命週期長於 activity 的線程,它們忘記釋放對 activity 的強引用進而導致內存泄漏。下面我們就來詳細分析一下這些可能導致 activity 泄漏的情況。

1. 靜態 Activity

泄漏 activity 最簡單的方法就是在 activity 類中定義一個 static 變量,並且將其指向一個運行中的 activity 實例。如果在 activity 的生命週期結束之前,沒有清除這個引用,那它就會泄漏了。這是因爲 activity(例如 MainActivity) 的類對象是靜態的,一旦加載,就會在 APP 運行時一直常駐內存,因此如果類對象不卸載,其靜態成員就不會被垃圾回收。

[代碼]xml代碼:

?
01
02
03
04
05
06
07
08
09
10
11
void setStaticActivity() {
  activity = this;
}
 
View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
  }
});

內存泄漏場景 1 - Static Activity

2. 靜態 View

另一種類似的情況是對經常啓動的 activity 實現一個單例模式,讓其常駐內存可以使它能夠快速恢復狀態。然而,就像前文所述,不遵循系統定義的 activity 生命週期是非常危險的,也是沒必要的,所以我們應該極力避免。

但是如果我們有一個創建起來非常耗時的 View,在同一個 activity 不同的生命週期中都保持不變呢?所以讓我們爲它實現一個單例模式,就像這段代碼。現在一旦 activity 被銷燬,那我們就應該釋放大部分的內存了。

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
10
11
voidsetStaticView() {
  view = findViewById(R.id.sv_button);
}
 
View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(newView.OnClickListener() {
  @Overridepublic void onClick(View v) {
    setStaticView();
    nextActivity();
  }
});

內存泄漏場景 2 - Static View

內存泄漏了!因爲一旦 view 被加入到界面中,它就會持有 context 的強引用,也就是我們的 activity。由於我們通過一個靜態成員引用了這個 view,所以我們也就引用了 activity,因此 activity 就發生了泄漏。所以一定不要把加載的 view 賦值給靜態變量,如果你真的需要,那一定要確保在 activity 銷燬之前將其從 view 層級中移除

3. 內部類

現在讓我們在 activity 內部定義一個類,也就是內部類。這樣做的原因有很多,比如增加封裝性和可讀性。如果我們創建了一個內部類的對象,並且通過靜態變量持有了 activity 的引用,那也會發生 activity 泄漏。

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
voidcreateInnerClass() {
    classInnerClass {
    }
    inner = newInnerClass();
}
 
View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(newView.OnClickListener() {
    @Overridepublic void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

內存泄漏場景 3 - Inner Class
不幸的是,內部類能夠引用外部類的成員這一優勢,就是通過持有外部類的引用來實現的,而這正是 activity 泄漏的原因。

4. 匿名類

類似的,匿名類同樣會持有定義它們的對象的引用。因此如果在 activity 內定義了一個匿名的 AsyncTask 對象,就有可能發生內存泄漏了。如果 activity 被銷燬之後 AsyncTask 仍然在執行,那就會組織垃圾回收器回收 activity 對象,進而導致內存泄漏,直到執行結束才能回收 activity。

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
voidstartAsyncTask() {
    newAsyncTask<void,void,=""void="">() {
        @Overrideprotected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}
 
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(newView.OnClickListener() {
    @Overridepublic void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});
</void,>

內存泄漏場景 4 - AsyncTask
5. Handlers

同樣的,定義一個匿名的 Runnable 對象並將其提交到 Handler 上也可能導致 activity 泄漏。Runnable 對象間接地引用了定義它的 activity 對象,而它會被提交到 Handler 的 MessageQueue 中,如果它在 activity 銷燬時還沒有被處理,那就會導致 activity 泄漏了。

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
voidcreateHandler() {
    newHandler() {
        @Overridepublic void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(newRunnable() {
        @Overridepublic void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}
 
 
View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(newView.OnClickListener() {
    @Overridepublic void onClick(View v) {
        createHandler();
        nextActivity();
    }
});
內存泄漏場景 5 - Handler
6. Threads

同樣的,使用 Thread 和 TimerTask 也可能導致 activity 泄漏。

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
voidspawnThread() {
    newThread() {
        @Overridepublic void run() {
            while(true);
        }
    }.start();
}
 
View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(newView.OnClickListener() {
  @Overridepublic void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});
內存泄漏場景 6 - Thread

7. Timer Tasks

只要它們是通過匿名類創建的,儘管它們在單獨的線程被執行,它們也會持有對 activity 的強引用,進而導致內存泄漏。

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
voidscheduleTimer() {
    newTimer().schedule(newTimerTask() {
        @Override
        publicvoid run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}
 
View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(newView.OnClickListener() {
    @Overridepublic void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }
});

內存泄漏場景 7 - TimerTask
8. Sensor Manager

最後,系統服務可以通過 context.getSystemService 獲取,它們負責執行某些後臺任務,或者爲硬件訪問提供接口。如果 context 對象想要在服務內部的事件發生時被通知,那就需要把自己註冊到服務的監聽器中。然而,這會讓服務持有 activity 的引用,如果程序員忘記在 activity 銷燬時取消註冊,那就會導致 activity 泄漏了。

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
voidregisterListener() {
       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(newView.OnClickListener() {
    @Overridepublic void onClick(View v) {
        registerListener();
        nextActivity();
    }
});

內存泄漏場景 8 - Sensor Manager

現在,我們展示了八種很容易不經意間就泄漏大量內存的情景。請記住,最壞的情況下,你的 APP 可能會由於大量的內存泄漏而內存耗盡,進而閃退,但它並不總是這樣。相反,內存泄漏會消耗大量的內存,但卻不至於內存耗盡,這時,APP 會由於內存不夠分配而頻繁進行垃圾回收。垃圾回收是非常耗時的操作,會導致嚴重的卡頓。在 activity 內部創建對象時,一定要格外小心,並且要經常測試是否存在內存泄漏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章