性能優(yōu)化實踐(四)-內(nèi)存優(yōu)化思考

一、應(yīng)用層

對應(yīng)用層來說,最主要的內(nèi)存問題還是內(nèi)存泄漏問題。

Java中的內(nèi)存分配

靜態(tài)儲存區(qū):編譯時就分配好,在程序整個運行期間都存在。它主要存放靜態(tài)數(shù)據(jù)和常量;
棧區(qū):當方法執(zhí)行時,會在棧區(qū)內(nèi)存中創(chuàng)建方法體內(nèi)部的局部變量,方法結(jié)束后自動釋放內(nèi)存;
堆區(qū):通常存放 new 出來的對象。由 Java 垃圾回收器回收。

Java中的內(nèi)存泄漏

root可達,但對象本身已無用。造成問題的原因是:生命周期長的對象持有了生命周期短對象的引用,即使生命周期短的對象完成了任務(wù),但是由于還繼續(xù)被持有引用,造成無法被回收。

四種引用類型的介紹

強引用(StrongReference):JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強引用的對象;
軟引用(SoftReference):只有在內(nèi)存空間不足時,才會被回的對象;
弱引用(WeakReference):在 GC 時,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內(nèi)存空間足夠與否,都會回收它的內(nèi)存;
虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作為GC回收Object的標志。

常見的內(nèi)存泄漏的場景包含如下幾類:

1 靜態(tài)造成的泄漏

靜態(tài)對象的生命周期與應(yīng)用程序一樣長,持有對象的引用基本都是泄漏,因為一般對象的生命周期都不會有它長。

舉例:

  • 單例造成的內(nèi)存泄露;
  • 靜態(tài)集合保存對象;
  • 全局靜態(tài)大對象。

解決方案:

  • 如果需要持有對象引用,將該對象的引用方式改為弱引用;
  • 如果需要持有Context,使用ApplicationContext;
  • 大對象本身盡量別做成靜態(tài)的

2 非靜態(tài)內(nèi)部類和匿名類造成的泄漏

在Java中,非靜態(tài)內(nèi)部類 和 匿名類 都會潛在的引用它們所屬的外部類,但是,靜態(tài)內(nèi)部類卻不會。如果這個非靜態(tài)內(nèi)部類實例做了一些耗時的操作,就會造成外圍對象不會被回收,從而導致內(nèi)存泄漏。

舉例:

這部分常見的有Thread、Handler、AsyncTask作為Activity內(nèi)部類使用時,當然也包括自定義的內(nèi)部類。

解決方案:

  • 將內(nèi)部類變成靜態(tài)內(nèi)部類;
  • 如果有強引用Activity中的屬性,則將該屬性的引用方式改為弱引用
  • 在業(yè)務(wù)允許的情況下,當Activity執(zhí)行onDestory時,結(jié)束這些耗時任務(wù);

3 Activity Context 的不正確使用

在Android應(yīng)用程序中通常可以使用兩種Context對象:Activity和Application。當類或方法需要Context對象的時候常見的做法是使用第一個作為Context參數(shù)。這樣就意味著View對象對整個Activity保持引用,因此也就保持對Activty的所有的引用。

假設(shè)一個場景,當應(yīng)用程序有個比較大的Bitmap類型的圖片,每次旋轉(zhuǎn)是都重新加載圖片所用的時間較多。為了提高屏幕旋轉(zhuǎn)是Activity的創(chuàng)建速度,最簡單的方法時將這個Bitmap對象使用Static修飾。 當一個Drawable綁定在View上,實際上這個View對象就會成為這份Drawable的一個Callback成員變量。而靜態(tài)變量的生命周期要長于Activity。導致了當旋轉(zhuǎn)屏幕時,Activity無法被回收,而造成內(nèi)存泄露。

解決方案:

  • 使用ApplicationContext代替ActivityContext,因為ApplicationContext會隨著應(yīng)用程序的存在而存在,而不依賴于activity的生命周期;
  • 對Context的引用不要超過它本身的生命周期,慎重的對Context使用“static”關(guān)鍵字。Context里如果有線程,一定要在onDestroy()里及時停掉。

下圖可以參考下什么場景下選擇ApplicationContext,建議是能用ApplicationContext的就不用ActivityContext:

4 代碼習慣

