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)關於內存泄露檢測的兩個開源方案

在項目中使用到了兩個開源的內存泄露檢測庫:

LeakCanary
BlockCanary

推薦使用一下這兩個庫檢測一下項目,或許會有意想不到的收穫(曾檢測出一個主流第三方SDK的內存泄露BUG)。

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

BlockCanary庫的使用方式和LeakCanary類似,更多關於其使用方式的介紹可查看其github文檔。

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

(5)關於屏蔽內存泄露的建議

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

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

  • 靜態化資源對象其生命週期就會變成與進程的生命週期相同,在使用靜態化時一定要考慮清楚該對象靜態化是否存在內存泄露的可能;

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

總結:

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


另外對產品研發技術,技巧,實踐方面感興趣的同學可以參考我的:
Android產品研發(十三)–>App輪訓操作
Android產品研發(十四)–>App升級與更新
Android產品研發(十五)–>內存對象序列化
Android產品研發(十六)–>開發者選項
Android產品研發(十七)–>Hybrid開發
Android產品研發(十八)–>webview問題集錦
Android產品研發(十九)–>Android studio中的單元測試
Android產品研發(二十)–>代碼Review
Android產品研發(二十一)–>Android中的UI優化
Android產品研發(二十二)–>Android實用調試技巧
Android產品研發(二十三)–>Android中保存靜態祕鑰實踐


本文以同步至github中:https://github.com/yipianfengye/AndroidProject,歡迎star和follow


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章