我這樣減小了26.5M Java內存!

做者:楊超,騰訊移動客戶端開發 工程師
商業轉載請聯繫騰訊WeTest得到受權,非商業轉載請註明出處。
原文連接:http://wetest.qq.com/lab/view/359.htmlhtml

WeTest 導讀

歷時五天的內存優化已經結束,這裏總結一下這幾天都作了什麼,有哪些收穫。優化了,或能夠優化的地方都有哪些。(由於不少事還沒作,有些結論須要必定樣本量才能判定,因此叫一期)一期優化減小JavaHeap內存佔用約26.5M。java


在任何性能優化以前,要作的第一件事就是找到性能瓶頸!而找到性能瓶頸一般須要強大的debug工具輔助。內存方面Android有 AndroidStudio 的 Android Profiler、Allocation Tracker,以及Eclipse的MAT用於分析java的內存佔用,至關強大。而偏向native層面的內存佔用則找不到太好的工具,所以這裏在作優化前,先造了幾個工具。python

1、造輪子


1. 線程建立分析工具android

該工具使用native hook的方式,直接hook了pthread_create調用,並記錄每個線程建立時的堆棧,並打印日誌。同時維護一個running thread的集合,必要時 dump下來全部running thread的建立堆棧,用於分析野蠻線程建立的場景。以及對應的日誌分析工具。git

二、 Linux /proc/<pid>/smaps 文件分析腳本github

主要用於跟蹤進程的 Code 部份內存(見下文)佔用,分析出佔用內存較多的dex,so文件。排查第三方SDK佔用過多內存場景。網上只能找到一個perl腳本,功能不是很強大,鑑於筆者不熟悉perl的語法規範,改起來會比較困難,所以直接用python重寫了一個。數據庫

代碼在這裏:https://gist.github.com/LanderlYoung/aedd0e1fe09214545a7f20c40c01776c數組

三、 快速Dump Android java heap腳本緩存

由於分析內存須要不少dump操做,因此乾脆寫了個Bash腳本。性能優化

Bash腳本連接:https://gist.github.com/LanderlYoung/9cd0f49e49e42746622cc8e7b4bbcc8a

(順便提一下,android提供的 hprof-conv 工具備個參數 -z 用於排除zygote的內存,十分便利。)

2、Android 進程 內存分類


一般咱們在系統的內存管理頁面看到的內存佔用是進程的PSS,也就是整個進程的內存佔用,所以咱們作優化的要考慮到全部的內存,不只僅是Java Heap

使用Android Studio(3.0 beta)的 Android Profiler工具。

這裏寫圖片描述

咱們能夠很清晰的看到

1)進程總內存佔用: 180M

2)JavaHeap: 48M

3)NativeHeap:native層的 so 中調用malloc或new建立的內存 —— 28M

4)Graphics:OpenGL和SurfaceFlinger相關內存 ——58M

5)Stack:線程棧——1.89M

6)Code:dex+so相關代碼佔用內存——37.75M

7)Other:蜜汁存在

上述6中內存佔用除了兩種不須要考慮,其餘5中統統須要優化。不須要考慮的是:

1)Other:暫時無從分析

2)Graphics:若應用沒有直接調用OpenGL,則能夠肯定這部份內存是由Android Framework操控的,能夠忽略。(固然對於遊戲類應用,這裏確定是優化重點。)

下面按照內存分類分開逐一介紹分析方法,和結論:

JavaHeap

這裏必然是內存優化的重點,無需多言。可是企鵝FM的業務,UI,代碼已經比較龐大,分析起來會顯得力不從心。所以這裏主要從兩個方面入手,但願能總結出一套分析方法。

一、分析應用 靜息態 內存佔用。
所謂靜息態,是筆者自行定義的概念:

應用在退後臺以後,不保留活動的場景下的內存佔用。

爲何要考察這個維度?由於這個是一個應用內存佔用最低點的時候,後續打開任何Activity內存只會更多,不會更少!