Cursor,Stream沒有close,Bitmap沒有recyle,注冊監(jiān)聽沒有取消注冊,集合對象不使用之后,內(nèi)部對象沒有及時清理掉, Adapter沒有使用convertView等等。

解決方案:按規(guī)范來。

5 大View造成的泄漏

這里所謂的大View比如:WebView,當我們不要使用WebView對象時,應(yīng)該調(diào)用它的destory()函數(shù)來銷毀它,并釋放其占用的內(nèi)存,否則其占用的內(nèi)存長期也不能被回收,從而造成內(nèi)存泄露。

解決方案:

  • 為webView開啟另外一個進程,通過AIDL與主線程進行通信,WebView所在的進程可以根據(jù)業(yè)務(wù)的需要選擇合適的時機進行銷毀,從而達到內(nèi)存的完整釋放。

VideoView也同樣如此,業(yè)務(wù)條件允許,盡量全局使用一個VideoView。

如果這些你都熟練于心的話,那在日常開發(fā)中就會避免掉不少主流的泄漏問題。未雨綢繆,防范于未然是非常重要的,畢竟內(nèi)存泄漏很大程度上就是一個量變引起質(zhì)變的問題。

6 謹慎使用三方庫

當然,避免歸避免,內(nèi)存泄漏還是有可能會發(fā)生的。發(fā)生之后如何分析?

常用的內(nèi)存泄漏排查流程:

1)adb shell dumpsys meminfo packageName

先通過dumpsys ,宏觀了解下當前進程的內(nèi)存具體分配情況,看看dalvik heap、native heap等等內(nèi)存占用情況,大致了解下泄漏發(fā)送在java層還是native層,或者graphic跟視圖、SurfaceView、圖片相關(guān)的內(nèi)存占用情況。

2)LeakCanary

用LeakCanary掃一遍,可以非常高效地先檢查出Activity/Fragment的內(nèi)存泄漏。觸發(fā)的點是:Activity/Fragment執(zhí)行onDestroy回調(diào),它的原理大概是:

當然,它無法覆蓋所有問題,最底層的activity因為很難調(diào)用onDestroy,所以檢測不到,這里倒是可以配合dumpsys meminfo的Objects部分通過back退出app,activitys 是否為0來幫助看最底層的activity是否有泄漏。另外,Service的內(nèi)存泄漏也無法檢測。

3)AS memory monitor / Heap Viewer

經(jīng)過LeakCanary的大掃除,大部分問題已經(jīng)被解決了,那么還剩下一部分問題如何處理呢?
如果你更喜歡視圖:AS memory monitor ,盡量使用AS 3.0之后的profiler。
如果你更喜歡數(shù)據(jù):Heap Viewer。
操作app,AS memory monitor 中視圖曲線是否存在不斷上升的趨勢且不會在程序返回時明顯回落?;蛘逪eap Viewer中free值是不是逐漸減少,兩種方式都提供手動GC按鈕,GC之后看內(nèi)存回調(diào)數(shù)值是不是期望的。對可疑點可以進行反復多次的操作,縮小懷疑范圍。

4)MAT

dump hprof文件,一般我喜歡再轉(zhuǎn)一下格式,用MAT打開分析,不轉(zhuǎn)的話只能用AS分析,但是MAT功能更全面。一般會生成兩份hprof,一份是正常情況的,一份是懷疑存在泄漏的,針對懷疑的點進行對比,是否有泄漏。

主要看:
with outgoing references: 查看它所引用的對象
with incoming references: 查看它被哪些對象引用

然后排除掉軟弱虛引用,剩下的再去分析是否存在泄漏,如果沒有泄漏,那再重復懷疑重復分析,具體分析MAT工具篇有詳細說明。

泄漏的問題基本上就到這了,下面我再分析下應(yīng)用程序其他的內(nèi)存問題:

內(nèi)存抖動:短時間內(nèi)大量創(chuàng)建和回收對象,造成頻繁GC,Art虛擬機在Dalvik的基礎(chǔ)上針對GC做了優(yōu)化,GC效率提高了2-3倍,且縮短了線程暫停時間,偶爾進行的GC暫停所有工作線程的時間幾乎無感覺,但是如果頻繁GC的話,那是有可能影響到用戶體驗的。

優(yōu)化策略:

  • 避免不必的對象開銷。
  • 對于就是頻繁需要創(chuàng)建和使用對象的場景下,引入池的概念,或則設(shè)計模式中的享元模式都是可以考慮使用的。

