Android 常見內存泄露 & 解決方案

前言

內存泄漏(Memory Leak)是指程序中己動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰 (OOM) 等嚴重後果。html

那什麼狀況下不能被回收呢?java

目前 java 垃圾回收主流算法是虛擬機採用 GC Roots Tracing 算法。算法的基本思路是:經過一系列的名爲 GC Roots (GC 根節點)的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑,當一個對象到GC Roots沒有任何引用鏈相連(圖論說:從GC Roots 到這個對象不可達)時, 證實此對象是不可用的。web

關於可達性的對象,即是能與 GC Roots 構成連通圖的對象,以下圖:算法

這裏寫圖片描述

根搜索算法的基本思路就是經過一系列名爲 "GC Roots" 的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈 ( Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的。數據庫

從上圖,reference一、reference二、reference3 都是 GC Roots,能夠看出:緩存

reference1-> 對象實例1;多線程

reference2-> 對象實例2;異步

reference3-> 對象實例4;ide

reference3-> 對象實例4 -> 對象實例6;函數

能夠得出對象實例一、二、四、6都具備 GC Roots 可達性,也就是存活對象,不能被 GC 回收的對象。

而對於對象實例三、5直接雖然連通,但並無任何一個 GC Roots 與之相連,這即是 GC Roots 不可達的對象,這就是 GC 須要回收的垃圾對象。

在瞭解 GC 以後,開始去了解 Android 的內存泄露狀況了。

Android 內存泄露場景 

 下面會詳細介紹一些常見的內存泄露場景,以及對應的修復辦法。

 非靜態內部類的靜態實例

好比咱們在 Activity 內部定義了一個內部類 InnerClass,同時定義了一個靜態變量 inner,並給予賦值。假設你在 onDestory 的時候沒有將 inner 置 null;那麼就會引發內存泄露。緣由是靜態變量持有了內部類的實例,內部類會對外部類有個引用,從而致使 Activity 得不到釋放。

    private static Object inner;
       
        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();
        }
    });

記得在生命週期結束的時候,將不須要的靜態變量置 null。

多線程相關的匿名內部類/非靜態內部類

和非靜態內部類同樣,匿名內部類也會持有外部類實例的引用。多線程相關的類有 AsyncTask 類,Thread 類和 Runnable 接口的類等,它們的匿名內部類若是作耗時操做
就可能發生內存泄露,這裏以 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();
        }
    });

當異步任務在後臺執行耗時任務期間,Activity 不幸被銷燬了(好比:用戶退出,系統回收),這個被 AsyncTask 持有的 Activity 實例就不會被垃圾回收器回收,直到異步任務結束。
解決方法是繼承 AsyncTask 新建一個靜態內部類,用靜態內部類建立實例就不會存在對外部實例的引用了。

 

Handler 內存泄露

一樣道理,Handler 的 message 被傳遞到消息隊列 MessageQueue 中,在 Message 消息沒有被處理以前,handler 的實例也不沒法被回收,若是 handler 實例不是靜態的,就會致使引用它的 activity 或者 service 不能被回收,因而就會發生內存泄漏。

    void createHandler() {
        new Handler() {
            @Override public void handleMessage(Message message) {
                super.handleMessage(message);
            }
        }.sendMessageDelayed(Message.obtain(), 60000);
    }
    
    
    View hButton = findViewById(R.id.h_button);
    hButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            createHandler();
            nextActivity();
        }
    });

 對於上述問題,有兩種解決辦法,一種是使用一個靜態的 handler 內部類,而且其持有的對象都改爲弱引用形式進行引用。還有一種是在銷燬 activity 的時候,將發送的消息進行移除。

myHandler.removeCallbackAndMessages(null);

這種有個問題就是 Handler 中的消息可能沒法所有被處理完。

另外還有一個要注意的是,最好不要直接使用 View#post 來作一些操做。若是要用,確保要用的話,確保 view 已經被 attach 到了 window。

具體能夠參考:View的post方法致使的內存泄漏分析

靜態 Activity 或 View

