Android 性能優化之內存洩漏,使用MAT&LeakCanary解決問題

2021-03-02 劉桂林

本文較長,閱讀大約5分鐘

App進行到最終的測試的時候,往往會出現一些性能上,以及內存上的問題,需要優化,這也是一個Android高級工程師所需要了解並且掌握的知識點,內存這個小妮子比較調皮,每個月總有那麼幾次洩漏或者溢出(OOM),這篇文章所講的是內存溢出,這裡要注意,內存溢出和內存洩漏是兩個概念,這點大家要清楚,當然,內存洩漏過多會導致內存洩漏,至於什麼是內存洩漏呢,大家都知道我們的內存回收機制是GC,所以用一句話來概括:GC回收機制所無法回收的垃圾對象。


如果把垃圾回收機制比喻你在用餐,而服務員會來收盤的話,那麼理想中的狀態便是你吃完飯一走,服務員就把盤子收走了,即對象用完GC自動回收,但是這裡卻只是理想中的樣子,實際上,對於Android的內存管理機制和回收機制,Android系統的一個內存管理機制,被稱為Low Memorry Killer的一種管理機制,其實就是根據優先級去kill掉一些優先級較低的程序,而回收機制就比較佛系了,叫做GC,採用的也是懶人機制,你不需要用的變量,對象等,你放那裡就好,系統會在Heap剩餘空間不夠的時候去回收,並且有一個隱患,即GC觸發後,所有的線程都會被暫停。


內存


要了解內存洩漏,我們首先了解內存,我們都知道Android系統的底層是Linux,並且他運行是一個沙箱機制,即每個App對應獨立運行在一個虛擬機中,並且有一個進程,這也延伸出了多任務機制,並且每個App都是獨立的,即使你崩潰了也不會對系統造成影響,如果想看進程,可以使用ps命令:

            

並且每個進程都有一個pid,按照順序分配的,可以發現,init進程就是第一個,這個我們不做深究,你知道有這麼一回事兒就行了


我們可以再次輸入一個命令:dumpsys meminfo packagename

            

這裡我們就可以看到更多的內存信息了,統計了一些物理內存,虛擬內存使用情況以及統計,裡面有三個參數,Heap Size , Heap Alloc ,Heap Free ,指分配了多少內存,使用了多少內存,剩餘多少內存,一般 Heap Size = Heap Alloc + Heap Free (1985 = 1374 + 611),這裡單位是K。


講完系統,我們再化大為小,說一下App,其實App在內存中安裝後,系統會預分配一個最大內存,這跟沙箱機制有一定關係,每家的系統都是不一樣的,我們可以通過代碼去讀取出來:


ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);

//最大內存

int mc = am.getMemoryClass();

//Large最大內存

int lm = am.getLargeMemoryClass();


這裡有一個Large,實際上我們之前就用過,也就是開啟硬體加速後的最大內存,如何開啟硬體加速,則需要在清單文件的Application根節點添加android:largeHeap="true"


不過大部分廠商會禁止此功能


我們把應用跑在模擬器上可以得到如下的數據:mc = 96 lm = 256


也得到了一個結論,即我這套模擬器給我的這個Demo App分配的最大內存為96,開啟硬體加速後為256,這裡的單位是M,但是大部分真機,這兩個數值都是一樣的,這裡不曾考究,自行探索下。


其實這裡要提一下,現在大部分的App其實所考慮的什麼所謂的內存優化,都是因為圖片過多,所以我們真正考慮的,還是如何有效率的優化圖片給App帶來的負荷,圖片吃內存比較大。


我們繼續來說一下Low Memorry Killer內存管理機制,這裡涉及要當你的App切入後臺後的管理方式,實際上可以用四個字概括:先進先出,當你的應用進入後臺並且開始啟動kill機制或者內存不夠的時候,會優先清理任務棧最底層的應用,也就是最先開啟的應用,而近期應用則相當於保護起來。這種機制叫做:LRU Cache (緩存淘汰)算法。


並且當系統內存存在變化的時候,可以通過Application的onTrimMemory方法監聽


@Override

public void onTrimMemory(int level) {

// level 等級

       super.onTrimMemory(level);

}


這裡的內存等級是這樣劃分的:


int TRIM_MEMORY_BACKGROUND = 40;

int TRIM_MEMORY_COMPLETE = 80;

int TRIM_MEMORY_MODERATE = 60;

int TRIM_MEMORY_RUNNING_CRITICAL = 15;

int TRIM_MEMORY_RUNNING_LOW = 10;