內(nèi)存瘦身
說白了就是減少不必要的內(nèi)存開銷,首當其沖的就是資源瘦身,也變相的是APK瘦身,能用代碼實現(xiàn)的視圖就盡量用代碼,不得不用圖片的,在保證一定清晰度要求情況下盡量減少圖片大小,Linit檢測去掉不必要的代碼,三方庫,用不到的代碼也能去掉的盡量去掉(同時還減少了方法數(shù))等等吧。其次緩存優(yōu)化,很多時候為了提升效率,會把部分數(shù)據(jù)緩存在內(nèi)存中,包括圖片,不是調(diào)用很頻繁的完全可以以文件的形式保存到硬盤上。另外能用生命周期短的對象就不用長的,這樣能更快被回收。

二、Java Framework

這一層主要是從進程調(diào)度的角度來優(yōu)化內(nèi)存,詳細內(nèi)存之前《Android進程管理篇(三)-AMS進程調(diào)度》做過詳細分析。

作為系統(tǒng)層,并不是內(nèi)存越低越好,內(nèi)存低往往體驗也會差。大部分時候都是在用戶體驗和剩余內(nèi)存上做博弈。這里我主要總結(jié)下低內(nèi)存手機的內(nèi)存優(yōu)化思路:

1 lmk:調(diào)整合適的lmk水線,及時在內(nèi)存低的時候殺掉部分進程騰出內(nèi)存。減少cache進程緩存數(shù),只要活著就會占內(nèi)存,犧牲啟動速度來換取內(nèi)存。b-service級別進程在內(nèi)存偏低時,降級為更低的cache級別,讓lmk能殺到更多進程。為某些占用內(nèi)存大的高日活應(yīng)用增加白名單,啟動時,默認清掉所有cache進程等等。

2 調(diào)整虛擬機能分配給進程的內(nèi)存大小 dalvik.vm.heapgrowthlimit 和 dalvik.vm.heapsize

等等

三、Kernel層

這層主要做的優(yōu)化大部分是提高系統(tǒng)剩余內(nèi)存,也總結(jié)幾個優(yōu)化點吧:

1 可以針對文件頁和匿名頁做回收,在lru鏈表中,把不活躍鏈隊尾的頁盡量回收掉。提高swapiness值來傾向于回收更多匿名頁,這樣能增大Free RAM。

2 針對低內(nèi)存手機,都會打開zram。Android 4.4 推出ZRAM內(nèi)存壓縮技術(shù). zram是內(nèi)核的一個模塊,是內(nèi)存管理的一種機制??梢詫⒔粨Q出去的頁按照1:4的比例進行壓縮存儲,減小內(nèi)存占用。壓縮的區(qū)域依然是內(nèi)存,并不是硬盤。利用這一特性,我們可以將不經(jīng)常使用的進程占用內(nèi)存盡可能的放在ZRAM,實現(xiàn)占用盡量少的內(nèi)存,占用盡量多的緩存,節(jié)省內(nèi)存;保證前臺的速度,前臺的應(yīng)用或者關(guān)鍵應(yīng)用不放在ZRAM中。

3 cat proc/meminfo 時候發(fā)現(xiàn)Mlocked值比較高,固化了部分文件頁,目的是對于頻繁使用的文件頁,系統(tǒng)會把他們鎖定到對應(yīng)的page頁中,減少io,方便使用,典型的空間換時間,比如google 的pinnerService就是這樣,建議關(guān)閉。

4 配置MALLOC_SVELTE 來disable tcache,即jemalloc不采用tcache方法分配內(nèi)存,這樣的好處是占用內(nèi)存少,但是分配內(nèi)存速度會變慢。

另外,cat proc/meminfo

Kernel內(nèi)存可使用內(nèi)存 ≈ Slab + KernelStack + PageTables。

Slab: 236424 kB
SReclaimable: 66836 kB
SUnreclaim: 169588 kB

如果Slab 與 SUnreclaim 都非常高,且一段時間后也沒有明顯回落的話,可能存在內(nèi)存泄漏。

可以通過page_owner工具抓出kernel的調(diào)用棧,用腳本捋出top問題,分析其調(diào)用棧來排查。

好了,先寫這么多。一些不成熟的小建議,歡迎補充。

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

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

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