1. 程式人生 > >Android記憶體洩漏問題分析及解決方案

Android記憶體洩漏問題分析及解決方案

大家新年好,由於工作繁忙原因,有好一段時間沒有更新博文了(當然Github是一直都有更新的),趁著年底有點放假時間,我覺得抽空更新下部落格,總結一下工作中最常見記憶體洩漏問題,也是自己之前踩過的坑,為了讓大家少走彎路,系統全面總結一下記憶體洩漏問題分析原因及尋找解決方案。


概念

首先要理解什麼叫做記憶體洩漏(Memory Leak),有很多人把記憶體洩漏和記憶體溢位(Out of Memory)混為一談,其實他們兩者並不相同。當然有些人可能“記憶體洩漏”打成“記憶體洩露”,個人認為前者更加準確地描述它本身的含義。 
大家都知道Android中的應用的最大使用記憶體是有限的,而不是可以佔完所有的手機剩餘記憶體(具體又有

Java Heap和Native Heap區別,而且又有版本的區別,所以這裡不做詳細敘述,可以查閱其他文章看看),而android採用的是記憶體自動回收機制,也就是對於不使用的記憶體,Android會自動回收等待複用。由於這兩個原因:如果分配記憶體超出了應用可以使用的最大記憶體,就會發生記憶體溢位;如果一個應該被回收的物件沒有被Android自動回收機制回收,這種情況稱為記憶體洩漏。

簡單總結為:

  • 記憶體洩漏(Memory Leak):應該被回收的物件沒有被回收
  • 記憶體溢位(Out of Memory):分配記憶體超出了應用可以使用的最大記憶體

值得注意,兩者並沒有必然關係

,記憶體洩漏不一定導致記憶體溢位,記憶體溢位時不一定發生了記憶體洩漏。


常見原因及解決方案

瞭解了記憶體洩漏的概念後,我們知道,如果一個物件分配記憶體使用完畢後並沒有被記憶體回收機制自動回收,這就會導致這個物件記憶體洩漏,下面來說說記憶體洩漏的幾種常見原因。

  1. 資源使用未關閉 
    這是最常見的記憶體洩漏原因之一,對於使用了檔案、圖片、廣播、資料庫指標、流等資源的時候,應該在使用完成後關閉資源,否則可能導致物件回收時無法回收記憶體。值得注意的是,呼叫關閉的位置不正確,也會導致記憶體洩漏。 
    比如說,下面一段程式碼:

    try {
        InputStream inputStream = new
    FileInputStream("path"); //inputStream.read();具體操作 inputStream.close(); } catch (IOException e) { e.printStackTrace(); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    初看這一段程式碼並沒有什麼問題,但是如果具體操作過程中出現了IOException,這樣最後的inputStream.close();就不會執行,導致流實際上並沒有關閉,從而導致使用流的物件最終無法回收。 
    這種問題解決方法應該把close方法放到finally裡面,保證其一定執行:

    InputStream inputStream = null;
    try {
        inputStream = new FileInputStream("path");
        //inputStream.read();具體操作
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  2. 單例物件成員為可回收物件 
    這種情況也是常見的記憶體洩漏原因,很多第三方SDK早期版本甚至最新版也存在這個問題。 
    最常見就是一個單例類持有Activity物件作為成員變數,例如下面程式碼:

    public class SampleClass {
    
        private Context context;
    
        //一堆單例程式碼...
    
        public void init(Context context) {
            this.context = context;
            //...
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上面這段程式碼,如果傳入的Context是Activity的話,就會導致Activity銷燬時候由於被其他物件所持有,自動回收機制不會回收這個物件,從而導致Activity記憶體洩漏。

  3. 外部類建立非靜態內部類的靜態例項 
    這個比較隱晦一些,也是經常會出現的錯誤。比如下面的程式碼

    public class SampleClass extends Activity{
    
        private static TestInner testInner;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            testInner = new TestInner();
        }
    
        class TestInner{}
    
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上述的TestInner生命週期比Activity長,Activity銷燬後,TestInner依然持有Activity的物件引用,導致Activity的記憶體無法回收。 
    正確的方法應該把TestInner改成靜態內部類,當然也可以把它抽取出來作為單例類。 
    這裡要注意了,有人說自己不會犯這種錯,但是有時候使用系統的某些物件作為靜態成員也會出現這種情況:

    最典型就是使用getDrawable獲得物件作為靜態成員變數,Drawable物件設定到View身上時候,會持有View物件,View物件則會持有Activity物件,而Drawable物件生命週期比Activity長,最後Activity銷燬後無法被自動回收。
    

    要注意處理上述的兩種情況,實質上匿名內部類也是一種非靜態的內部類,比如Handler洩漏、AsyncTask以及執行緒洩漏等等,這兩種是非常常見的。這些和上述的類似,Handler這個典型的問題,Android Studio中Lint還可以檢查出來,如果一定要使用Activity物件的話,可以採用WeakReference來處理。以Handler內部類為例:

    private static class MyHandler extends Handler { //注意這個是內部類
    
        private final WeakReference<Context> hContext;
    
        private MyHandler(Context hContext) {
            this.hContext = new WeakReference<>(hContext);
        }
    
        @Override
        public void handleMessage(Message msg) {
            //利用get()方法獲得例項,注意判斷非空
            Context context = this.hContext.get();
            if (context != null) {
                //...
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

檢測記憶體洩漏

除了上述的原因以外還有其他可能導致記憶體洩漏,而且我們不可能在實際開發中重新review一遍程式碼來解決記憶體洩漏,我們需要一些工具來幫助我們解決記憶體洩漏。 
這裡介紹一款我在實際專案中使用的工具Leakcanary,這個工具使用非常簡單:

  1. 首先新增Gradle依賴並且同步
  2. 在Application中註冊初始化,新增下面的程式碼

    LeakCanary.install(this);
    • 1
    • 1
  3. 然後就可以交給測試測試了,出現了問題會記錄成log,並且彈出對應問題,如下圖(借用下官方的圖)

    這裡寫圖片描述

這樣就可以定位到哪裡有記憶體洩漏了,更多使用方法可以參考官方的Github文件,這裡就不一一闡述了。


總結

到此為止我們瞭解了什麼是記憶體洩漏,記憶體洩漏原因以及如何檢測記憶體洩漏。 
實際上在實際開發專案中,大部分的錯誤都是上述的常見原因,所以開發的時候要注意:

  • 關閉使用了的資源
  • 不要隨便傳遞Activity,儘量使用Application Context或者使用弱引用
  • 謹慎使用statiic成員,使用的時候要注意生命週期問題

做到這些記憶體洩漏就不再是令人頭疼的問題。


宣告

原創文章,歡迎轉載,請保留出處。
有任何錯誤、疑問或者建議,歡迎指出。
我的郵箱:[email protected]