天天看點

Android性能優化之被忽視的Memory Leaks

起因

寫部落格就像講故事。得有起因,經過,結果,人物。地點和時間。今天就容我給大家講一個故事。

人物呢。肯定是我了。

故事則發生在近期的這兩天,地點在coder君上班的公司。那天無意中我發現了一個奇怪的現象,随着我點開我們App的頁面,Memory Monitor中顯示占用的記憶體越來越多(前面的頁面已經finish掉了)。

咦?什麼鬼?

經過

有了問題就解決嘛,俗話說的好。有bug要上。沒有bug寫個bug也要上。那究竟是是什麼問題會引起這個現象呢?

Android中記憶體相關的問題無非就是這麼幾點:

  • Memory Leaks 記憶體洩漏
  • Memory Churn 記憶體抖動
  • OutOfMemory 記憶體溢出

阿西吧。細緻想想怎麼這麼像記憶體洩漏呢。那究竟是不是呢?那我們就一點一點分析一下呗。

記憶體相關資料

關于記憶體我們可能想了解的資料大概有三點:

  • 總記憶體

    private String getTotalMemory() { String str1 = "/proc/meminfo";// 系統記憶體資訊檔案 String str2; String[] arrayOfString; long initial_memory = 0; try { FileReader localFileReader = new FileReader(str1); BufferedReader localBufferedReader = new BufferedReader( localFileReader, 8192); str2 = localBufferedReader.readLine();// 讀取meminfo第一行,系統總記憶體大小 arrayOfString = str2.split("\\s+"); for (String num : arrayOfString) { Log.i(str2, num + "\t"); } initial_memory = Integer.valueOf(arrayOfString[1]).intValue() * 1024;// 獲得系統總記憶體,機關是KB,乘以1024轉換為Byte localBufferedReader.close(); } catch (IOException e) { } return Formatter.formatFileSize(getBaseContext(), initial_memory);// Byte轉換為KB或者MB,記憶體大小規格化 }

  • 系統目前可用記憶體

    private String getAvailMemory() { // 擷取android目前可用記憶體大小 ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); am.getMemoryInfo(mi); //mi.availMem; 目前系統的可用記憶體 return Formatter.formatFileSize(getBaseContext(), mi.availMem);// 将擷取的記憶體大小規格化 }

  • 我們能夠使用的記憶體

    每個Android裝置都會有不同的RAM總大小與可用空間。是以不同裝置為app提供了不同大小的heap限制。你能夠通過調用getMemoryClass())來擷取你的app的可用heap大小。假設你的app嘗試申請很多其它的記憶體,會出現OutOfMemory的錯誤。

    在一些特殊的情景下,你能夠通過在manifest的application标簽下加入largeHeap=true的屬性來聲明一個更大的heap空間。假設你這樣做,你能夠通過getLargeMemoryClass())來擷取到一個更大的heap size。

    然而。能夠擷取更大heap的設計本意是為了一小部分會消耗大量RAM的應用(比如一個大圖檔的編輯應用)。不要輕易的由于你須要使用大量的記憶體而去請求一個大的heap size。

    僅僅有當你清楚的知道哪裡會使用大量的記憶體而且為什麼這些記憶體必須被保留時才去使用large heap. 是以請盡量少使用large heap。使用額外的記憶體會影響系統總體的使用者體驗,而且會使得GC的每次執行時間更長。

    在任務切換時,系統的性能會變得大打折扣。

    另外, large heap并不一定能夠擷取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。是以即使你申請了large heap。你還是應該通過執行getMemoryClass()來檢查實際擷取到的heap大小。

    private String getAllocationMemory() { // 擷取系統配置設定的記憶體大小 ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); //開啟了android:largeHeap="true",米4系統能配置設定的記憶體為512M,不開啟為128M //return am.getLargeMemoryClass()+""; //return am.getMemoryClass()+""; }

Java中的四種引用

開始分析之前。有必要先了解下Java的記憶體配置設定與回收。

Java的資料類型分為兩類:基本資料類型、引用資料類型。

基本資料類型的值存儲在棧記憶體中,而引用資料類型須要開辟兩塊存儲空間。一塊在堆記憶體中。用于存儲該類型的對象;還有一塊在棧記憶體中。用于存儲堆記憶體中該對象的引用。

當中引用類型變量分為四類:

  • 強引用

    最經常使用的引用形式。把一個對象賦給一個引用類型變量,則為強引用。

    僅僅要一個引用是強引用,則垃圾回收器永遠都無法回收這個對象的記憶體空間,除非JVM終止。

  • 軟引用

    當記憶體資源充足的時候,垃圾回收器不會回收軟引用相應的對象的記憶體空間;但當記憶體資源緊張時,軟引用所相應的對象就會被垃圾回收器回收。

    //建立一個Student類型的軟引用 SoftReference<Student> sr = new SoftReference<Student>(new Student());

  • 弱引用

    無論JVM記憶體資源是否緊張,僅僅要垃圾回收器執行,弱引用所相應的對象就會被釋放。

  • 虛引用

    虛引用等于沒有引用,無法通過虛引用訪問其相應的對象。

    軟引用和弱引用在其對象被回收之後。這些引用會被加入到引用隊列中去;而虛引用在其對象被回收之前,虛引用就被加入到引用隊列中去了。是以虛引用能夠在其對象被釋放之前進行一些操作。

    虛引用和引用隊列綁定的方法:

    //建立引用隊列 ReferenceQueue<String> queue = new ReferenceQueue<String>(); //建立虛引用,并綁定引用隊列 PhantomReference<String> str = new PhantomReference<String>("啦啦啦",queue);

Garbage Collection Android中的垃圾回收

Android系統會在适當的時機觸發GC操作,一旦進行GC操作,就會将一些不再使用的對象進行回收。

執行GC操作的時候,全部線程的不論什麼操作都會須要暫停。等待GC操作完畢之後,其它操作才幹夠繼續執行。

通常來說,單個的GC并不會占用太多時間,可是大量不停的GC操作則會顯著占用幀間隔時間(16ms)。假設在幀間隔時間裡面做了過多的GC操作,那麼自然其它相似計算。渲染等操作的可用時間就變得少了

Memory Leaks記憶體洩漏

記憶體洩漏表示的是不再用到的對象由于被錯誤引用而無法進行回收。發生記憶體洩漏會導緻Memory Generation中的剩餘可用Heap Size越來越小,這樣會導緻頻繁觸發GC,更進一步引起性能問題。

總結起來事實上非常easy:存在無效的引用!

記憶體洩露能夠引發非常多的問題。常見的記憶體洩露導緻問題例如以下:

  • 應用卡頓。響應速度慢(記憶體占用高時JVM虛拟機會頻繁觸發GC);
  • 應用被從背景程序幹為空程序;
  • 應用莫名的崩潰(也就是超過了HeepSize門檻值引起OOM)。

記憶體洩漏分析工具

看到這些問題。突然發現好像離真相越來越近了0.0。

想要更加清楚地實時知曉目前應用程式的記憶體使用情況,我們須要通過一些工具來實作。比較好用的工具有兩種:

  • Memory Analyzer Tool
  • LeakCanary

以下我們分開介紹。

Memory Analysis Tools(​​點我下載下傳​​)是一個專門分析Java堆資料記憶體引用的工具,我們能夠使用它友善的定位記憶體洩露原因,核心任務就是找到GC ROOT位置。

接下來說下使用步驟。

抓取記憶體資訊

AndriodStudio中抓取記憶體資訊還是非常友善的,有兩種方法:

  • 使用Android Device Monitor

    點選Android Studio工具欄上的Tool–>Android Device Monitor

    在Android Device Monitor界面中選在你要分析的應用程式的包名,點選Update Heap來更新統計資訊,然後點選Cause GC就可以檢視目前堆的使用情況,點選Dump HPROF file,将該應用目前的記憶體資訊儲存成hprof檔案,放在桌面就可以。操作例如以下圖

  • 直接擷取

    Android Studio的最新版本号能夠直接擷取hprof檔案,可是注意在使用之前一定要手動點選 Initiate GCbutton手動觸發GC。這樣抓到的記憶體使用情況就是不包含Unreachable對象的。

    稍等片刻,生成的檔案會出如今captures中。然後選擇檔案,點選右鍵轉換成标準的hprof檔案。就能夠在MAT中打開了。

使用MAT工具檢視分析

這裡我寫了個簡單的demo來測試,這個demo一共同擁有兩個頁面,在跳轉到第二個頁面之後,新開一個現成去列印activity資訊。

/**
 * 列印ActivityName
 */

public void printActivityName() {

    for (int i = 0; i < 100; i++) {

        new Thread(new Runnable() {

            @Override

            public void run() {

                while (true)

                    try {

                        Thread.sleep(1000 * 30);

                        Log.e(ActivityHelper.class.getSimpleName(), ((Activity) mContext).getClass().getSimpleName());

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

            }

        }).start();

    }

}   
      

多次進入SecondActivity之後會發現記憶體一直在增長,并沒有減少。

而且log裡會不停的輸出log,列印目前activity的name。

在MAT中打開抓取到的檔案後如圖

MAT中提供了非常多的功能,這裡我們僅僅要學習幾個最經常使用的就能夠了。上圖最中央的那個餅狀圖展示了最大的幾個對象所占記憶體的比例,這張圖中提供的内容并不多。我們能夠忽略它。紅色框中有兩個非常實用的工具是我們經常使用的。

Histogram能夠列出記憶體中每個對象的名字、數量以及大小。

Dominator Tree會将全部記憶體中的對象按大小進行排序,而且我們能夠分析對象之間的引用結構。

我們先來看Histogram

我們應該怎樣去分析記憶體洩漏呢?

即分析大記憶體的對象。

可是假如我們有目标對象的話,左上角值支援正則表達式的,我們輸入SecondActivity。這裡我們看到。我們有5個SecondActivity的執行個體。由于我們引用SecondActivity的現成沒有銷毀。導緻會有非常多執行個體。

接下來對着SecondActivity右鍵 -> List objects -> with incoming references檢視詳細SecondActivity執行個體。例如以下圖所看到的:

假設想要檢視記憶體洩漏的詳細原因,能夠對着随意一個MainActivity的執行個體右鍵 -> Path to GC Roots -> exclude weak references,結果例如以下圖所看到的:

能夠看到紅色框中,由于我們的線程持有SecondActivity的執行個體。全部導緻記憶體洩漏。

此外。我們能夠選擇以我們項目的包結構的形式來檢視

接下來我們看下Dominator Tree。

關于Dominator Tree我們須要注意三點:

  • 首先Retained Heap表示這個對象以及它所持有的其它引用(包含直接和間接)所占的總記憶體,是以從上圖中看,前兩行的Retained Heap是最大的。我們分析記憶體洩漏時,記憶體最大的對象也是最應該去懷疑的。
  • 帶有黃點的對象就表示是能夠被GC Roots訪問到的,依據上面的解說,能夠被GC Root訪問到的對象都是無法被回收的。
  • 并非全部帶黃點的對象都是洩漏的對象,有些對象系統須要一直使用。本來就不應該被回收。我們能夠注意到。有些帶黃點的對象最右邊會寫一個System Class,說明這是一個由系統管理的對象,并非由我們自己建立并導緻記憶體洩漏的對象。

如今我們能夠對着我們想檢視的内容點選右鍵 -> Path to GC Roots -> exclude weak references,為什麼選擇exclude weak references呢?由于弱引用是不會阻止對象被垃圾回收器回收的。是以我們這裡直接把它排除掉。然後一步一步分析。

leakcanary是一個開源項目,一個記憶體洩露自己主動檢測工具,是著名的GitHub開源組織Square貢獻的,它的主要優勢就在于自己主動化過早的發覺記憶體洩露、配置簡單、抓取貼心,缺點在于還存在一些bug,隻是正常使用百分之九十情況是OK的,其核心原理與MAT工具相似。

由于配置十分簡單。這裡就不多說了,​​​​

我們看下分析結果

簡單直白!

常見記憶體洩漏情況

  • 構造Adapter時。沒有使用緩存的 convertView
  • Bitmap對象不在使用時調用recycle()釋放記憶體
  • Context使用不當造成記憶體洩露:不要對一個Activity Context保持長生命周期的引用。

    盡量在一切能夠使用應用ApplicationContext取代Context的地方進行替換。

  • 非靜态内部類的靜态執行個體easy造成記憶體洩漏:即一個類中假設你不能夠控制它當中内部類的生命周期(譬如Activity中的一些特殊Handler等)。則盡量使用靜态類和弱引用來處理(譬如ViewRoot的實作)。
  • 警惕線程未終止造成的記憶體洩露。譬如在Activity中關聯了一個生命周期超過Activity的Thread,在退出Activity時切記結束線程。一個典型的樣例就是HandlerThread的run方法是一個死循環,它不會自己結束,線程的生命周期超過了Activity生命周期。我們必須手動在Activity的銷毀方法中中調運thread.getLooper().quit();才不會洩露。
  • 對象的注冊與反注冊沒有成對出現造成的記憶體洩露。譬如注冊廣播接收器、注冊觀察者(典型的譬如資料庫的監聽)等。
  • 建立與關閉沒有成對出現造成的洩露;譬如Cursor資源必須手動關閉,WebView必須手動銷毀。流等對象必須手動關閉等。
  • 不要在執行頻率非常高的方法或者循環中建立對象(比方onmeasure),能夠使用HashTable等建立一組對象容器從容器中取那些對象。而不用每次new與釋放。
  • 避免代碼設計模式的錯誤造成記憶體洩露;譬如循環引用,A持有B。B持有C。C持有A,這種設計誰都得不到釋放。

結果

真相僅僅有一個,那就是确實是由于記憶體洩漏才出現我遇到的情況。程式猿嘛,誰還不踩個坑,跳出來,拍拍身上的灰塵。總結一下,過兩天又是一條棒棒的coder。​