這是很久以前釋出在簡書平臺上的一篇有關記憶體洩漏的譯文。
這篇文章提及的8種記憶體洩漏的場景,現在來看依舊很經典。為了避免記憶體洩漏,開發過程中需要謹慎謹慎再謹慎。同時,保持良好的開發習慣也至關重要。
具有垃圾回收特性的語言(如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();
}
});複製程式碼
我們已經見識到了一系列記憶體洩露,也知道他們是多麼容易不小心就洩露一堆的記憶體。記住,儘管最壞的可能性也就是導致你的應用因為記憶體不足而崩潰,也不一定會一直這樣。但是它會吃掉你應用內的一大部分不必要的記憶體。在這個時候,你的應用將會缺少記憶體來生成別的物件,進而導致垃圾回收器頻繁的執行,以便釋放記憶體給新的物件使用。垃圾回收是一個非常昂貴(耗時)的操作,還會產生使用者可感知的卡頓。因此,需要對可能存在的記憶體洩露保持警惕,並時常對記憶體洩露進行測試。