如果說前兩節(jié)對應用性能優(yōu)化幅度有限的話,那么本篇內(nèi)存則直接關系到應用的生死存亡煞赢。
好的優(yōu)化可以讓死亡邊緣的應用起死回生慎璧,避免內(nèi)存泄漏及OOM床嫌。
內(nèi)存泄漏一般是長生命周期的對象持有短生命周期對象的引用跨释,當短生命周期完成使命要被資源回收時,GC Root發(fā)現(xiàn)對象可達厌处,所以并不回收鳖谈,如果這樣的情況發(fā)生很多,就容易造成內(nèi)存浪費阔涉,嚴重時導致OOM缆娃。形象的說就好比,在餐廳吃飯瑰排,顧客點了一餐龄恋,實際上吃完了飯,但是手還端著碗沒放開(持有碗的引用,占用內(nèi)存), 服務員(GC)看到后認為其沒吃完飯凶伙,所以本將收回碗筷結果就不收了郭毕,顧客吃完了,沒吃飽函荣,又點了一餐显押,吃完又沒松手(之前的摞在一起),來回幾次后傻挂,餐廳的碗不夠用了乘碑,,金拒,雖然不夠準確兽肤,但也差不多是這個意思。
這里我們將使用多種手段揪出內(nèi)存中的“病原體”, 涉及到 Android profiler Mem 和 Mat 以及 adb相關命令的使用绪抛。
一.Android profiler
Android Profiler網(wǎng)上教程太多了资铡,包括官網(wǎng)也有詳細介紹,常規(guī)的就不多說了幢码,這里想給大家說下基于Android Profiler的內(nèi)存優(yōu)化的思路笤休。
如圖,Profiler的內(nèi)存分析頁面主要有兩個功能按鈕,一個是heap dump症副,一個是record店雅,它們有什么區(qū)別呢?
Heap Dump有個官方的中文名叫堆轉儲(重要概念后面還會用到)贞铣,不能指定時長闹啦,自動收集幾秒的內(nèi)存分配情況,保存了當前Java堆上所有的內(nèi)存使用信息辕坝,能夠完整的反映虛擬機當前的內(nèi)存狀態(tài)窍奋,并且還有內(nèi)存泄漏的直接提示;它的文件格式是.hprof。
Record 用于記錄內(nèi)存分配费变,可以自由控制時長,但功能沒有dump全面挚歧,不能直接查看內(nèi)存泄漏扛稽。并且在Android 7.1以上版本時是沒有這個按鈕的,它的文件格式是.alloc滑负。
我們關注 heap dump就好在张。
先使用dump快速查看內(nèi)存的大體分配情況,以及是否有內(nèi)存泄漏情況矮慕。
點擊dump后生成如下視圖(點擊dump時會執(zhí)行一次GC,內(nèi)存也會稍微升高,這是正嘲镓遥現(xiàn)象)
可以看到,1處提示有14處內(nèi)存泄漏的地方,我們在2處切換到”show activity/fragment Leaks”痴鳄,查看頁面導致的內(nèi)存泄漏瘟斜,3處顯示了這些造成內(nèi)存泄漏的fragment,選中第一個痪寻,在4處顯示了它的所有實例螺句,5處顯示了它們的內(nèi)存分配。
這里有4列橡类,分別解釋下它們的含義
Depth : 從任意 GC 根到選定實例的最短路徑蛇尚。
Native Size: 從 C 或 C++ 代碼分配的對象的內(nèi)存
Shallow Size: 對象本身占用內(nèi)存的大小,不包含其引用的對象顾画。這里可以看到6個實例它們的Shallow size都一樣,因為創(chuàng)建fragment的動作都是一樣的取劫。
Retained size: 是該對象自己的shallow size,加上從該對象能直接或間接訪問到對象的shallow size之和(也可以理解為本身對象內(nèi)存加上成員變量的內(nèi)存)研侣。換句話說谱邪,retained size是該對象被GC之后所能回收到內(nèi)存的總和。這里用一個圖來描述更為直觀:
把內(nèi)存中的對象看成圖中的節(jié)點义辕,并且對象和對象之間互相引用虾标。這里有一個特殊的節(jié)點GC Roots寓盗,這就是reference chain的起點灌砖。從obj1入手,上圖中藍色節(jié)點代表僅僅只有通過obj1才能直接或間接訪問的對象傀蚌。因為可以通過GC Roots訪問基显,所以左圖的obj3不是藍色節(jié)點;而在右圖卻是藍色善炫,因為它已經(jīng)被包含在retained集合內(nèi)撩幽。
所以對于左圖,obj1的retained size是obj1、obj2窜醉、obj4的shallow size總和宪萄;右圖的retained size是obj1、obj2榨惰、obj3拜英、obj4的shallow size總和。obj1的Depth為1琅催。
在左圖中居凶,obj2的retained size是obj2和obj4的shallow size的和;在右圖中是obj2藤抡、obj3和obj4的shallow size的總和侠碧。Obj2的Depth為2。
清楚了基本知識后缠黍,我們繼續(xù)往后看弄兜,點擊6處的reference可查看所有引用當前NewsListFragment的對象。
隨便選擇一個瓷式,在紅框處右鍵jump to source,定位到代碼挨队,如下:
代碼跳轉到了NewsListFragment的父類BaseFragment中的內(nèi)部類SpaceItemDecoration,可以看到它是非靜態(tài)的,在運行時會持有NewsListFragment的引用蒿往,我們將其改為static的,再運行應用重新dump盛垦,這個引用不存在了:
這里只是介紹解決問題的思路,并不是說這個SpaceItemDecoration就是內(nèi)存泄漏的元兇瓤漏,我這里的NewsListFragment是常駐的并不會銷毀腾夯,只會隱藏和顯示,所以即使有個多個不同的對象引用它也是沒問題的蔬充,proflier這里提示leak蝶俱,也只是從對象引用的角度來說,它并不知道我們的實際意圖饥漫。所以僅做參考榨呆,并不是說有l(wèi)eak了就一定是問題,必須解決庸队。當然如果你的Activity或者Fragment已經(jīng)關閉了积蜻,在dump中還依然存在實例,那就是內(nèi)存泄漏彻消,需要解決竿拆。
這里放出官方對我們的建議:
在您的堆轉儲中,請注意由下列任意情況引起的內(nèi)存泄漏:
1.長時間引用 Activity宾尚、Context丙笋、View谢澈、Drawable 和其他對象,可能會保持對 Activity 或 Context 容器的引用御板。
2.可以保持 Activity 實例的非靜態(tài)內(nèi)部類锥忿,如 Runnable。
3.對象保持時間比所需時間長的緩存
Tips
1.我們也可以通過命令導出.hprof文件怠肋。
adb shell am dumpheap pid /data/local/tmp/x.hprof
二.Android profiler + MAT
Dump的進階使用方式缎谷,先記錄一次,頻繁操作一段時間后(可以使用monkey或者按鍵精靈或者其它自動化測試的工具灶似,實現(xiàn)壓力測試)列林,再dump一次, 把兩次的結果放到MAT中對比酪惭,從而清楚的了解到內(nèi)存的變化情況殉摔。這種方式比只dump一次更加合理和直觀山憨。
點擊保存按鈕可以把內(nèi)存信息保存為.hprof文件,這個文件需要轉成MAT支持的格式(或者說標準的Java hprof格式,主要是版本號不一致)厕诡,使用SDK/platform-tools里面hprof-conv.exe這個命令,如下:
hprof-conv old.hprof new.hprof
將第一次和第二次的.hprof文件都轉換完成后温艇,把這兩個文件導入到MAT中(直接拖拽即可)肠虽。
Tips:MAT這里稍微科普下
MAT是Memory Analyzer的簡稱, 是基于Eclipse開發(fā)的(這個老Androider應該都用過吧)
官網(wǎng):http://www.eclipse.org/mat/
導入時醉者,選中Leak Suspects Report 再finish即可。
打開后,先選擇after的那個hprof窥岩,然后選擇 1 overview甲献,2 Histogram(直方圖)
再點擊3處按鈕和 before比較。
比較結果會直觀列出Objects(對象數(shù)量) 颂翼,Shallow內(nèi)存的變化情況晃洒,可以看到,after比before新增加了很多對象和內(nèi)存朦乏。
(注意:跑monkey時盡量保證主Activity不會重建球及,否則會造成增長過多的假象,影響準確性)呻疹。
默認是以類的方式排列吃引,還可以使用包名的方式排列:
這樣就可以很直觀的看到自己應用內(nèi)存的變化了。
下面還有第二種比較方式刽锤,可以更全面友好的顯示內(nèi)存變化的情況镊尺,
在Histogram tab頁下點擊1處的Navigation history,然后在OverviewPane最后一個histogram上右鍵Add to Compare Basket
兩個都添加過來后(before在上)姑蓝,然后點擊紅框處的紅色嘆號(Compare the results) 進行比較
比較結果如下鹅心,相比第一種比較方式,這種的列數(shù)更多纺荧,把before和after的數(shù)據(jù)都列了出來。
還可以切換其它的視圖,比如用百分比顯示變化的情況宙暇。
如果有增長特別多的類输枯,那么有可能存在內(nèi)存泄漏,可以選擇with incoming references查看引用它的對象占贫。
關于outgoing和incoming備注里還會介紹桃熄。
以上是自己手動對比內(nèi)存的變化,如果你想偷懶型奥,想快速查看是否存在內(nèi)存泄漏的方法瞳收,MAT提供了一個名字很霸氣的功能,叫做:
Leak Hunter(泄漏捕手)
兩種方式打開:
打開后厢汹,長這樣:
按泄漏的大小排序螟深,查看其中一個問題的detail:
點擊Refercenec Pattern里的類名-List objects-with incoming references 查看誰引用了它。(有的不一定有Refercenec Pattern)
然后在這里列表里查看詳細的引用關系:
除了泄漏捕手烫葬,MAT還提供了大對象的查看方法界弧,這個也是我們的優(yōu)化方向。
在overview頁面選擇Top Consumers查看應用中的大對象并按照大小排序搭综,以餅圖的形式展示垢箕。
可以看到這個byte[]大對象里面包含了Glide和CircleImageView中的bitmap。
遺憾的是這里并不能像AS proflier中那樣直接調轉到代碼,因為這里是脫離了項目的代碼環(huán)境兑巾。不過也足夠詳細了条获,畢竟應用內(nèi)所創(chuàng)建的java 對象這里都會一個不拉的顯示出來:
展開CircleImageView后可以看到,personFragment中有個名為 mHeadIv的CircleImageView類型的對象蒋歌,它占用的內(nèi)存空間分別是 Shallow Heap 304字節(jié) , Retained heap 2544字節(jié)月匣。
好了,MAT的介紹就到此為止奋姿。僅僅只是拋磚引玉锄开,MAT的強大遠遠不止這些,比如它支持OQL(Object Query Language)称诗,你可以查某個類的所有實例甚至是按地址搜索某個對象萍悴。
到這里,我們了解到了 4種分析內(nèi)存的方法寓免,總結如下:
1.單純Android Profiler Mem: 最便利的方法癣诱,可以直接查看leaks情況,功能強大袜香,可跳轉到源碼(首選)
2.MAT 對比.hprof文件: 操作稍微繁瑣,但很直觀,也更貼近真實場景
3.MAT leak hunter: 快速查看可能的內(nèi)存泄漏(感興趣的人可以和profiler中的leak對比下撕予,看看有沒有異同)
4.MAT top consumer: 快速查看大對象
可以說使用了Profiler 和MAT, 簡直就是中西結合,藥到病除蜈首。
三.adb命令
有人說了实抡,前面介紹的這些方法都太麻煩欠母,還有沒有簡單點的?我就想看下內(nèi)存占用的情況吆寨。
當然有了赏淌,它就是adb命令(挺適合沒有as的測試人員使用),本節(jié)介紹五種方式啄清。
第一種:procrank
adb shell su (需要賦予超級權限,否則可能報錯)
procrank -p (按pss排序)
VSS - Virtual Set Size 虛擬耗用內(nèi)存(包含共享庫占用的內(nèi)存)
RSS - Resident Set Size 實際使用物理內(nèi)存(包含共享庫占用的內(nèi)存)
PSS - Proportional Set Size 實際使用的物理內(nèi)存(比例分配共享庫占用的內(nèi)存)
USS - Unique Set Size 進程獨自占用的物理內(nèi)存(不包含共享庫占用的內(nèi)存)
第二種:dumpsys
adb shell dumpsys meminfo 包名(后面不加包名則是查看所有的):
不僅能看應用的各項內(nèi)存指標六水,還能看到創(chuàng)建了多少個view和activity(1處),甚至還能看到有哪些數(shù)據(jù)庫辣卒,是不是很強大(2處)掷贾。
第三種:查看系統(tǒng)內(nèi)存情況:
adb shell cat /proc/meminfo(一般看available即可)
第四種:showmap,可以查看每個so庫占用的內(nèi)存大小
adb shell showmap pid
第五種:smaps
查看進程的虛擬內(nèi)存空間
adb shell run-as 包名 cat /proc/pid/smaps
參考:https://www.cnblogs.com/bravery/archive/2012/06/27/2560611.html
adb總結: 我們介紹了5種查看內(nèi)存的方式荣茫,分別是
Procrank想帅、dumpsys 、proc计露、showmap博脑、smaps(其實也都是基于Linux的),操作方便票罐,一行命令即可叉趣,前四個適合普通測試人員使用。smaps入手門檻較高该押,適合進階使用疗杉。以上命令在末尾處添加 > xxx.log 都可以把信息保存到文件中。
總結:
本章節(jié)我們主要介紹了應用穩(wěn)定性的重中之重-內(nèi)存蚕礼,惱人的OOM和內(nèi)存泄漏就常常是因為內(nèi)存使用不當而產(chǎn)生(嚴格來說,造成oom的原因還可能有線程數(shù)量過大烟具、Fd(文件描述符)數(shù)量過大、虛擬內(nèi)存不足等)奠蹬,幾乎是最影響應用穩(wěn)定性的部分了朝聋。
我們的解決辦法就是dump出hprof內(nèi)存快照文件,通過兩種工具AS和MAT進行分析得出結論囤躁,但是這部分只是針對于Java層的堆內(nèi)存冀痕,除了這個還有native的內(nèi)存,主要和so庫相關狸演,不能夠直接通過現(xiàn)成的方法獲取到相關信息言蛇,一般是通過hook系統(tǒng)庫libc.so的malloc和free函數(shù)去獲取到native層所有的內(nèi)存的申請和釋放操作,再結合smaps(進程虛擬內(nèi)存信息)文件進行分析,將相對獨立的內(nèi)存信息追溯到業(yè)務堆棧從而定位到具體方法(這也是為什么bugly中的native異常需要提供so符號表才能定位的原因)宵距。當然也有一些現(xiàn)成的工具腊尚,比如 HeapSnap,malloc_debug满哪,asan婿斥,valgrind劝篷。
這部分涉及到 Linux動態(tài)鏈接等底層知識,我們沒法過多的介紹受扳。
感興趣的小伙伴可以參考 騰訊的native內(nèi)存分析與監(jiān)控:
https://mp.weixin.qq.com/s/0cF5Q6_LXrkLAdjkXIwrVQ
以及西瓜視頻的線上native內(nèi)存的泄漏監(jiān)控Raphael:
https://github.com/bytedance/memory-leak-detector
備注:
1.MAT中的引用關系
list objects -- with outgoing references : 查看這個對象持有的外部對象引用(引用誰)携龟。
list objects -- with incoming references : 查看這個對象被哪些外部對象引用(被誰引用)兔跌。
show objects by class -- with outgoing references :查看這個對象類型持有的外部對象引用
show objects by class -- with incoming references :查看這個對象類型被哪些外部對象引用
2.三方分析Heaphero
如果懶得自己去分析內(nèi)存問題勘高,其實可以交給三方機構HeapHero(應該還有很多類似的),免費還不用注冊坟桅,只需要提交dump文件即可(知道它為什么叫堆轉儲了吧华望,其實就是把瞬間的內(nèi)存堆信息轉化為可存儲的持久化信息,有了這個堆信息就像是體檢報告仅乓,你可以拿著到處問醫(yī)生開處方了 )
是不是很方便赖舟?但前提是英語要過關,夸楣,宾抓,
https://heaphero.io/heap-index.jsp#header
3.Android 8.0之后 圖片內(nèi)存的申請由Java層切換到了native層
4.這里需要注意,物理內(nèi)存不足時豫喧,會引起 onLowMemory 回調石洗;當虛擬內(nèi)存占用超過最大限制的 90% 時,觸發(fā)為低內(nèi)存告警紧显。超過最大限制則直接觸發(fā) OOM讲衫,因此我們也需要監(jiān)聽虛擬內(nèi)存的占用情況。
一些可供參考的文章:
MAT: Incoming Vs Outgoing References
https://cloud.tencent.com/developer/article/1530223
Java堆:Shallow Size和Retained Size
https://blog.csdn.net/kingzone_2008/article/details/9083327
內(nèi)存分析診斷系列-理解heap dump
https://blog.csdn.net/u012811805/article/details/106547389
官方教程
https://developer.android.google.cn/studio/profile
https://developer.android.google.cn/studio/command-line/dumpsys