您的位置:首頁技術文章
文章詳情頁

Android 內存暴減的秘密?!

瀏覽:20日期:2022-09-27 09:43:18
WeTest 導讀

在 我這樣減少了26.5M Java內存! 一文中內存優化一期已經告一段落,主要做的事情是,造了幾個分析內存問題的輪子,定位進程各種類型內存占用情況,分析了線程創建OOM的原因。當然最重要的是,優化了一波進程靜息態的內存占用(減少26M+)。而二期則是在一期的基礎之上,推進已發現問題的SDK解決問題,最終要的是要優化進程的動態Java內存占用!

通常來說不管是做什么性能優化,逃不出性能優化3步曲:

找到性能瓶頸 分析優化方案 執行優化

上述三步看似第三步最能決定優化結果,而事實上,從筆者的幾次性能優化經歷來看,找到瓶頸確占據了絕對的影響力!

● 能否找到瓶頸意味著優化做不做的下去。

● 找到的瓶頸性能越差意味著優化效果越明顯。

● 找到的瓶頸越多同樣意味著優化效果越好。

一、如何找瓶頸所在

在分析方法上,主要:

● 分析代碼邏輯,檢查有問題的邏輯,對其進行相關優化。

● 模擬用戶操作 在內存占用較高的時候dump內存,使用MAT分析

● 然后是分析HeapDump的方法

看 DominatorTree,確定占用內存最多的實例 通過 GC root輔助分析內存占用的來源 通過 RetainHeapSize 量化的分析內存占用

動態內存優化比靜態要更難,其難點在于動態二字之上。動態不僅是的查找瓶頸變得困難,也使得對比優化成果不顯而易見。而不同的環境、操作路徑、設備、使用習慣等各個因素都有可能導致內存占用的不同??赡艿那闆r是:找到的性能瓶頸和用戶實際操作的方式不同,導致不能解決外網的OOM。因此直接獲取手機用戶的真實數據則是最行之有效的一種方式。

因此輔助采取了另一種方式, 收集真實的用戶數據。

● 在手機發生OOM的時候dump內存,上傳到后臺,以便后續分析

措施1:可以優化現有代碼邏輯,針對內存占用過多/不合理的場景進行優化。這是主場景。

措施2:主要分析外網用戶的使用習慣下,發生OOM的場景。比較容易發現bug類問題導致瞬間內存占用過多的場景。

二、找到哪些瓶頸

找到的瓶頸問題很多,稍微按照分類梳理一下:

1. 加載進內存,實際上沒用到(還沒用到)的數據

1)PullToRefreshListView 的 Loading 和 Empty View lazyLoad,這是下拉刷新的組件,其下拉刷新有一個幀動畫,圖片較多,占用較多內存。

2)Minibar PlayListView。每個頁面都會有一個Minibar,但是不一定Minibar都會打開播放列表。

3)AsyncImageView 的 默認圖和失敗圖以Drawble的形式直接加載進內存的。

2、 UI 相關數據,未及時釋放

1)24 小時直播間數據,只在節目切換的時候才有用

2)彈幕,只在播放頁展示彈幕的時候才有用

3)播放頁 TransitionBackgroundManager 大圖內存占用問題 。這個一個大圖,為了做漸變動畫。

3、數據結構不合理,占用內存過多

1)播放歷史最多記錄600個節目信息,每一個ShowInfo占用內存多達22K(通過MAT查看RetainHeap)

2)下載管理會在內存中存儲用戶下載的 節目信息,歌詞,專輯信息,分別占用內存 12K, 0-10K, 12K。并且這里沒有數量限制。

4、 圖片占用內存過多

1)在應用主頁操作一下,發現圖片(Bitmap)占用的內存很多

2)高斯模糊圖片。

5、 bug類導致內存占用過多

播放歷史應為代碼邏輯bug,導致沒有控制記錄數量上限。于是用戶聽的節目越多內存占用就越大。這里的問題主要通過OOM上報發現,占用內存最多的一次上報,僅播放歷史記錄就占內存50M之多。

上述 1-4 點通過措施1主動檢查內存發現。而第5點則是在分析了OOM上報“意外”發現的,如果是通過措施1的方式,幾乎不可能知道這么多OOM竟然是因為這個問題引起的。

三、怎么優化瓶頸

