1. 程式人生 > >Android產品研發(二十四)-->記憶體洩露場景與檢測

Android產品研發(二十四)-->記憶體洩露場景與檢測

上一篇文章中本文我們講解了一個Android產品研發中可能會碰到的一個問題:如何在App中儲存靜態祕鑰以及保證其安全性。許多的移動app需要在app端儲存一些靜態字串常量,其可能是靜態祕鑰、第三方appId等。在儲存這些字串常量的時候就涉及到了如何保證祕鑰的安全性問題。如何保證在App中靜態祕鑰唯一且正確安全,這是一個很重要的問題,公司的產品中就存在著靜態字串常量型別的祕鑰,所以一個明顯的問題就是如何生成祕鑰,保證祕鑰的安全性?上一篇文章中我們做了一個簡單的介紹。

本文我們將講解一下關於Android開發過程中常見的記憶體洩露場景與檢測方案。Android系統為每個應用程式分配的記憶體是有限的,當一個應用中產生的記憶體洩漏的情況比較多時,這就會導致應用所需要的記憶體超過這個系統分配的記憶體限額,進而造成了記憶體溢位而導致應用崩潰。在實際的開發過程中我們由於對程式程式碼的不當操作隨時都有可能造成記憶體洩露。

(1)什麼是記憶體洩露

當一個物件已經不需要再使用了,本該被回收時,而有另外一個正在使用的物件持有它的引用從而導致它不能被回收,這導致本該被回收的物件不能被回收而停留在堆記憶體中,這就產生了記憶體洩漏。

(2)系統分配的應用記憶體大小

ActivityManager的getMemoryClass()獲得內用正常情況下記憶體的大小
ActivityManager的getLargeMemoryClass()可以獲得開啟largeHeap最大的記憶體大小

ActivityManager activityManager = (ActivityManager)context.getSystemService
(Context.ACTIVITY_SERVICE); activityManager.getMemoryClass(); activityManager.getLargeMemoryClass();

需要指出的是這裡獲取的記憶體大小是JVM為程序分配的記憶體大小,而當我們的應用中存在多個程序的時候,該應用理論上的記憶體大小限制:

  • 應用記憶體 = 程序記憶體大小 * 程序個數

所以當我們應用需要較大記憶體的時候也可以考慮通過多程序的方式進而獲取更多的系統記憶體。

這樣獲取到的應用記憶體大小就是應用所能獲取到的最大記憶體大小,當應用需要更多記憶體以支援其執行的時候,系統無法為其分配更多的記憶體,這樣就造成了OOM的異常。

(3)記憶體洩露的常見場景

  • 非靜態內部類,靜態例項化
/**
 * 自定義實現的Activity
 */
public class MyActivity extends AppCompatActivity {

    /**
     * 靜態成員變數
     */
    public static InnerClass innerClass = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);

        innerClass = new InnerClass();
    }

    class InnerClass {

        public void doSomeThing() {
        }
    }
}

這裡內部類InnerClass隱式的持有外部類MyActivity的引用,而在MyActivity的onCreate方法中呼叫了

innerClass = new InnerClass();

這樣innerClass就會在MyActivity建立的時候是有了他的引用,而innerClass是靜態型別的不會被垃圾回收,MyActivity在執行onDestory方法的時候由於被innerClass持有了引用而無法被回收,所以這樣MyActivity就總是被innerClass持有而無法回收造成記憶體洩露。

  • 不正確的使用Context物件造成記憶體洩露
/**
 * 自定義單例物件
 */
public class Single {
    private static Single instance;
    private Context context;
    private Object obj = new Object();

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

    /**
     * 初始化獲取單例物件
     */
    public static Single getInstance(Context context) {
        if (instance == null) {
            synchronized(obj) {
                if (instance == null) {
                    instance = new Single(context);
                }
            }
        }
        return instance;
    }
}

我們通過懶漢模式建立單例物件,並且在建立的時候需要傳入一個Context物件,而這時候如果我們使用Activity、Service等Context物件,由於單例物件的生命週期與程序的生命週期相同,會造成我們傳入的Activity、Service物件無法被回收,這時候就需要我們傳入Application物件,或者在方法中使用Application物件,上面的程式碼可以改成:

/**
 * 自定義單例物件
 */
public class Single {
    private static Single instance;
    private Context context;
    private Object obj = new Object();

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

    /**
     * 初始化獲取單例物件
     */
    public static Single getInstance(Context context) {
        if (instance == null) {
            synchronized(obj) {
                if (instance == null) {
                    instance = new Single(context.getApplication());
                }
            }
        }
        return instance;
    }
}

這樣就不會有記憶體洩露的問題了。

  • 使用Handler非同步訊息通訊