在類中定義了靜態 Activity變量,把當前運行的 Activity實例賦值於這個靜態變量。
若是這個靜態變量在 Activity生命週期結束後沒有清空,就致使內存泄漏。由於 static 變量是貫穿這個應用的生命週期的,因此被泄漏的  Activity 就會一直存在於應用的進程中,不會被垃圾回收器回收。
static 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();
      }
    });

爲了可以被回收,須要在不須要使用的時候進行置 null 操做。好比銷燬當前 activity 的時候。

特殊狀況:若是一個 View 初始化耗費大量資源,並且在一個 Activity 生命週期內保持不變,那能夠把它變成 static,加載到視圖樹上 (View Hierachy),像這樣,當 Activity 被銷燬時,應當釋放資源。

static view;
    
    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();
      }
    });

一樣的,爲了解決內存泄露的問題,在 Activity 銷燬的時候把這個 static view 置 null 便可,可是仍是不建議用這個 static view的方法。

Eventbus 等註冊監聽形成的內存泄露

相信不少同窗都在項目裏面會用到 Eventbus。對於一些沒有經驗的同窗在使用的時候常常會出現一些問題。好比說在 onCreate 的時候進行註冊,卻忘了反註冊,或者說,在onStop的時候進行反註冊,這些都會致使 Eventbus 的內存泄露。

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    EventBus.getDefault().register(this);// 注意在onCreate()方法中註冊
}

@Override
public void onDestroy() {
    EventBus.getDefault().unregister(this);// 注意在onDestory()方法中註冊
    super.onDestroy();
}

註冊和反註冊(取消註冊)是對應的,必需要添加,不然會引發組件的內存泄漏。由於註冊的時候組件是被 EventBus 內部的單例隊列所持有引用的。

若是你是在 View 裏面註冊 Eventbus 的,記得是在 View 的生命週期 onAttachedToWindow 和 onDetachedFromWindow 的時候進行註冊和反註冊。

最近跟個人同事進行聊天的時候發現,他們爲了解決 eventbus 致使的內存泄露問題(已經成對註冊和反註冊仍是存在內存泄露問題),因而打算建立一個 object 的實例,用這個來進行註冊與反註冊,這樣即便發生內存泄露也只會佔用很小的內存空間。

單例引發的內存泄露

項目中,常常會存在不少單例。有時候須要咱們將當前 Activity 實例傳給單例,而後去作一些事情。以下面的代碼:

public class SingleInstance {
    private Context mContext;
    private static SingleInstance instance;
 
    private SingleInstance(Context context) {
        this.mContext = context;
    }
 
    public static SingleInstance getInstance(Context context) {
        if (instance == null) {
            instance = new SingleInstance(context);
        }
        return instance;
    }
}

 上述單例中傳入一個 context ,就會致使 context 的生命時長和應用的生命時長同樣。就會形成內存泄露。

對於這種有三種解決辦法:

一、採用弱引用的方式進行引用,確保可以被回收;

二、在對應的 context 要被銷燬的時候,進行置 null;確保不會長於本來的生命時長;

三、看是否可以使用 APP context;這樣就不會存在內存泄露的問題了。

資源對象沒關閉形成內存泄漏

當咱們打開資源時,通常都會使用緩存。好比讀寫文件資源、打開數據庫資源、使用 Bitmap 資源等等。當咱們再也不使用時,應該關閉它們,使得緩存內存區域及時回收。雖然有些對象,若是咱們不去關閉,它本身在 finalize() 函數中會自行關閉。可是這得等到 GC 回收時才關閉,這樣會致使緩存駐留一段時間。若是咱們頻繁的打開資源,內存泄漏帶來的影響就比較明顯了。

解決辦法:及時關閉資源

 

WebView 

不一樣的Android 版本的 webView 會有差別,加上不一樣的廠商定製的 ROM 的 webView 差別,這就致使 webView 存在很大的兼容性問題。weView 都會存在內存泄露問題,在應用中只要使用一次,內存就不會被釋放。一般的作法是爲 webView 單獨開一個進程,使用 AIDL 與應用的主進程進程通訊。webView 進程能夠根據業務的需求,在合適的時機進行銷燬。

 

 參考文獻:

一、《Android進階解密》

二、Android內存泄漏的八種可能 

相關文章
相關標籤/搜索