Android 內(nèi)存優(yōu)化篇 - 使用profile 和 MAT 工具進(jìn)行內(nèi)存泄漏檢測

前言

在 Android 開發(fā)中,內(nèi)存泄漏這個(gè)名詞我想大家都不陌生,但是真正注意到這個(gè)問題并去解決的估計(jì)很少,因?yàn)閮?nèi)存泄漏表面上并不會表現(xiàn)出對app的任何影響,加之現(xiàn)在的手機(jī)配置與內(nèi)存都挺高的,所以對于中小型app來說,可能不怎么去處理也幾乎看不出來,但是作為一名android 開發(fā)者,你肯定和我一樣不能忍受這種瑕疵吧,那 就擼起袖子干它就完事了

內(nèi)存抖動 & 內(nèi)存泄漏 & 內(nèi)存溢出(OOM)

內(nèi)存抖動

含義:短時(shí)間內(nèi)有大量對象創(chuàng)建銷毀,它伴隨著頻繁的GC。

  1. 查看:可以使用android studio自帶的profile工具檢測。

  2. 現(xiàn)象:在profile中的內(nèi)存圖像就像是心電圖一樣,忽上忽下,如下圖所示:

image
  1. 常見場景:循環(huán)使用字符串拼接,比如我們項(xiàng)目的日志打印等

  2. 預(yù)防內(nèi)存抖動方法:

  • 避免在循環(huán)中創(chuàng)建對象,能復(fù)用的盡量復(fù)用。
  • 避免在頻繁調(diào)用的方法中創(chuàng)建對象,如自定義view中的onDraw()等方法中創(chuàng)建畫筆。
  • 獲取對象盡量從對象池中獲取,如Handler獲取Message對象應(yīng)使用obtain()方法獲取了。

內(nèi)存泄漏

程序中己動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi)。
長生命周期對象持有短生命周期對象強(qiáng)引用,從而導(dǎo)致短生命周期對象無法被回收!

  1. 查看:使用profile工具檢測內(nèi)存情況,重復(fù)執(zhí)行進(jìn)入然后退出一個(gè)activity,看activity實(shí)例是否還存在。如果activity實(shí)例還存在,很可能就出現(xiàn)了內(nèi)存泄漏。

  2. 現(xiàn)象:反復(fù)進(jìn)入A,然后退出A ,執(zhí)行三次,可以看到A 的實(shí)例存在兩個(gè)。如下圖,VideoPlayerActivity:

image

這就說明我們的activity并沒有被銷毀,至少目前是這樣的。至于究竟會不會內(nèi)存泄漏,就需要接下來使用另一款工具配合使用了。

  1. 如何判斷內(nèi)存泄漏:
  • 使用可達(dá)性分析法

通過一系列稱為“GC Roots”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)向下搜索,搜索所有的引用鏈,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達(dá))時(shí),則證明此對象是不可用的。也就會被回收。

image
  1. 何為GC Roots 對象,一般靜態(tài)變量就是gc root對象,可以理解成生命周期很長的對象。

  2. 如何預(yù)防內(nèi)存泄漏:

  • 使用 軟引用、弱引用間接的持有對象的引用。

  • 軟引用:

定義一些還有用但并非必須的對象。對于軟引用關(guān)聯(lián)的對象,GC不會直接回收,而是在系統(tǒng)將要內(nèi)存溢出之前才會觸發(fā)GC將這些對象進(jìn)行回收。

image
  • 弱引用 :

同樣定義非必須對象。被弱引用關(guān)聯(lián)的對象在GC執(zhí)行時(shí)會被直接回收。

image
  1. 造成內(nèi)存泄漏的常見場景:
  • 使用集合時(shí),例如add一個(gè)監(jiān)聽器,我們必須要手動remove掉。
  • 使用靜態(tài)成員變量/單利對象時(shí),如果持有短生命周期對象的引用(Activity)將導(dǎo)致短生命周期對象無法被釋放。
  • 進(jìn)行文件io操作時(shí),沒有close()。最好寫在finally{ }里面;
  • android 系統(tǒng)bug、第三方類庫造成的內(nèi)存泄漏。

內(nèi)存溢出

內(nèi)存溢出(Out Of Memory,簡稱OOM)是指應(yīng)用系統(tǒng)中存在無法回收的內(nèi)存或使用的內(nèi)存過多,最終使得程序運(yùn)行要用到的內(nèi)存大于能提供的最大內(nèi)存。此時(shí)程序就運(yùn)行不了,系統(tǒng)會提示內(nèi)存溢出,有時(shí)候會自動關(guān)閉軟件,重啟電腦或者軟件后釋放掉一部分內(nèi)存又可以正常運(yùn)行該軟件

  • 頻繁的出現(xiàn)內(nèi)存抖動或者大量內(nèi)存泄漏很有可能就會導(dǎo)致內(nèi)存溢出(OOM)。

Android 中的垃圾回收器 CMS