找到問題之后,剩下的就是比較好做的了,只需順藤摸瓜,各個擊破!

1、懶加載 (LazyLoad)

針對上面的1.1, 1.2, 都可以做LazyLoad,真正需要下拉刷新/展示播放列表的時候再創建相關實例。

1.4 則可以在動畫結束之后清理掉相關Bitmap

1.3 會復雜一點。圖片加載組件可以提供default圖,在圖片加載過程中臨時展示;以及faild圖,在圖片加載失敗之后展示。這兩個圖在AsyncImageView中都是直接引用住圖片 (Drawable)的。事實上絕大多數場景都會顯示成功的圖片。因此這里的修改方式是:

AsyncImageView的 default/fail 圖片不再引用 drawable,而是引用資源ID,在需要的時候再由ImageLoader加載進內存,同時這些圖片將有ImageCache統一管理,并占用內存LRU空間(之前是由Resource管理)。

這里去掉了幾個大圖的內存占用。內存占用在幾M級別。

2、及時釋放

上面 2.1 中的24小時直播間的數據會一直在內存中,即使用戶當前沒有在聽24小時直播間。這個顯然是不合理的。

修改的做法是 業務數據緩存的DB中,在需要用到的時候從DB中查詢出來

2.2 的彈幕則是純粹的UI相關數據,在播放頁退出之后即可釋放了。

2.3 是為了動畫準備的一張大圖,為了做一個炫酷的動畫效果。事實上,在動畫結束之后,就可以釋放了。這個圖片占用的內存和手機分辨徐率相關,分辨率(嚴格來說是density)越高的手機,圖片尺寸越大。在主流手機上1080p約1M。

這里分別減少了 287K + 512K + 1M

3、 優化數據結構

3.1 和 3.2 都會存儲節目信息,而節目信息相關的jce結構都比較大,通過MAT,可以看到 Show:12K, Album:10K, 一個ShowInfo同時包含了上面兩種數據結構。

最合理的方式應該是:

數據存儲在DB 在需要數據的時候通過一次db查詢,拿到具體的數據。

但是因為現有代碼都是從內存中查詢,接口是同步的方式,全部改異步的成本會比較大,這里我們的時間成本和測試自由都有限。

綜合上面MAT分析的結果,有個思路:

內存中存儲 節目信息 (ShowMeta)最少的內存,例如: 節目名,節目id,專輯id 之類的信息。而真正的Show和Album結構存在DB中。

這樣內存中的數據可以盡量的少,同時大部分已有接口還可以保持同步調用的方式。

此外,從用戶的角度出發,假設一個重度用戶下載了1000個節目,那么每一個ShowMeta占用的內存都會被放大1000倍,因此載極限的優化ShowMeta都不為過。

這里做了兩件事:

1. 刪字段,把ShowMeta中的非必要字段刪掉。

比如其中的url字段,實際只用來通過hash生成文件名,我們完全可以用showId代替。而一個url長度可達500Byte,1000個ShowMeta的話,這里就能節省500K內存了!

再比如:dowanloadTaskId字段,是存儲下載任務的id的,在節目下載完成后,該字段即失去意義,因此可以刪除之。

2、 intern 這里是參考了 String.intern 的思路。不同的ShowMeta可能會有相同的字段,或者說字段中有相同的部分。

比如同一個專輯中的ShowMeta其albumId字段都會是相同的,我們只需要保留一份albumId,其他ShowMeta都可以用同一個實例。(內存優化一期對ShowList做了同樣的改造)

再比如:ShowMeta中會存儲下載文件的全路徑,而事實上所有節目都會存儲在同一個文件目錄中,因此這里把文件路徑拆成 目錄+文件名來存儲,而路徑采用 intern 的方式,保證了內存中只會有一份。

Android 內存暴減的秘密?!

優化前

Android 內存暴減的秘密?!

優化后

最直觀的看變化是內存占用從 14272B 到 120B。仔細看會發現 ShowRecordMeta 的retainHeap 不等于各字段內存占用之和,這是因為上面提到的 String intern 的作用,相同字段被復用了,因此這里的retainheap不準確,通過RecordDataManager/countof(records) 計算,平均每一個record 14800/60 = 247B,減少98%。

這里的修改結果:

播放歷史 ShowHistoryBiz -> ShowHistoryMeta 內存占用從 19k 到 約216B

