Android技術分享| Android 中部分記憶體洩漏示例及解決方案

語言: CN / TW / HK

簡單介紹記憶體洩漏&記憶體抖動

記憶體洩漏

Memory leak, 是一種資源洩漏,主因是計算機程式對儲存器配置管理失當,失去對一段已分配記憶體空間的控制,造成程式繼續佔用已經不再使用的記憶體空間,或是儲存器所儲存之物件無法透過執行程式碼而訪問,令記憶體資源空耗。

簡單來說,記憶體洩漏 是指無法正確回收已經不再使用的記憶體

舉例:

請注意以下的例子是虛構的

在此例中的應用程式是一個簡單軟體的一小部分,用來控制電梯的運作。
此部分軟體當乘客在電梯內按下一樓層的按鈕時執行。

當按下按鈕時:

要求使用儲存器,用作記住目的樓層
把目的樓層的數字儲存到儲存器中
電梯是否已到達目的樓層?
如是,沒有任何事需要做:程式完成
否則:
等待直至電梯停止
到達指定樓層
釋放剛才用作記住目的樓層的儲存器

此程式有一處會造成儲存器洩漏:如果在電梯所在樓層按下該層的按鈕(即上述程式的第4步),程式將觸發判斷條件而結束執行,但儲存器仍一直被佔用而沒有被釋放。這種情況發生得越多,洩漏的儲存器也越多。

這個小錯誤不會造成即時影響。因為人不會經常在電梯所在樓層按下同一層的按鈕。而且在通常情況下,電梯應有足夠的儲存器以應付上百次、上千次類似的情況。不過,電梯最後仍有可能消耗完所有儲存器。這可能需要數個月或是數年,所以在簡單的測試下這個問題不會被發現。

而這個例子導致的後果會是不那麼令人愉快。至少,電梯不會再理會前往其他樓層的要求。更嚴重的是,如果程式需要儲存器去開啟電梯門,那可能有人被困電梯內,因為電梯沒有足夠的儲存器去開啟電梯門。

儲存器洩漏只會在程式執行的時間內持續。例如:關閉電梯的電源時,程式終止執行。當電源再度開啟,程式會再次執行而儲存器會重置,而這種緩慢的洩漏則會從頭開始再次發生。

記憶體抖動

源自Android文件中的Memory churn一詞,中文翻譯為記憶體抖動。 指快速頻繁的建立物件從而產生的效能問題。

引用Android文件原文:

垃圾回收事件通常不會影響應用的效能。不過,如果在短時間內發生許多垃圾回收事件,就可能會快速耗盡幀時間。系統花在垃圾回收上的時間越多,能夠花在呈現或流式傳輸音訊等其他任務上的時間就越少。

通常,“記憶體抖動”可能會導致出現大量的垃圾回收事件。實際上,記憶體抖動可以說明在給定時間內出現的已分配臨時物件的數量。

例如,您可以在 for 迴圈中分配多個臨時物件。或者,您也可以在檢視的 onDraw() 函式中建立新的 PaintBitmap 物件。在這兩種情況下,應用都會快速建立大量物件。這些操作可以快速消耗新生代 (young generation) 區域中的所有可用記憶體,從而迫使垃圾回收事件發生。

記憶體洩漏(Memory leak)的產生和避免方式

Java記憶體洩漏的根本原因是長生命週期的物件持有短生命週期物件的引用就很可能發生記憶體洩漏。

儘管短生命週期物件已經不再需要,但因為長生命週期依舊持有它的引用,故不能被回收而導致記憶體洩漏。

幾種引起記憶體洩漏的問題:

靜態集合類引起的記憶體洩漏

HashMapArrayList等集合以靜態形式宣告時,這些靜態物件的生命週期與應用程式一致。他們所引用的物件也無法被釋放,因為它們也被集合引用著。 Java private static HashMap<String, Object> a = new HashMap(); public static void main(String args[]) { for (int i = 0; i < 1000; i++) { Object tO = new Object(); a.put("0", tO); tO = null; } } 如果僅僅釋放引用本身(tO = null),ArrayList依然在引用該物件,GC無法回收

監聽器

在Java應用中,通常會用到很多監聽器,一般通過addXXXXListener()實現。但釋放物件時通常會忘記刪除監聽器,從而增加記憶體洩漏的風險。

各種連線

如資料庫連線、網路連線(Socket)和I/O連線。忘記顯式呼叫close()方法引起的記憶體洩漏

內部類和外部模組的引用

內部類的引用是很容易被遺忘的一種,一旦沒有釋放可能會導致一系列後續物件無法釋放。此外還要小心外部模組不經意的引用,內部類是否提供相應的操作去除外部引用。