android 中使用的垃圾回收器 叫做CMS ,下面簡單介紹下他的垃圾回收算法。

  • 新生代對象

新生代對象采用的是復(fù)制算法,當(dāng)大對象也可能直接進(jìn)入老年代。

  • 老年代對象

老年代對象采用的是標(biāo)記-清除算法,所以頻繁的內(nèi)存抖動,會造成內(nèi)存碎片化,最后可能我們需要加載一個(gè)大對象的時(shí)候,就OOM 了。

簡單介紹下CMS垃圾回收算法,如果不熟悉的建議請先百度jvm 垃圾回收機(jī)制相關(guān)知識。

實(shí)戰(zhàn) 內(nèi)存泄漏

配置環(huán)境

  • android studio
  • eclipse memory analyzer (mat)

下載mat

選擇相應(yīng)版本進(jìn)行下載安裝。

image
  • 配置mat 環(huán)境,因?yàn)閺?android profile直接獲取到的hprof文件格式與mat的格式不兼容,所以需要使用工具轉(zhuǎn)換一下

win 環(huán)境配置,請自行百度,由于本人用的Mac 所以這里只寫Mac的配置

  1. 打開終端輸入:echo $HOME
  2. 繼續(xù)輸入:touch .bash_profile
  3. 繼續(xù)輸入: open -e .bash_profile
  4. 在打開的bash文件中輸入: export PATH=${PATH}:/Users/用戶名/你的sdk路徑/platform-tools
  5. 最后輸入: source .bash_profile
  6. 沒有6了,已經(jīng)成功配置了。

使用profile獲取內(nèi)存分析文件

image
image

名字隨便了,怎么方便怎么來

打開終端,進(jìn)行文件轉(zhuǎn)換

轉(zhuǎn)換格式 : hprof-conv before.hprof after.hprof

我們這里輸入 : hprof-conv memory-99.hprof 66.hprof
能看懂吧,吧我們的源文件 -99 轉(zhuǎn)換成 -66文件,

注意了 : 需要進(jìn)入-99 所在的文件目錄,要不然會報(bào)錯找不到文件

打開mat工具,導(dǎo)入我們的-66 文件

image

打開后可以看到這樣的界面

image

點(diǎn)擊紅框的選項(xiàng),這個(gè)是進(jìn)行內(nèi)存泄漏分析的

下面就是這段時(shí)間所產(chǎn)生的對象,點(diǎn)擊紅框 可以直接搜索你要分析的對象

image

這里找到了我們的VideoPlayerActivity

鼠標(biāo)右鍵選擇

image

可以看到,意思就是我們排除掉軟、弱、虛引用,因?yàn)檫@幾種是不會造成內(nèi)存泄漏的,可以不用管它,我們只需要看排除后還有沒引用存在,有的話 那就是強(qiáng)引用了,也就發(fā)生了內(nèi)存泄漏了。

繼續(xù)看我們的結(jié)果:

點(diǎn)擊后,發(fā)現(xiàn)里面存在數(shù)據(jù),那就說明我們有內(nèi)存泄漏發(fā)生了,也就是為什么上面我們已經(jīng)退出了,profile里面還有三個(gè)activity的存在,剛剛上面那張圖右側(cè)也有顯示有3個(gè)activity對象存在。

我們一一展開,看看到底哪里內(nèi)存泄漏了

image

可以看到,我們的Avtivity 作為mContext 變量被我們的自定義CoverVideoPlayerView 持有了,那也就是說,因?yàn)槲覀兊淖远xView不能被gc回收,所以activity也無法被回收。

那就繼續(xù)看,為什么自定義view無法被回收,可以看到,this$0,這表示在自定義view的內(nèi)部有一個(gè)非靜態(tài)內(nèi)部類,而非靜態(tài)內(nèi)部類是默認(rèn)持有外部類的引用的,也就是我們的,mNetChangeListener對象,這個(gè)就熟悉了吧,肯定是new 一個(gè)匿名內(nèi)部類啊,

繼續(xù)看,這個(gè)內(nèi)部類又被NetInfoModeule引用了,我丟,然后繼續(xù)往上看,我就不看了,你自己看吧

我去喝杯水,你慢慢看吧

啥? 我喝完回來了,你還沒看完呢? 那還是我們一起看吧,

繼續(xù)往上的話,可以看到 NetInfoModeule又被什么xxxxxBroadcastReceiver 引用了,那不就是廣播嗎? 猜想,肯定又是一個(gè)非靜態(tài)內(nèi)部類了,在往上看

咳咳,別看了,上面和我們沒啥關(guān)系了,全是系統(tǒng)在搞事情了,

通過上面一波分析,我們應(yīng)該清楚了,泄漏的原因了,

然后梳理一下:

VideoPlayerActivity -> mContext -> CoverVideoPlayerView -> mNetChangeListener -> NetInfoModeule -> xxxxxBroadcastReceiver;

