在做Android開發過程中最長遇到的一個難點就是在Activity的生命週期中執行長時間任務而導致的不可避免的內存泄露。看看下面的代碼,有一個Activity在創建的時候會啓動一個線程,並且循環執行任務。
/**
* 示例向我們展示了在 Activity 的配置改變時(配置的改變會導致它其下的Activity實例被銷燬)
* 此外,Activity的context也是內存泄露的一部分,因爲由於線程被初始化爲匿名內部,使得其持有外部
* Activity的隱式引用,使得Activity不會被java的垃圾回收機制回收。
*/
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleOne();
}
private void exampleOne() {
new Thread() {
@Override
public void run() {
while (true) {
SystemClock.sleep(1000);
}
}
}.start();
}
}
當一個配置改變時,會導致整個Activity被銷燬及重新創建,我們總會簡單的認爲Android系統會在這之後清理並回收與Activity有關的內存和正在運行的線程。然而,事情並不是這樣的,所有提到的這些再也不會被回收,並且會導致內存泄露,從而很可能很大程度上影響到android的性能。
導致Acitvity內存泄露的根源
如果你讀過我之前發表的有關於Handlers跟內存類的博文的話,那我接下來要講的知識你肯定知道。在Java中,非靜態匿名類會隱式持有外部類的引用,如果你沒有注意這一點的話,存儲這些引用將導致Acitvity被保留而不是被垃圾回收機制回收。Activity對象持有其View層以及所有的資源,所以說一旦你出現Activity內存泄露,那麼你將會失去一大片的內存空間。
這樣的問題在Activity配置改變時更加嚴重,因爲Activity配置的改變表示Android系統將要銷燬一個Activity並且重新創建一個。例如執行10次橫屏豎屏的操作後,每次的操作都會執行前面的代碼,那麼我們會發現(使用Eclipse的內存分析工具可以看到)每一個Activity都因爲留有隱式引用而被保留下在內存中。
在每次配置改變時,Android系統會創建一個新的Activity,並且把改變前的Actvity交給垃圾回收機制回收。然而,線程隱式的持有了舊的Activity的引用,使該Activity沒有被垃圾回收機制回收,這樣的問題會導致每一個Activity都會發生內存泄露,並且與他們相關的所有資源都再也無法得到回收。
一旦我們瞭解到了問題的本質修復這個問題是非常容易的:將線程聲明爲靜態內部類就跟下面的代碼一樣:
/**
* 這個例子通過聲明線程爲私有的靜態內存類的方式避免了Activity Context的內存泄露,
* 但是所有的線程仍然在繼續的運行,即時配置發生變化。因爲DVM虛擬機持有這些所有正在運行
* 的線程的引用,並且這些線程是否被垃圾回收機制回收對Activity的生命週期沒有任何的
* 影響,這些線程會一直運行直到Android系統銷燬了你的應用程序的進程。
*/
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleTwo();
}
private void exampleTwo() {
new MyThread().start();
}
private static class MyThread extends Thread {
@Override
public void run() {
while (true) {
SystemClock.sleep(1000);
}
}
}
}
新的線程將不會再隱式的持有Activity的引用並且在配置發生改變時,Activity也能夠被垃圾回收機制回收。
導致線程內存泄露的根源
第二個問題是對於每一個新創建的Activity,線程的內存泄露將再也不能夠被回收,線程是JAVA垃圾回收機制的根源,由於在運行系統中DVM虛擬機一直持有着所有運行狀態的線程的引用,結果導致處於運行狀態的線程將永遠不會被回收。因此你必須要爲你的後臺進行實現銷燬的邏輯!下面的例子將展現如何完成這些銷燬邏輯的。
/**
* 通例二一樣,除了這次我們爲線程實現了一個銷燬的邏輯,確保它再也不會出現內存泄露的問題。
* OnDestroy()通常是一個很好的地方在我們退出Activity時關閉你正在運行的線程
*/
public class MainActivity extends Activity {
private MyThread mThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleThree();
}
private void exampleThree() {
mThread = new MyThread();
mThread.start();
}
/**
* 靜態內部類將不會再隱式的持有外部類的引用,所以在配置改變時,你的Activity的實例在也不會
* 出現內存泄露
*/
private static class MyThread extends Thread {
private boolean mRunning = false;
@Override
public void run() {
mRunning = true;
while (mRunning) {
SystemClock.sleep(1000);
}
}
public void close() {
mRunning = false;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mThread.close();
}
}
通過上面的代碼,我們在 onDestroy() 方法中結束了線程,確保不會發生意外的線程的內存泄漏問題。如果你想要在配置改變後保留該線程(而不是每一次在關閉 Activity 後都要新建一個線程),那我建議你使用 Fragment 去完成該耗時任務。你可以翻我以前的博文,一名叫作“Handling Configuration Changes with Fragments”應該能滿足你的需求,在API demo中也提供了很好理解的例子來爲你闡述相關概念。
結論
Android 開發過程中,在 Activity 的生命週期裏協調耗時任務可能會很困難,你一不小心就會導致內存泄漏問題。下面是一些建議當你在Activity的生命週期中處理你的長時間後臺任務時:
- 儘可能的使用靜態內部類而不是非靜態內部類。 每一個非靜態內部類的實例都會持有外部Activity的實例引用,存儲這些引用將導致Acitvity被保留而不是被垃圾回收機制回收。如果你的靜態內部類需要引用相關的Activity以確保功能的正常使用,那麼你得確保你在對象中使用的是一個 Activity 的弱引用,否則你的 Activity 將會發生意外的內存泄漏。
- 不要總想着java會爲你清理你的正常運行的線程。在上面的例子中,我們很容易的認爲,當用戶退出Activity的時候,Activity的實例以及與他相關的正在運行的線程都會被垃圾回收機制回收,這個是不可能的,Java的線程將會一直存在直到他們都被顯式的關閉或者整個進程被Android系統結束掉。所以爲你的後臺線程實現回收邏輯是極其重要,此外,你在設計銷燬邏輯時要根據 Activity 的生命週期去設計,避免出現 Bug。
- 考慮你是否真的需要用到線程。Android應用的框架層爲我們提供了很多便於開發者執行後臺操作的類,例如:我們可以使用 Loader 代替在 Activity 的生命週期中用線程通過注入執行短暫的異步後臺查詢操作,考慮用 Service 將結構通知給 UI 的 BroadcastReceiver。最後,記住,這篇博文中對線程進行的討論同樣適用於 AsyncTask(因爲 AsyncTask 使用 ExecutorService 執行它的任務)。然而,雖說 ExecutorService 只能在短暫操作(文檔說最多幾秒)中被使用,那麼這些方法導致的 Activity 內存泄漏應該永遠不會發生。
這篇博文的源代碼都在Github ,你也可以從Google play下載到