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()方法銷燬

在這裏插入圖片描述