下載記錄 ShowRecordBiz -> ShowRecordMeta 內存占用 從 14k 到 約100B

粗略估計,這里修改的播放歷史(每次播放都會增加一個記錄,上限600個),(19256-216)* 600 = 10.9M

和下載記錄(假設一個輕度使用用戶用戶下載100個節目),內存總共可以減少:

(14727-100)* 100 = 1.4M

如果是重度用戶,下載1000個節目,則有14M之多!

不得不說這是個很大的數字!

四、圖片內存

在Android 2.3 之后,Bitmap改了實現,圖片內存從native heap轉移到了Java heap。這就導致了JavaHeap占用暴增。(然而8.0又改成NativeHeap了,具體原因官方文檔并沒有提及,有待考察)。

通常我們分析 heap dump 的時候會發現Bitmap占用的內存是絕對的大頭。這次我們做內存優化也不例外。

這里的思路是分析內存占用是否合理:

是否所有圖片都用于界面展示 是否圖片尺寸過大。

首先,分析內存占用是否合理。經過一期的優化,在不打開MainActivity的時候,內存中幾乎沒有圖片。但是打開MainActivity之后,內存中會出現幾十兆的圖片內存。

圖片內存主要是用于展示的,也即:被AsyncImageView持有的部分。

另外是內存的圖片緩存,會持有 最大JavaHeap 1/8 的內存充當 Bitmap 緩存,使用LRU算法淘汰老數據。

當然另外一些圖片過大屬于使用不當,實際上可以裁剪才View實際的大小。

而一些全屏(和屏幕等寬的圖,主要是Banner)圖其實可以裁剪的更小一點(如3/4大?。p少近46%的內存占用,而觀感不會有特別明顯的區別。(寫這個文檔的時候突然想到的,TODO一下)。

問題1:針對AsyncImageView的問題,思考是否所有圖片都在用戶展示?

答案顯然是否定的,一部分圖片被ListView回收的view所持有,這些內存占用顯然是不合理的。

問題2:另外就是ViewPager這種多頁面視圖,給用戶展示的實際上只有一個,其他幾個視圖并沒有在展示,因此這里是否可以改造ViewPager呢?

針對第一個問題,被ListView回收的view仍然在內存中的問題,通過改造AsyncImageView,在View從windowdetach的時候,主動釋放Bitmap,attach到Window的時候再次嘗試加載圖片。另外是多圖滾動視圖,這里的圖片很大,因此占用內存也很多。因為歷史原因之前使用的是Gallery,其有bug導致會額外引用住兩個大圖(已經不可見),因此這里使用RecyclerView修改了其實現,解決上述問題。

針對第二個問題,目前還沒有采取有效措施,主要依賴Android系統,主動回收Activity的內存。(這里存疑,需要深挖系統代碼,理清理邏輯之后再下結論。短期的結論是:系統的清理行為不可靠)。如果要改的話,可以簡單的修改一下ViewPager的內存,保證在其他page不可見的時候,回收其相關的Fragment。留個TODO。

LRU + TTL

針對圖片緩存,這里本身只是緩存圖片并且有LRU算法保證不會超過最大內存,理論上內存占用合理。但是LRU算法有一個問題,就是一旦緩存滿了,后續只能通過添加新Bitmap才能淘汰掉老的Bitmap,而此時緩存占用的內存仍然是最大值。因此這里的思考是LRU+TTL算法:即在LRU的基礎上,指定每一個Bitmap在緩存中存在是有效時長。超過時長之后主動將其從緩存中清理掉。這樣我們就可以解決LRUcache占用的內存不可減少的問題。

再次感謝afc組件作者raezlu和筆者討論問題,欣然接受建議,并身體力行的實現了TTL方案!

高斯模糊

這里補充一個,關于高斯模糊圖片占用內存過高的問題,在之前版本已經優化過了。

因為高斯模糊的圖片本身會讓圖片變得模糊(廢話。。),因此圖片的信息實質上是丟失了很大一部分的。在此思路的基礎上,我們可以把需要高斯模糊的圖片先縮?。ū热?100x100),然后再做高斯模糊。這樣不僅減少了內存占用,同時高斯模糊處理的速度也可以大大增加!