單例模式

由於單例的靜態特性,使其生命週期與應用的生命週期一樣長,一旦使用不恰當極易造成記憶體洩漏。如果單利持有外部引用,需要注意提供釋放方式,否則當外部物件無法被正常回收時,會進而導致記憶體洩漏。

常見的記憶體洩漏處理方式:

集合類洩漏

如集合的使用範圍超過邏輯程式碼的範圍,需要格外注意刪除機制是否完善可靠。比如由靜態屬性static指向的集合。

單利洩漏

以下為簡單邏輯程式碼,只為舉例說明記憶體洩漏問題,不保證單利模式的可靠性 ```Java public class AppManager { private static AppManager instance; private Context context;

private AppManager(Context context) { this.context = context; }

public static AppManager getInstance(Context context) { if (instance == null) { instance = new AppManager(context); } return instance; } } ```

AppManager建立時需要傳入一個Context,這個Context的生命週期長短至關重要。 1. 如果傳入的是ApplicationContext,因為Application的生命週期等同於應用的生命週期,所以沒有任何問題 2. 如果傳入的是ActivityContext,則需要考慮這個Activity是否在整個生命週期都不會被回收了,如果不是,則會造成記憶體洩漏

非靜態內部類建立靜態例項造成的記憶體洩漏

```Java public class MyActivity extends AppCompatActivity { private static MyInnerClass mInnerClass = null;

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ...

if (mInnerClass == null) {
  mInnerClass = new MyInnerClass();
}

}

class MyInnerClass { ... } } ```

內部類持有外部類引用,而static宣告的物件宣告週期通常會比Activity長。即使關閉這個頁面,由於mInnerClass為靜態的,並且持有MyActivity的引用,導致無法回收此頁面從而引起記憶體洩漏

應該將該內部類單獨封裝為一個單例來使用。

匿名內部類/非同步執行緒

```Java public class MyActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ...

new Thread(new Runnable() {
  @Override
  public void run() {
    ...
  }
}).start();

} } ```

Runnable都使用了匿名內部類,將持有MyActivity的引用。如果任務在Activity銷燬前未完成,將導致Activity的記憶體無法被回收,從而造成記憶體洩漏

解決方法:將Runnable獨立出來或使用靜態內部類,可以避免因持有外部物件導致的記憶體洩漏

Handler造成的記憶體洩漏

```Java public class SampleActivity extends AppCompatActivity { private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { ... } }

@Override protected void onCreate(Bundle savedInstanceState) { ...

mHandler.postDelayed(new Runnable() {
  @Override
  public void run() {
    ...
  }
}, 300000);

finish();

} } ``` Handler屬於TLS(Thread Local Storage)變數,生命週期與Activity是不一致的,容易導致持有的物件無法正確被釋放

當Android應用程式啟動時,該應用程式的主執行緒會自動建立一個Looper物件和與之關聯的MessageQueue。

當主執行緒中例項化一個Handler物件後,它就會自動與主執行緒Looper的MessageQueue關聯起來。所有傳送到MessageQueue的Messag都會持有Handler的引用,所以Looper會據此回撥Handle的handleMessage()方法來處理訊息。只要MessageQueue中有未處理的Message,Looper就會不斷的從中取出並交給Handler處理。

另外,主執行緒的Looper物件會伴隨該應用程式的整個生命週期。

在Java中,非靜態內部類和匿名類內部類都會潛在持有它們所屬的外部類的引用,但是靜態內部類卻不會。

當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在於主執行緒中,它持有該 Activity 的 Handler 引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成記憶體洩漏(因 Handler 為非靜態內部類,它會持有外部類的引用,在這裡就是指 SampleActivity)。

解決方法:在 Activity 中避免使用非靜態內部類,比如上面我們將 Handler 宣告為靜態的,則其存活期跟 Activity 的生命週期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去,見如下程式碼:

```Java public class SampleActivity extends AppCompatActivity {

private static class MyHandler extends Handler { private final WeakReference mActivity;

public MyHandler(SampleActivity activity) {
  mActivity = new WeakReference<SampleActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
  SampleActivity activity = mActivity.get();
  if (activity != null) {
    ...
  }
}

} private final MyHandler mHandler = new MyHandler(this);

private static final Runnable mRunnable = new Runnable() { @Override public void run() { ... } }

@Override protected void onCreate(Bundle savedInstanceState) { ...

mHandler.postDelayed(mRunnable, 300000);
finish();

} } ```

避免不必要的靜態成員變數

對於BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap等資源的使用,應在Activity銷燬前及時關閉或登出

不使用WebView物件時,應呼叫destroy()方法銷燬

在這裡插入圖片描述