具有垃圾回收特性的語言(如Java)的優點在于,它使得開發者不需要顯式的對內存的分配和回收進行管理。這個特性降低引發段錯誤引發應用崩潰的風險,避免沒有釋放的內存長期占據堆內存,從而編寫出更加安全的代碼。可惜這并不是銀彈,在Java里還是有其他方式導致內存泄露,這意味著我們的Android App依然存在浪費不必要的內存,最終由于內存不足(OOM)導致Crash的可能性。原文鏈接
傳統的內存泄露方式是:在所有相關的引用離開作用域后,沒有釋放之前申請的內存空間。邏輯上的內存泄露,是沒有釋放不再需要的對象的引用的結果。如果一個對象的強引用依然存在,垃圾回收器就不能把這個對象從內存里回收。在Android開發里,Context上下文的泄露就通常就屬于這種泄露。因為Context對象如Activity通常引用了一大堆內存,如View的層級和其他資源。如果泄露了Context對象,通常意味著它所引用的所有對象也跟著泄露。Android應用運行在內存受限的設備上,如果有多處地方泄露的話,應用很容易耗光所有的可用內存。
如果對象沒有明確的生命周期,那么檢測邏輯上的內存泄露更像是一個主觀的問題。幸運的是,Activity擁有明確定義的生命周期,因此我們能明確的知道一個Activity實例是否已經泄露。Activity的onDestroy()方法在Activity的生命周期的最后被調用,意味著它在編程意圖上或Android系統調度上需要進行一些內存的回收。如果這個方法調用完畢后,Activity實例依舊能從堆的根通過強引用鏈被訪問到,垃圾回收器也就無法將它標記為可從內存回收——盡管從原本的意圖是將它從內存中刪除。因此,我們可以將一個在生命周期結束后依舊存在的Activity對象標記為被泄露。
Activity是一個很重的對象,因此你不應該選擇干預Android框架對它們的調度處理。然而,依舊有方法不經意的導致Activity泄露。在Android上,所有導致內存泄露的陷阱都離不開兩個基礎場景。第一個內存泄露的類別是進程級別的全局共享靜態變量,它們的存在狀態不取決于應用的狀態,同時還持有指向Activity的引用鏈。另一個內存泄露類別是因為線程的運行時間比Activity的生命周期還長,忽視了清除一個指向Activity的強引用鏈。下面我們來看下幾種可能會遇到的內存泄露的情況。
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();
}
});
2. 靜態View
另一個類似的場景:如果一個Activity需要經常被訪問,那么我們可能會選擇使用單例模式,保持一個實例在內存里,以便它可以被快速的使用到。然而,若前所述,干預Activity的生命周期并將它保持在內存里是一件很危險也沒有必要的事情,應該盡可能的避免這么做。
但如果我們有一個View對象,需要花費很大的代價去創建它,而它在Activity的不同的生命周期里保持不變,那么我們能不能把在這個實例存在靜態變量里,再講他附加到View的層級結構里去?讓我們來看下。完整代碼當我們的Activity被回收的時候,大部分的內存可以被回收。
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();
}
});
等下!看到沒。你知道一個attach了的view內部會持有一個指向Context的引用,換句話說,那就是我們的Activity。通過吧一個View設為靜態變量,我們創建了一個能長期持有Activity的引用鏈,導致Activity被泄露了。千萬不要把attach的view設為靜態變量,如果實在必須這么做,至少保證在Activity的生命周期結束前把它從View的層級結構里detach掉。
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();
}
});
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();
}
});
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();
}
});
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();
}
});
我們已經見識到了一系列內存泄露,也知道他們是多么容易不小心就泄露一堆的內存。記住,盡管最壞的可能性也就是導致你的應用因為內存不足而崩潰,也不一定會一直這樣。但是它會吃掉你應用內的一大部分不必要的內存。在這個時候,你的應用將會缺少內存來生成別的對象,進而導致垃圾回收器頻繁的執行,以便釋放內存給新的對象使用。垃圾回收是一個非常昂貴(耗時)的操作,還會產生用戶可感知的卡頓。因此,需要對可能存在的內存泄露保持警惕,并時常對內存泄露進行測試。