在日常開發中我們通常都是這樣定義Handler物件:

/**
 * 定義Handler成員變數
 */
Handler handler = new Handler() {  
        @Override  
        public void handleMessage(Message msg) {  
            dosomething();  

        }  
    };  

但是這樣也存在著一個隱藏的問題:在Activity中使用Handler建立匿名內部類會隱式的持有外部Activity物件的引用,當子執行緒使用Handler暫時無法完成非同步任務時,handler物件無法銷燬,同時由於隱式的持有activity物件的引用,造成activity物件以及相關的元件與資原始檔同樣無法銷燬,造成記憶體洩露。
好吧,那麼如何解決這個問題呢?具體可以參考:Android中使用Handler造成記憶體洩露的分析和解決

  • 使用資原始檔結束之後未關閉

在使用一些資源性物件比如(Cursor,File,Stream,ContentProvider等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於Java虛擬機器內,還存在於Java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩露。

因為有些資源性物件,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性物件在不使用的時候,應該立即呼叫它的close()函式,將其關閉掉,然後再置為null.在我們的程式退出時一定要確保我們的資源性物件已經關閉。

/**
 * 初始化Cursor物件
 */
Cursor cursor = getContentResolver().query(uri...); 
if (cursor.moveToNext()) { 
    /**
     * 執行自設你的業務程式碼
     */ 
     doSomeThing();
}

這時候我們應當在doSomeThing之後執行cursor的close方法,關閉資源物件。

```
/**
 * 初始化Cursor物件
 */
Cursor cursor = getContentResolver().query(uri...); 
if (cursor.moveToNext()) { 
    /**
     * 執行自設你的業務程式碼
     */ 
     doSomeThing();
}

if (cursor != null) {
    cursor.close();
}
  • Bitmap使用不當

bitmap物件使用的記憶體較大,當我們不再使用Bitmap物件的時候一定要執行recycler方法,這裡需要指出的是當我們在程式碼中執行recycler方法,Bitmap並不會被立即釋放掉,其只是通知虛擬機器該Bitmap可以被recycler了。

當然了現在專案中使用的一些圖片庫已經幫我們對圖片資源做了很好的優化快取工作,是我們省去了這些操作。

  • 一些框架使用了註冊方法而未反註冊

比如我們時常使用的事件匯流排框架-EventBus,具體的實現原理可參考:Android EventBus原始碼解析 帶你深入理解EventBus當我們需要註冊某個Activity時需要在onCreate中:

EventBus.getDefault().register(this);

然後這樣之後就沒有其他操作的話就會出現記憶體洩露的情況,因為EventBus物件會是有該Activity的引用,即使執行了改Activity的onDestory方法,由於被EventBus隱式的持有了該物件的引用,造成其無法被回收,這時候我們需要在onDestory方法中執行:

EventBus.getDefault().unregister(this);
  • 集合中的一些方法的錯誤使用

(1)比如List列表靜態化,只是新增元素而不再使用時不清楚元素;
(2)map物件只是put,而無remove操作等等;

(4)關於記憶體洩露檢測的兩個開源方案

在專案中使用到了兩個開源的記憶體洩露檢測庫:

推薦使用一下這兩個庫檢測一下專案,或許會有意想不到的收穫(曾檢測出一個主流第三方SDK的記憶體洩露BUG)。

關於LeakCanary,可參考我的:Android記憶體洩露監測之leakcanary,大概講解了一下LeakCanary的使用方式。

BlockCanary庫的使用方式和LeakCanary類似,更多關於其使用方式的介紹可檢視其github文件。

除了以上兩個開源庫之外,還可以考慮使用軟引用的方式,更多關於Java引用型別的知識,可參考我的:Java中的四種引用

(5)關於遮蔽記憶體洩露的建議

  • 正確的保證記憶體物件的生命週期,就是儘量保證記憶體物件在其生命週期內創建於結束,比如Android中的“上帝物件Context”,要保證不同的場景下使用不同的Context物件,下面是一張Context物件的使用場景圖:
    這裡寫圖片描述

  • 對資源物件的使用要在使用完成之後保證呼叫其資源的關閉方法,而非僅僅是對資源引用的關閉操作;

  • 靜態化資源物件其生命週期就會變成與程序的生命週期相同,在使用靜態化時一定要考慮清楚該物件靜態化是否存在記憶體洩露的可能;

  • 對Android開發中常見的記憶體洩露場景要做到了然於胸,瞭解一些Android中常見的記憶體洩露檢測方法;

總結:

關於記憶體洩露其實主要記住一個原則就好:確保物件能夠在正確的時機被回收掉。然後我們根據具體記憶體洩露的場景具體解決就好了。