二、分析方法

1)開發者選項開啓「不保留活動」

2)進入MainActivity,滑動頁面,操做一下

3)退後臺,Android Studio中強制執行GC

4)dump java heap (注意上面提到的 hprof-conf 加上 -z 參數排除zygote的干擾)

5)MAT 分析 dump 下來的JavaHeap

重點介紹一下MAT:

這裏能夠直接打開domanitor_tree看佔用內存最多的實例。

這裏寫圖片描述

從這裏按照RetainedHeap倒序排列,一點一點的排查內存佔用。很容易發現不正常的內存狀況。

在企鵝FM中發現:

1)圖片的內存級緩存退後臺沒清空(此處屬於onTrimMemory回調的處理有誤),佔用10M內存

2)ImageMisc — 280k

這裏寫圖片描述

② 是一個buffer,能夠在不用的時候釋放內存

③ 優化目標,完全乾掉

3)播放頁應用動畫的關係,UI是單例。其中相關View佔用數百K內存,而button的icon直接引用住了5-6M的bitmap資源。

4)播放列表存儲了103個ShowInfo,每一個ShowInfo 22k,總計內存約2.24M,ShowInfo冗餘信息不少,能夠考慮優化數據結構

這裏寫圖片描述

5) DanmuManager — 510k

● mDanmuItemManager 內含衆多彈幕

● 每條彈幕6k

● UI相關數據,離開播放頁後應該清理彈幕(由於無需展現了)

● 優化目標,完全乾掉。

6) FileCacheService — 362k

這裏寫圖片描述

② 其中緩存了每個cache entry,其中圖片緩存較多

③ 每個entry記錄完整文件路徑其比較長,所以路徑的字符串佔用了不少內存

④ 優化方案:

● 文件Parent能夠共用同一個File對象。

● entry = new File(parent, 「entry_name」)

⑤ 優化目標 到100k

7)LiveRoomShowListManager -- 287k

這裏寫圖片描述

② 優化目標:UI相關數據,離開界面應該完全乾掉

8) DB InsertHelper, Sql Statement clearBinding

① 700K到2M

② InsetHelper中會引用住最後一次執行DB insert調用的 數據(佔位符)

③ InsertHelper的佔位數據能夠在insert完成以後清掉

針對上面提到的ShowInfo的數據結構優化

這裏寫圖片描述

擬定優化方案:

1)ShowList存儲的ShowInfo數量過多,30個足矣。

2)ShowInfo中Album字段佔用10k內存,其實同一個ShowList中大多數album是徹底一致的(好比專輯類型的ShowList,主播類型的,自選集類型的,本地專輯的,etc...)。

預計內存佔用 2M -> 30*12K = 360K

3)靜息態內存優化總結:

上述幾點加起來預期能夠減小內存佔用:

10M + 280K + 5M + 2M + 510K + 260k + 287k + 1M = 約20M

三、 MainActivity 操做一段時間以後內存增量

上面分析的是靜息態內存,下面看一下MainActivity操做一點時間以後,內存有怎樣的變化。

這裏採用的方式是:

1)dump靜息態內存

2)進入MainActivity,當即dump內存

3)操做一段時間以後再dump內存

一共有三次dump,能夠利用MAT對比heap的功能對比內存增量。

打開MAT的historgram視圖

這裏寫圖片描述

工具欄最右邊有個雙箭頭的icon,點擊可對比dump:以下圖

這裏寫圖片描述

增量最多的仍是Bitmap(底層用byte[]存儲),藉助MAT的 Finer 工具能夠直接看到Bitmap的圖片。

這裏發現的幾個問題是(時間關係,應該屢次測試的,會發現更多問題):

① Banner的大圖沒有 Clip 致使 分辨率 很高

② 分類頁的 配置區域 沒有Clip

③ onRecycle沒有清除掉已經引用的Bitmap,致使引用住不能gc