int TRIM_MEMORY_RUNNING_MODERATE = 5;

int TRIM_MEMORY_UI_HIDDEN = 20;


我們有好幾種方式可以監聽到內存的使用情況和波動,這裡我一一道來,首先,我們知道,當我們打開手機的設置 - 應用 - 對應的某一個App的時候就可以看到這個App的使用情況,實際上我們可以通過代碼的方式來獲取:


float totalMemory = Runtime.getRuntime().totalMemory() * 1.0f / (1024 * 1024);

float freeMemory = Runtime.getRuntime().freeMemory() * 1.0f / (1024 * 1024);

float maxMemory = Runtime.getRuntime().maxMemory() * 1.0f / (1024 * 1024);


這樣獲取到運行時的內存情況了,我們可以看下數據:


     


當然,你也可以通過Android Profile 查看


           


也可以通過Android Monitor查看

      

其實在面試中也經常有人會被問到內存優化的方法,只能說內存控制方面有很多的小技巧,但是歸根結底還是要你自己有一個良好的代碼習慣,當然,如果真發生了錯誤,比如內存洩漏或者溢出,那麼你也應該知道如何去解決這些問題。


內存洩漏


如果想解決內存洩露,那麼我們應該如何找到問題的根源尼?如果你只是一味的看內存增長是找不到問題所在的,應該內存洩漏如果不嚴重是察覺不到的,這裡我們可以來寫一段這樣的代碼:


public class MainActivity extends AppCompatActivity {


   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);


       findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {

           @Override

           public void onClick(View v) {

               finish();

           }

       });


       new Thread(new Runnable() {

           @Override

           public void run() {

               try {

                   Thread.sleep(15000);

               } catch (InterruptedException e) {

                   e.printStackTrace();

               }

           }

       }).start();

   }

}


這段代碼我只需要啟動後點擊退出按鈕,再啟動,再點擊,那麼Activity就會每次都finish掉,但是子線程卻一在運行,Runnable是持有Activity對象的,這樣我們就可以看到如下的Memory走勢圖

      

我反反覆覆的啟動後finish,最終的結果將原本15.7MB的內存變成25.2MB,並且還會無限增加,最終導致內存溢出


      

那好,如果項目龐大的話,光這樣看是定位不到的,我們可以這樣來:先點擊Profile app


       

     


然後在下面的Profiler中點擊Record,然後開始使用App,當看到波形變動的時候再點擊Stop


       

     


這樣就會出現如下的文件列表,這裡可以選擇按照包名分類

        

可以看到,這裡的MainActivity出現了3個實例,這肯定是有問題的,也就定位到了發生溢出的界面為MainActivity。但是到這裡還只是定位到了Activity,我們還可以更加精確一點,我們點擊Record按鈕旁邊的下載按鈕,然後點擊保存hprof文件

        

有了這個文件之後我們就可以進一步使用MAT工具來分析了


MAT

mat工具是Eclipse的,沒有的話可以到這裡去下載:


MAT工具下載:http://www.eclipse.org/mat/downloads.php


下載後如圖:

        

打開之後就是我i們久違的Eclipse風格了


但是這裡還不能直接導入,因為Android Studio導出的hprof文件並不是MAT標準的文件,所以我們需要用到SDK目錄下的platform-tools下hprof-conv.exe工具,在此目錄下進入cmd,通過命令:hprof-conv old.hprof new.hprof 來轉換文件:

        

現在我們可以回到MAT點擊菜單欄的File - Open Heap Dump 導入new.hpfof

        

只需要點擊Create a historam from an arbitray set of objects 也就是這個小圖標,即可生成分析表


       

     


然後我們在這裡輸入過濾:

           

到這裡就很明朗了,我們繼續縮小範圍


      

右鍵選擇 List object - with outgoing references ,這個的意思是查看外部所引用的對象

  

     


然後繼續過濾一下,並且右鍵 選擇 Merge Shortest Paths to GC Roots - exclude all phantom/weak/soft etc .references 這個的意思是排查所有的弱引用,虛引用

到這裡你是否有一種恍然大悟的感覺,我們過濾之後只剩下一條Thread的錯誤,而所指向的對象為this$0,也就是他本身,意思是 子線程中所持有本類對象,那聯想到內存溢出是我們退出後所引起的,所以最終得到的結論:Activity已經退出,但是子線程仍然持有本類對象所導致內存洩漏。


LeakCanary


當然,上述的方法我更多的傾向於你所了解這個一個追述的過程,畢竟有些繁瑣,所以這裡再教大家使用一款工具 —— LeakCanary