比如,之前遇到播放頁封面cover圖 720 720的大小,占內存 720 720 4 = 2M,降低到 100x100 占用內存大小 100 100 * 4= 40K,內存優化效果明顯,而視覺上幾乎沒有差距。

五、其他優化

這里主要針對外網的TOP1 crash,WNS內部線程創建導致的OOM。

筆者的解決方案是先根據crash上報信息,深挖系統源碼《 Android 創建線程源碼與OOM分析 》,徹底理清楚線程創建邏輯,并最終確定crash原因是線程的無節制創建。然后針對crash,整理出詳細的原因分析,再給WNS的小伙伴提了bug,待修復之后替換sdk。

六、成果對比

內存優化的效果總體還不錯,這里一共做了兩期,優化了幾十個項目。首先要比較感謝項目組給了可觀的排期,這樣才有時間做一些比較深入的改動。

靜息態內存

一期優化效果是在Nexus6P@7.1上測試到的靜息態內存優化 26.5M。

二期又進一步做了優化(上文3.2 3.3節),現在靜息態內存再次dump會發現只有3M內存了,而這3M有一部分是播放列表,一部分是播放頁持有的小圖片。

通過計算,可以得出靜息態內存進一步減少了:

24小時直播間單例: 287K

彈幕manager 單例: 512K

播放頁動畫大圖:1M

播放歷史 600個(上限):(19256-216) * 600 = 10.9M

下載記錄 下載100個節目:(14727-100)* 100 = 1.4M

總共減少: 28M+

動態內存

動態內存比較不好對比,這里決定采用黑盒測試的方式:

打開應用,MainActivity各個tab操作一遍,打開播放頁,然后對比內存占用量。鑒于筆者只有一臺Nexus6P開發機,為了控制變量,這里創建了兩臺模擬器,并排擺放,分別打開企鵝FM4.0和3.9版本,確保使用相同的操作路徑。

這里測試了兩種場景:

應用新安裝 老用戶,聽了很多節目(播放歷史600個),下載近200個節目

Android 內存暴減的秘密?!

experiment

操作對照圖

通過AndroidStudio查看內存占用情況。

Android 內存暴減的秘密?!

compare clean install

在場景一種:4.0版本占用 38.74M,而3.9版本占用 59.78M。減少了21.04M內存。

compare heavy use

在場景二中:4.0版本占用 45.5M,而3.9版本占用 87.4M。減少了41.9M內存。

事實上,因為有圖片緩存在LRU算法的基礎上增加了TTL邏輯,在靜止1分鐘之后(只要不再加載新圖片),4.0版本,內存還會下降。(圖片緩存超時主動清理)。

Android 內存暴減的秘密?!

4.0 ImageCache TTL

可以看到Java內存下降到 34.92M,而此時3.9版本仍然沒有變化,此時內存減少 52.48M。

PS:需要注意的是3.9版本的“廣播”tab在4.0版本替換成了“書城”tab,而書城tab的頁面要遠復雜的多,圖片也更多。

最后,在4.0版本發布外網之后,筆者對比了一下3.9版本的Crash上報,結果如下:

Android 內存暴減的秘密?!

總的crash率從 0.41%下降到%0.16,減少了0.21%。而OOM類型的crash率從 0.19%下降到 0.04%,減少了0.15%!而剩下的0.04%則主要是線程創建導致的。目前在通過線程監控組件查找根本原因,后續推動相關SDK進行優化!

七、結論

另外需要注意的一點是,動態內存和靜態內存雖然分別減少了 52M 和 28M,但是兩者是有一部分交集的。

兩者的測量標準稍有不同,對應用的影響也不同。

動態內存主要優化app在低內存設備上的性能,并減少OutOfMemory發生的幾率。

而靜態內存,主要優化app退后臺后的內存占用,一方面可以減少應用進程被Android系統的LowMemoryKiller殺死,另一方面可以讓用戶的設備有更多剩余內存,用戶體驗更好。

UPA——一款針對Unity游戲/產品的深度性能分析工具,由騰訊WeTest和unity官方共同研發打造,可以幫助游戲開發者快速定位性能問題。旨在為游戲開發者提供更完善的手游性能解決方案,同時與開發環節形成閉環,保障游戲品質。

來自:https://segmentfault.com/a/1190000012708312

標簽: Android
相關文章:
国产综合久久一区二区三区