主要說一下第3點,是Banner每個Item有一個大圖作背景,當item的view被回收的時候,相應的ImageView仍然持有着大圖,致使其不能回收。這裏發現了4張1M+的大圖,其實理論上應該只有1張。

這個問題能夠推廣到全部的ListView場景,建議方式是:

替換爲RecyclerView,在view回收的時候,ScarpView釋放圖片引用。

此外,MainActivity有5個tab,各個tab之間其實會用到相同的View(listview 的item),若是使用RecyclerView能夠作到5個tab的RecyclerView共同複用同一個RecyclerPool,在節省內存的同時還能顯著提升性能。

這裏不方便直接測試內存佔用,預估能夠節省內存5-10M。

四、 正常操做應用,觀察內存佔用圖表是否有突起

這裏主要用來測試異常內存分配的場景。

這裏仍然須要很大人力,過不少頁面。

目前發現問題有:

1)service進程,發送wns請求的時候,內存異常增加2-3M。

這裏寫圖片描述

這時可使用AllocationTracker工具(點擊下圖工具欄的紅點),記錄峯值那一段內存的分配,如圖:
這裏寫圖片描述

這裏能夠直接看到分配的棧,定位過去看,發現是這樣的代碼,由於head是一個65536長的數組(在 com.tencent.wns.session.Session 的構造函數寫死的長度),這裏建立string就浪費了超大量的內存。建議能夠改爲下圖彈窗裏的樣子
這裏寫圖片描述

2)另一個問題是播放進程,在切換節目的時候內存會忽然增加2-3M,簡單跟進去看是exo建立buffer。彷佛有問題,須要再多分析一下~

Native Heap

目前能看到的NativeHeap大小是

應用啓動:26M 此時已經初始化了 X5內核和IM SDK

UGC錄音:26M->34M 退出以後時32M,還有部分沒釋放,疑似內存泄漏

發起直播:32M->72M 退出以後42M,一樣沒有徹底釋放

具體內部佔用狀況還沒測。。。(都說了是一期)。

官方文檔:

https://source.android.com/devices/tech/debug/native-memory

Code

這一段明顯看到佔用了不少內存。各個場景下的使用狀況是:

1)剛進入應用:38M

2)再使用UGC錄音:38.28M

3)再使用視頻直播(發起直播):46M

4)打開應用內WebView(X5內核):56M

以上是主進程的內存,佔用至關多。須要注意的是code內存佔用通常是經過read-only方式mmap映射到內存中的的dex、odex、so等文件,所以在內存緊張的狀況下,系統會回收這些內存,只是在oom-killer中仍然會計算在內。

另外播放進程2.27M,service進程1.1M還屬於比較正常的水平。

顯然主進程的Code內存佔用太多了,須要分析。這裏經過解析Linux標準的 /proc/<pid>/smaps文件,這個文件記錄了進程內每一段虛擬內存的文件映射狀況,這個文件只有進程本身有讀權限,因此要麼用root的機器,要麼就本身寫段代碼copy出來。結合上面提到的工具。分析結果以下

● 應用so佔用 app so map Rss = 3984 kB (其中IM SDK 2576k)

● 應用的dex佔用 app dex map Rss = 15101 kB

● X5內核的so+dex內存佔用 tbs mem map Rss = 29048 kB

● 直播so相關 avlive mem map Rss = 3092 kB

● 其中X5內核的代碼沒有打進apk,所以能夠比較獨立的統計出來,佔用有29M之多,讓人驚訝!

● 其次直播的java代碼打進了apk不方便單獨統計內存用量,可是so是獨立加載的,內存佔用3M也是很多的。

● 最後是應用自身的dex佔用有15M之多,由於自身代碼量很大,彷佛能夠理解,可是仍然不少啊!

這裏須要考慮的是 X5 內核可否延時加載?由於沒打開WebView的時候就已經佔用了數M了。另外WebView關閉以後是否能夠銷燬。