Github:https://github.com/square/leakcanary


我們在app/build.gradle下配置:


implementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'


最新的v2.0-alpha-1貌似有些問題,所以我還是使用穩定版本,在Application中增加


public class BaseApp extends Application {


   @Override

   public void onCreate() {

       super.onCreate();


       if (LeakCanary.isInAnalyzerProcess(this)) {

           return;

       }

       LeakCanary.install(this);

   }

}


這樣我們就可以正常的運行了,當發生內存洩漏的時候就會通知欄提示:


       

     


到這裡,基本上本章內容也講完了,當然,這也只是一些皮毛而已,當你的項目足夠大的時候,做這項優化工作還是比較繁瑣的,所以最好還是儘量保持良好的編碼習慣才是最重要的。


最新的精品文章我都會第一時間發表在我的星球中,文章內容比較紮實,歡迎加入!



我會繼續保持文章的更新,也會繼續給大家帶來高質量的精品,謝謝大家!

相關焦點

  • LeakCanary 一隻優雅的金絲雀
    通過它可以在 App 運行過程中檢測內存洩漏,當內存洩漏發生時會生成發生洩漏對象的引用鏈,並通知程序開發人員。說一個錦上添花的小知識點:17世紀,英國礦井工人發現,金絲雀對瓦斯這種氣體十分敏感。  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'}「That’s it, there is no code change needed!」
  • Android性能優化--內存優化
    本文來自Dotry投稿,連結:https://www.jianshu.com/p/38b627adaecd上一篇文章關於Android性能優化--啟動優化探討了啟動優化相關的知識點,在本篇將介紹內存優化的相關優化。
  • ...既然妹紙不在家,剛好最近一直在為項目做內存洩漏的優化工作,那...
    結果因為妹紙公司臨時有事,她不得不回公司一趟…然後我也只能宅家裡了,既然妹紙不在家,剛好最近一直在為項目做內存洩漏的優化工作,那就來寫一點個人總結好了。什麼是內存洩漏對於不同的語言平臺來說,進行標記回收內存的算法是不一樣的,像Android(Java)則採用GC-Root的標記回收算法。
  • 高頻面試點:Android性能優化之內存優化(上篇)
    眾所周知,內存優化可以說是性能優化中最重要的優化點之一,可以說,如果你沒有掌握系統的內存優化方案,就不能說你對Android的性能優化有過多的研究與探索。本篇,筆者將帶領大家一起來系統地學習Android中的內存優化。
  • Android性能優化:帶你全面實現內存優化
    本文主要講解性能優化中的內存優化,希望你們會喜歡目錄1.LeakCanary具體使用看內存洩漏那篇文章。總結本文主要講解內存優化的相關知識,總結如下:很多APP開發者,不在意一點一滴的內存洩漏。等到項目做大了,內存洩漏越來越多了,因內存洩漏而出現不可意料的問題或者內存警告,甚至APP閃退。這個時候他們就不得不去解決內存洩漏的問題了。
  • 老司機帶你摸懂LeakCanary
    LeakCanary洩漏目標推測LeakCanary想來也是我們的一個老朋友了,但是它是如何做到對我們的App進行內存洩漏分析的呢?這也是我們今天要去研究的主題了。我們要先思考的第一個問題也就是App中已經存在洩漏了,那我們該怎麼知道他洩漏了呢?
  • 最全的Android內存優化技巧
    本文主要介紹性能優化的一些手段,但是為了便於理解以及融會貫通,建議先了解Android內存管理機制,本文將從四個角度來介紹內存優化技巧減小對象的內存佔用儘量減少新分配出來的對象佔用內存的大小,使用更加輕量的對象1.
  • Android 內存洩漏探討
    除非在一些特定的場合,GC的執行影響應用程式的性能,例如對於基於Web的實時系統,如網路遊戲等,用戶不希望GC突然中斷應用程式執行而進行垃圾回收,那麼我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。同樣給出一個 Java 內存洩漏的典型例子,
  • 幾乎是史上最全最實用的Android性能全面分析與優化方案研究
    藉助性能優化工具分析解決問題性能優化指標性能問題分類1、渲染問題: 過度繪製、布局冗雜2、內存問題: 內存浪費(內存管理)、內存洩漏3、功耗問題: 耗電性能優化原則和方法2、優化方法了解問題(分為可感知和不可感知的性能問題):對於性能問題來講,這個步驟只適用於某些明顯的性能問題,很多無法感知的性能問題需要通過工具定位。例如:內存洩漏、層級冗雜、過度繪製等無法感知。滑動卡頓是可以感知到的。定位問題:通過工具檢測、分析數據,定位在什麼地方存在性能問題。
  • Android性能優化:手把手帶你全面實現內存優化
    最近有想換工作的同學們,可參考《5月技術崗位內推|RN開發招聘啦》,再往下看,一篇關於性能優化的好文章,很值得去學習。前言在 Android開發中,性能優化策略十分重要本文主要講解性能優化中的內存優化,希望你們會喜歡目錄
  • Android性能優化典範(三)
    4) The price of ENUMs 在StackOverFlow等問答社區常常出現關於在Android系統裡面使用枚舉類型的性能討論,關於這一點,Android官方的Training課程裡面有下面這樣一句話:
  • Android性能優化典範
    摘要:Google在Udacity上的《Android性能優化》在線課程詳細介紹了該如何優化性能,這些課程是Google之前在Youtube上發布的Android性能優化典範專題課程的細化與補充。本文是對渲染、運算、內存、電量四個篇章的學習筆記。
  • android內存優化總結
    之前做過公司產品的內存優化,不過時間有一段時間了,可能記憶不全,歡迎大家添加補充,有錯誤之處也方便指出。
  • Android性能優化典範(一)
    11) Performance Cost of Memory Leaks雖然Java有自動回收的機制,可是這不意味著Java中不存在內存洩漏的問題,而內存洩漏會很容易導致嚴重的性能問題。內存洩漏指的是那些程序不再使用的對象無法被GC識別,這樣就導致這個對象一直留在內存當中,佔用了寶貴的內存空間。
  • Android性能優化總結
    這是來自一位粉絲「MeloDev」的投稿,講真,我這裡投稿的不少,但是只有我自己覺得很不錯的才會通過,這篇文章我覺得對大家有用,而且性能優化也算是我面試必問的一個話題了,所以這裡推薦給大家。網絡:頻繁的網絡訪問會導致耗電和影響應用的性能;網絡交互數據大小會影響網絡傳輸的效率;5. 程序執行效率:糟糕的代碼會嚴重影響程序的運行效率,UI線程過多的任務會阻塞應用的正常運行,長時間持有某個對象會導致潛在的內存洩露,頻繁的IO操作、網絡操作而不用緩存會嚴重影響程序的運行效率。
  • Google 發布 Android 性能優化典範 - OSCHINA - 中文開源技術交流...
    課程專題不僅僅介紹了Android系統中有關性能問題的底層工作原理,同時也介紹了如何通過工具來找出性能問題以及提升性能的建議。主要從三個方面展開,Android的渲染機制,內存與GC,電量優化。下面是對這些問題和建議的總結梳理。0)Render Performance大多數用戶感知到的卡頓等性能問題的最主要根源都是因為渲染性能。
  • Linux如何調試內存洩漏
    內存洩漏是指由於疏忽或錯誤造成程序未能釋放已經不再使用的內存
  • Android 輕鬆解決內存洩漏
    內存洩漏的危害運行性能的問題: Android在運行的時候,如果內存洩漏將導致其他組件可用的內存變少,一方面會使得GC的頻率加劇,在發生GC的時候,所有進程都必須進行等待,GC的頻率越多,從而用戶越容易感知到卡頓。
  • 【221期】面試官:談談內存洩漏和內存溢出的聯繫與區別
    (memory leak)1、內存洩漏是指程序中已動態分配的堆內存由於某種原因未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統奔潰等嚴重後果。此外,內存洩漏不會直接產生可觀察的錯誤,而是逐漸積累,降低系統的整體性性能。4、如何有效的進行內存分配和釋放,防止內存洩漏,是軟體開發人員的關鍵問題,比如一個伺服器應用軟體要長時間服務多個客戶端,若存在內存洩漏,則會逐漸堆積,導致一系列嚴重後果。
  • 熬夜整理了一份Android高頻面試題集錦+開源框架實戰PDF
    Java1、Java的內存模型與線程的管理是怎樣的?2、DVM以及ART是如何對JVM進行優化的?你覺還有優化空間嗎?3、字節碼層面的class類文件結構是怎樣的?……2.Android性能優化(★★★★★)1、 進行Android app性能優化的方向是怎樣的?2,什麼情況下會導致性能優化,如何避免?3,leakCanary 用過沒有,它監控的原理是什麼?4,profile 熟練嗎?都可以用來分析哪些方面的性能?它的原理掌握嗎?5,面對UI卡頓,我們從哪些方面入手進行分析解決?