引用鏈找到了,所以,我們把鏈條給整段了,就不會內(nèi)存泄漏了啊

怎么段呢?

最簡單的方式: 假如啊 ,這些個(gè)不靠譜的代碼都是你自己寫的,那好辦

在activity 的onDestory()方法中加上,

1.CoverVideoPlayerView.;

2.在CoverVideoPlayerView方法中,添加個(gè)cancel()方法,
加上  
  mContext = null
  mNetChangeListener = null;
  NetInfoModeule = null;
  然后廣播記得unregister()一下,這樣不就完事了。
 

嗯,一切都入想象的那般甜美,but,殘酷的事實(shí)擺在眼前,這特么是第三方庫的代碼,你咋改?
下源碼改? 得 ,可行是可行,那要是很多個(gè)庫呢? 你還下不?還改不?

算了,不改了,嗯也行,反正也好像沒啥影響,而且我們的VideoPlayerActivity用的SingleTop 啟動模式,
但是你如果用戶不是在播放頁面點(diǎn)擊跳轉(zhuǎn)的呢,退出再進(jìn)來,退出再進(jìn)來,最后十幾個(gè)activity的實(shí)例,
連帶著我們的model 、viewmodel 、統(tǒng)統(tǒng)都是十幾份,你確定你不會被老大拿出去祭天?

界面如下:

image

那要怎么改呢? 你說說唄,

改當(dāng)然能改了,那就是用java反射,通過反射去拿到對象,進(jìn)行修改,

直接上代碼吧

  /**
     * 利用反射 解決gsy庫中導(dǎo)致的內(nèi)存泄漏
     */
    public void cancel()
    {
        mAudioManager.abandonAudioFocus(onAudioFocusChangeListener);
        try
        {
            // 拿到NetInfoModule對象中 mConnectivityBroadcastReceiver字段.
            Field mConnectivityBroadcastReceiver = NetInfoModule.class
                .getDeclaredField("mConnectivityBroadcastReceiver");
            // 由于是私有字段,所以需要調(diào)用setAccessible(true),否則會報(bào)錯
            mConnectivityBroadcastReceiver.setAccessible(true);
            // 根據(jù)當(dāng)前mNetInfoModule對象的 mConnectivityBroadcastReceiver字段值為null
            mConnectivityBroadcastReceiver.set(mNetInfoModule, null);
            Field mNetChangeListener =
                NetInfoModule.class.getDeclaredField("mNetChangeListener");
            mNetChangeListener.setAccessible(true);
            mNetChangeListener.set(mNetInfoModule, null);
            
        }
        catch (NoSuchFieldException e)
        {
            e.printStackTrace();
        }
        catch (IllegalAccessException e)
        {
            e.printStackTrace();
        }
        mAudioManager = null;
        mContext = null;
    }
    
    

好了,改完了,我們在來抓一下內(nèi)存情況

直接上圖:

image

看見沒有,啥也沒有了,說明我們的activiy再沒有被其他對象引用了,說的不對,糾正一下,是沒有被其他對象強(qiáng)引用了,而只要沒有強(qiáng)引用關(guān)系,gc肯定能夠回收的,自然你就也不需要擔(dān)心內(nèi)存泄漏了,

但是為什么還是會顯示有3個(gè)activity對象存在呢?

image

那我們排除一下弱應(yīng)用,看一看

image
image

看見沒有,這里有一個(gè)FinalizeRefernce對象,說明我們的activity被FinalizeRefernce對象所引用,而我們知道,只要一個(gè)對象將要被gc回收了,那么他就會被這個(gè)FinalizeRefernce所引用,這是為了讓gc知道我們不需要這個(gè)對象了,你可以回收了,

所以我們的activity 雖然顯示有兩個(gè),但是只是gc還沒來的及回收而已,并沒有內(nèi)存泄漏的風(fēng)險(xiǎn)。

上面的例子是我的開源項(xiàng)目里的真實(shí)實(shí)例,感興趣的可以前往支持 start 一下,

項(xiàng)目地址

總結(jié)

內(nèi)存抖動的分析用profile工具完全就夠用了,當(dāng)然我們這里沒有詳講怎么用profile分析解決內(nèi)存抖動,之后有時(shí)間在補(bǔ)上吧。而我們的內(nèi)存泄漏通過profile是不能看出他到底有沒有發(fā)生內(nèi)存泄漏的,所以我們還需要借助mat工具進(jìn)一步分析。

當(dāng)然還有一些第三方的檢測庫,比較知名的比如leakCannary工具,騰訊的xxxdog,但是個(gè)人還是推薦使用mat來分析,mat操作起來也很方便,好了,今天就到這吧,感謝!

歡迎關(guān)注作者darryrzhong,更多干貨等你來拿喲.

請賞個(gè)小紅心!因?yàn)槟愕墓膭钍俏覍懽鞯淖畲髣恿Γ?/h3>

更多精彩文章請關(guān)注

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容