直播相關SO,能夠考慮直播退出以後從內存中卸載掉。(java規範是加載so的classloader被GC,相關so便可卸載)。

應用自身dex佔用。android 8.0 對art優化一個叫作DexLayout 的能力,應爲mmap映射的文件不會被當即加載進內存,在用到的時候是按照頁大小(4k)加載的,當用到的類在dex中分佈很分散的時候,就會致使盲目加載不少頁,DexLayout就是把熱點類集中放到一塊兒。這裏FaceBook推出了ReDex工具,能夠參考一下。

PS:關於DexLayout

這裏寫圖片描述

3、線程建立


在AndroidStudio的Memory Profiler中沒有線程數這個維度。可是運行中,主進程的線程數量一般會在100個左右,這是個驚人的數字,要知道Mac版的AndroidStudio也不過77個線程。。。。請自行體會一下。

關於線程的建立和內存佔用,請參考筆者的另外一篇文章:《Android 建立線程源碼與OOM分析》 。

這裏分析用的自制工具,dump下載全部running的線程,和他們建立時的堆棧。

結果是

● X5:25個線程(簡直。。。)

● IMSDK:17個線程

● StackBlur:8個線程

● WNS:7個線程

● ImageLoader:6個線程

● magnifiersdk:5個線程

須要注意,這裏的棧和線程名,是建立線程的時候的調用棧,以及對應的線程名(而不是子線程名)

事實上,用一樣的方法,還能夠分析一下進程歷史中全部建立過的線程,統計哪裏建立線程最多。

一般來講,全部線程應該有應用統一的線程池來管理,sdk內部須要線程池,應該有外部注入一個線程池來提供給sdk使用。

若是有其餘狀況,如:不是在線程池建立的線程,在sdk本身的線程池裏建立的線程,這種均可能致使線程數量的野蠻增加,須要聯繫sdk的開發人員杜絕這種狀況。

4、總結


以上就是這5天的工做結果:

java內存佔用基本合理,靜息態 內存佔用能夠優化20M,MainActivity運行時的內存佔用能夠優化5-10M。

code內存佔用太多,其中X5內核佔用29M實在太多,須要考慮優化。

應用內的線程數量主要有X5內核,IMSDK和WNS貢獻,外網線程建立的OOM crash 系WNS的bug,須要聯繫相關sdk開發人員。

最後是Native內存佔用尚未詳細分析,暫時看不到使用狀況。可是能夠知道目前的結論是:Native內存佔用不少,且應該存在內存泄漏。

PS: 實際效果反饋

按照上述分析結果,進行了相關的代碼調整。

執行的點包括:

一、IntelliShowList pageSize 50->20

二、IntelliShowList 公用Album結構

三、Afc-db clearBinding after insert, 數據庫

四、Afc-FileCacheService cache Entry with fileName not full path, 文件緩存

五、 fix onTrimMemory bug,退後臺清空圖片內存緩存

六、播放頁相關控件,退後臺以後清掉icon,釋放bitma引用

未執行的點包括:

一、播放頁的bottomPannel部分icon由於邏輯較爲複雜,暫時未進行處理。預計內存佔用1M

二、PlayLogic的historyList邏輯複雜暫時未處理,預計內存佔用500K

三、24h直播間LiveRoomShowListManager -- 287k

四、DanmuManager — 510k

五、通過ice提醒,下載節目的record也會所有加載進內存。每一個ShowInfo 22k,內存佔用取決於用戶下載的節目數。

效果對比:

before:39.32M

這裏寫圖片描述

after:12.88M
這裏寫圖片描述

優化內存佔用 26.44M!

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

目前,限時內測正在開放中,點擊http://wetest.qq.com/cube/ 便可預定。

對UPA感興趣的開發者,歡迎加入QQ羣:633065352

若是對使用當中有任何疑問,歡迎聯繫騰訊WeTest企業QQ:800024531

相關文章
相關標籤/搜索