1. 程式人生 > >Android效能優化之被忽視的Memory Leaks

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,更進一步引起效能問題。

總結起來其實很簡單:存在無效的引用!

記憶體洩露可以引發很多的問題,常見的記憶體洩露導致問題如下:

  • 應用卡頓,響應速度慢(記憶體佔用高時JVM虛擬機器會頻繁觸發GC);

  • 應用被從後臺程序幹為空程序;

  • 應用莫名的崩潰(也就是超過了HeepSize閾值引起OOM);

記憶體洩漏分析工具

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

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

  • Memory Analyzer Tool
  • LeakCanary

下面我們分開介紹。

Memory Analyzer Tool

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 GC按鈕手動觸發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

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

因為配置十分簡單,這裡就不多說了,官方文件。

我們看下分析結果

簡單直白!

常見記憶體洩漏情況

  • 構造Adapter時,沒有使用快取的 convertView

  • Bitmap物件不在使用時呼叫recycle()釋放記憶體

  • Context使用不當造成記憶體洩露:不要對一個Activity Context保持長生命週期的引用。儘量在一切可以使用應用ApplicationContext代替Context的地方進行替換。

  • 非靜態內部類的靜態例項容易造成記憶體洩漏:即一個類中如果你不能夠控制它其中內部類的生命週期(譬如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。原始碼

相關推薦

Android效能優化忽視Memory Leaks

起因 寫部落格就像講故事,得有起因,經過,結果,人物,地點和時間。今天就容我給大家講一個故事。人物呢,肯定是我了。故事則發生在最近的這兩天,地點在coder君上班的公司。那天無意中我發現了一個奇怪的現象,隨著我點開我們App的頁面,Memory Monitor

Android性能優化忽視Memory Leaks

轉換 sta 工具欄 ase service 超過 基本數據類型 是我 它的 起因 寫博客就像講故事。得有起因,經過,結果,人物。地點和時間。今天就容我給大家講一個故事。人物呢。肯定是我了。故事則發生在近期的這兩天,地點在coder君上班的公司。那天無

Android——效能優化SparseArray

相信大家都用過HashMap用來存放鍵值對,最近在專案中使用HashMap的時候發現,有時候 IDE 會提示我這裡的HashMap可以用SparseArray或者SparseIntArray等等來代替。 SparseArray(稀疏陣列).它是Android內部特有的api,標準的jdk是沒有這

Android效能優化較精確的獲取影象顯示到螢幕上的時間

轉載自:http://blog.desmondyao.com/android-show-time/ 這兩天我的包工頭歪龍木·靈魂架構師·王半仙·Yrom給我派了一個活:統計App冷啟動時間。這個任務看上去不難,但是要求統計出來的時間要準,要特別準。 意思就是,我必須要按Activity繪製到

Android效能優化圖片壓縮優化

1 分類Android圖片壓縮結合多種壓縮方式,常用的有尺寸壓縮、質量壓縮、取樣率壓縮以及通過JNI呼叫libjpeg庫來進行壓縮。 參考此方法:Android-BitherCompress 備註:對於資源圖片直接使用:tiny壓縮 2 質量壓縮(1)原理:保持畫素的前提下改變圖片的位深及透明度,(即:通

Android效能優化apk瘦身技巧

隨著專案迭代,新功能的增加。回導致apk越大。那麼在下載安裝過程中。使用者耗費的流量越多。 安裝等待的時間也會越長。這就意味著下載轉化率會越低。那麼如何apk瘦身呢? 理解APK結構 在討論怎麼減小Apk體積之前,理解一個應用的APK結構是非常有幫助的。一個ap

Android效能優化佈局優化

          佈局優化可以通過減少佈局層級來提高,儘量減少使用效能低的佈局,LineaLayout的效率最高,在可以使用LinearLayout或者RelativeLayout時,選擇LinearLayout。因為RelativeLayout測量較為複雜,需要測量水平和

Android效能優化實現擁有Looper的執行緒--HandlerThread

1 HandlerThread 1.1 定義   HandlerThread是能夠新建擁有Looper的Thread,這個Looper能夠用來新建其他的Handler。HandlerThread本質是一個執行緒,線上程內部,程式碼是序列處理的。(執行緒中

Android 效能優化String篇

Android 效能優化之 String篇 關於String相關知識都是老掉牙的東西了,但我們經常可能在不經意的String 字串拼接的情況下浪費記憶體,影響效能,也常常會成為觸發記憶體OOM的最後一步。 所以本文對String字串進行深度解析,有

Android效能優化:XML佈局檔案優化

Android中XML佈局檔案的使用非常頻繁,在載入XML佈局的時候,如果對XML檔案其進行優化,將會提高載入的效率。 HierarchyViewer工具 再開始介紹之前先說一下HierarchyViewer工具的使用。 不合理的佈局會使我們的應用程式

效能優化記憶體洩露(Memory Leak)解決

1 分析記憶體洩漏遇到的問題 (1)把兩個dump檔案對比,找出GC root樹,發現MainActivity例項被CommonUtil引用,說懷疑此處可能有洩露。但實際開發的時候,很多這種情況,莫非都要懷疑一遍?我們必然知道mat只是個工具,提供洩露的建議,

Android效能優化提高ListView效能的技巧

ListView優化一直是一個老生常談的問題,不管是面試還是平常的開發中,ListView永遠不會被忽略掉,那麼這篇文章我們來看看如何最大化的優化ListView的效能。 1.在adapter中的getView方法中儘量少使用邏輯 2.盡最大可能避免GC 3

Android 效能優化記憶體洩漏檢測以及記憶體優化(中)

Android 記憶體洩漏檢測   通過上篇部落格我們瞭解了 Android JVM/ART 記憶體的相關知識和洩漏的原因,再來歸類一下記憶體洩漏的源頭,這裡我們簡單將其歸為一下三類:自身編碼引起由專案開發人員自身的編碼造成;第三方程式碼引起這裡的第三

Android 效能優化記憶體檢測、卡頓優化、耗電優化、APK瘦身

導語 自2008年智慧時代開始,Android作業系統一路高歌,10年智慧機發展之路,如今 Android 9.0 代號P  都發布了,Android系統性能已經非常流暢了。但是,到了各大廠商手裡,改原始碼自定系統,使得Android原生系統變得魚龍混雜。另外,到了不同層次的

Android 效能優化記憶體洩漏的檢測與修復

在 Android 開發中, 記憶體優化是APP效能優化中很重要的一個部分. 而在記憶體優化中, 最重要的就是修復記憶體洩漏問題. 本文就來介紹一下記憶體洩漏的基本概念以及常用的檢測手段. 1. 什麼是記憶體洩漏 簡單來說, 當一個物件不再被使用時,

Android效能優化Android安裝包大小優化

  隨著應用業務功能的日益複雜,使用的第三方庫和SDK也會日益增加,這樣導致的直接後果就是安裝包的體積日漸增加,除了使用外掛化的手段,減少安裝包的體積,我們還可以從以下幾個方面著手,進行安裝包大小的優化。   程式碼混淆   ProGuard是一個開源的Java程式碼混淆器

Android效能優化 App啟動原理分析及速度和時間優化

應用的啟動速度緩慢這是很多開發者都遇到的一個問題,比如啟動緩慢導致的黑屏,白屏問題,大部分的答案都是做一個透明的主題,或者是做一個Splash介面,但是這並沒有從根本上解決這個問題。那麼如何從根本上解決這個問題或者做到一定程度的緩解? 一、應用的啟動方式 1、冷啟動:

Android 效能優化記憶體洩漏檢測以及記憶體優化(下)

Android 記憶體優化   上篇部落格描述瞭如何檢測和處理記憶體洩漏,這種問題從某種意義上講是由於程式碼的錯誤導致的,但是也有一些是程式碼沒有錯誤,但是我們可以通過很多方式去降低記憶體的佔用,使得應用的整體記憶體處於一個健康的水平,下面總結一下記憶

Android 效能優化應用啟動

寫在前面 最近工作轉到Android 效能優化方向,剛轉過來,相關經驗缺乏,紀錄一個目前讓人惱火的問題。非常遺憾,本文到目前為止還未能提供解決問題的優化方案,也沒有明確定位到導致效能問題的瓶頸所在。就像解數學題一樣,花費了大把時間,然並卵。之所以寫它

Android效能優化UI渲染優化

原文轉自:http://www.cnblogs.com/yezhennan/p/5442031.html UI效能測試 效能優化都需要有一個目標,UI的效能優化也是一樣。你可能會覺得“我的app載入很快”很重要,但我們還需要了解終端使用者的期望,是否可以去量化這些期望