本篇主要講解android內存性能優化之檢測方案。內存性能主要包括內存泄漏, 內存抖動, 內存持續增長(但GC后會下降), 內存占用過大等問題。
Android內存分析方向:
- Java 內存分析
- Java中的內存泄露主要特征:可達,無用
- 無用指的是創建了但是不再使用之后沒有釋放
- 能重用但是卻創建了新的對象進行處理
- Native 內存分析
- 堆中new的對象未釋放
- 對象引用導致無法釋放
- JS 中內存分析
本篇主要講解Java內存分析。
一. 日志分析
查看日志中是否有頻繁的GC。通常通過log,我們可以初步定為大部分內存等問題。
二. 常見內存泄漏查找
Context 泄漏, 主要為Activity 傳遞泄漏, context 未使用applciationConext 在單例創建時。
Handler 泄漏 , handler中持有view ,context 等做耗時操作。
Cursor 泄漏 , cursor未關閉
register 未 unregister
Bitmap
adapter 未使用convertView
不良代碼等
三. 命令dumpsys meminfo分析
adb shell dumpsys meminfo com.i2finance.shexpress
Applications Memory Usage (kB):
Uptime: 142597122 Realtime: 236611715
** MEMINFO in pid 25126 [com.i2finance.shexpress] **
Pss Private Private Swapped Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 61111 61084 0 0 69888 64350 5537
Dalvik Heap 49451 49316 0 0 71737 67348 4389
Dalvik Other 3333 3332 0 0
Stack 960 960 0 0
Cursor 12 12 0 0
Ashmem 130 88 0 0
Gfx dev 23780 23780 0 0
Other dev 4 0 4 0
.so mmap 4373 396 3108 0
.jar mmap 80 0 76 0
.apk mmap 17986 64 17580 0
.ttf mmap 96 0 80 0
.dex mmap 15729 16 14244 0
.oat mmap 2378 0 624 0
.art mmap 1859 1624 8 0
Other mmap 2039 12 1308 0
Unknown 84240 84240 0 0
TOTAL 267561 224924 37032 0 141625 131698 9926
App Summary
Pss(KB)
------
Java Heap: 50948
Native Heap: 61084
Code: 36188
Stack: 960
Graphics: 23780
Private Other: 88996
System: 5605
TOTAL: 267561 TOTAL SWAP (KB): 0
Objects
Views: 429 ViewRootImpl: 2
AppContexts: 2 Activities: 1
Assets: 7 AssetManagers: 3
Local Binders: 37 Proxy Binders: 31
Parcel memory: 26 Parcel count: 65
Death Recipients: 2 OpenSSL Sockets: 6
SQL
MEMORY_USED: 567
PAGECACHE_OVERFLOW: 157 MALLOC_SIZE: 62
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 24 45 5/24/6 /data/user/0/com.i2finance.shexpress/databases/pa_data_cache.db
4 28 19 1/16/2 /data/user/0/com.i2finance.shexpress/databases/mpush.db
4 60 37 5/18/6 /data/user/0/com.i2finance.shexpress/databases/fstandard.db
4 60 91 466/22/11 /data/user/0/com.i2finance.shexpress/databases/fstandard.db (2)
4 24 40 5/24/6 /data/user/0/com.i2finance.shexpress/databases/pa_data_cache.db
Asset Allocations
zip:/data/user/0/com.i2finance.shexpress/files/paanydoor_resource_3.5.0.36.jar:/resources.arsc: 67K
meminfo
的信息中各字段都是什么含義, 要理解各字段含義,我們才好進行內存的優化。
首先了解兩個概念:
私有內存(Dirty and Clean):
進程獨占內存。也就是進程銷毀時可以回收的內存容量。通常private Dirty內存是最重要的部分,因為只被自己進程使用。Dirty內存是已經被修改的內存頁,因此必須常駐內存(因為沒有swap);Clean內存是已經映射持久文件使用的內存頁(例如正在被執行的代碼),因此一段時間不使用的話就可以置換出去。實際使用內存(PSS):
將跨進程共享頁也加入進來, 進行按比例計算PSS。這樣能夠比較準確的表示進程占用的實際物理內存。
通常我們需要關注PSS TOTAL 和 Private Dirty .
-
Dalvik Heap
dalvik虛擬機分配的內存。PSS Total包含所有Zygote分配使用的內存,共享跨進程加權。PrivateDirty 是應用獨占內存大小,包含獨自分配的部分和應用進程從Zygote復制時被修改的Zygote分配的內存頁。 HeapAlloc 是Dalvik堆和本地堆分配使用的大小,它的值比Pss Total和Private Dirty大,因為進程是從Zygote中復制分裂出來的,包含了進程共享的分配部分。 -
.so mmap & .dex mmap ... mmap
映射本地或虛擬機代碼到使用的內存中。 -
Unknown
無法歸類的其他項。主要包括大部分的本地分配。 -
Native Heap native
代碼申請的內存, 堆和棧,及靜態代碼塊等。 -
TOTAL
進程總使用的實際內存。 -
Objects
中顯示持有對象的個數。這些數據也是分析內存泄漏的重要數據。如activity等。
四. Heap Viewer
Heap Viewer 能做什么?
- 事實查看內存分配情況和空閑內存大小
- 發現memory Leaks
AS中點擊機器人圖標打開Android Device Mointor, 如下:
選中進程進行Heap 分析,點擊update heap, 查看右側的heap標簽頁
Heap視圖顯示了堆內存使用的情況,每次垃圾回收都會更新,要查看更新情況, 點擊Cause GC即可。
下面的內容顯示的是分配的內存,按照類型分類:
如何檢查內存泄漏
我們需要在執行查看內存是否有泄漏的用例之前和之后執行GC,即手動點擊Cause GC,觀察allocated大小,查看內存是否在一個穩定的數值,多次操作,只要內存穩定,即沒有內存泄漏, 如果不斷變大,即表示有內存泄漏。
該工具也可以用來查看是否會發生內存抖動
五. 生成Dump
分析內存泄漏,我們需要生成相關的內存Dump,那么我們如何生成dump文件來進行分析。
目前有兩種方式:
-
打開Android Device Monitor
點擊dump Hprof file
Paste_Image.png
會生成一份Hprof文件,但該hprof文件我們無法打開,需要進行轉換之后才能用MAT工具打開,可以使用命令
hprof-conv com.i2finance.shexpress.hprof xxx.hprof
轉換生成可用的hprof文件。
- 使用Android Studio
打開Android Studio 的Android Monitor ,
選中Memory 標簽:
Paste_Image.png
點擊Dump Java Heap 即可生成對應的hprof文件,在側邊欄中打開Captures文件,選中文件點擊右鍵,export 出標準的hprof文件。
Paste_Image.png
六. Heap Snapshot
獲取Java 堆內存詳細信息,可以分析出內存泄漏的問題。
打開Android Studio 的Android Monitor , 選中Memory 標簽, 點擊Dump heap,生成hprof文件。AS會自動打開該文件,見下圖,但是該功能有點弱,建議還是轉換成mat可識別的hprof,使用mat進行分析。
七. 使用LeakCanary
使用內存檢測軟件leakCanary
- 添加依賴包
在build.gradle
中增加依賴
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
- 開啟leakCanary
在Applciation
的oncreate
中增加語句
LeakCanary.install(this);
查看leak詳情。
當發生內存泄漏時,會生成leak 報告, 報告中會詳細寫明具體發現內存泄漏的語句。
其原理,可以自行上網搜索查看一下。
八. Allocation Tracker(DeviceMonitor)
Allocation Tracker
能夠追蹤內存分配信息, 按照順序排列,這樣我們能夠清晰的看出來每一個內存對象是怎么一步一步的分配出來的。比如內存抖動的可疑點,我們可以通過查看其內存分配軌跡來查看段時間內有多少相同或相似對象被創建,進而找到問題發生的代碼。
操作步驟:
- 進入追蹤界面
- 點擊start Tracking 按鈕,開始跟蹤內存分配軌跡
- 操作用例
-
點擊Get Allocations,獲取內存分配軌跡。
Paste_Image.png
如上圖,上行app 從后臺切換道前臺時會調用onResume,可以追蹤到最后創建了多個Configuration對象。
上圖中,Allcated class 表示創建的類型,第一個Allocated in 表示在哪個類中, 第二個Allocated in 表示在哪個方法中。
查看源代碼如下:
public Resources getResources() {
Resources res = super.getResources();
Configuration config = new Configuration();
config.setToDefaults();
try {
res.updateConfiguration(config, res.getDisplayMetrics());
}catch (Exception e){
e.printStackTrace();
}
return res;
}
九. Allocation Tracker(AndroidMonitor)
功能同Allocation Tracker(Andorid Device)
, 但是展示更酷炫,更全面。
打開Android Monitor, 選中Memory 標簽 , 點擊圖標
, 進行內存tracker, 再次點擊結束tracker。As會自動打開tracker文件。
下面我們詳細看一下這個面板:
AS給我們提供了多種展示方式
- by Method :用方法來分類我們的內存分配
- by Allocator : 用內存分配器來分類我們的內存分配
點開每一項,都能夠查看到方法調用棧, 點擊右鍵可以跳轉到源碼。
AS 還為我們提供了統計,點擊餅狀圖標
按鈕即可。
分為兩種展示形式,有柱狀圖和輪胎圖,分配比例可選分配次數和占用內存大小:
-
Sunburst
輪胎圖是以輪胎為起點,最外層是內存實際分配的對象,每一個同心圓可能被分配為多個部分,代表不同的子孫,每一個同心圓代表他的一個后代。雙擊同心圓中某一個分割部分,會變成以你點擊的那一代為圓心再向外展開,如果想回到初始狀態,雙擊圓心即可。
下圖為 Sunburst + by Method
Paste_Image.png
下圖為Sunburst + by Allocator
一個內存的完整路徑
比如上行的首頁中trace 的數據, 我們看下我們自己的包:
會發現,最外圍有很多PageScrollEvent 對象, 我們去看下源代碼:
代碼如下, 我們發現自動loop的viewpager 每次滑動都會創建多個PageScorllEvent 對象。這樣也就對應上面這幅圖了。
private class PageChangeListener implements OnPageChangeListener {
private PageChangeListener() {
}
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (isLoop) {
int count = getAdapter().getCount();
if (position < 1 || position > count - 2) {
return;
}
}
LoopViewPager.this.mEventDispatcher.dispatchEvent(new PageScrollEvent(LoopViewPager.this.getId(), SystemClock.uptimeMillis(), position, positionOffset));
}
- Layout
柱狀圖是以左邊為起點,從左到右的順序是某個的堆棧信息順序,縱坐標上的寬度是以Count/Size 的大小決定的。其內容和輪胎圖是一致的。
下圖為Layout + by Method
十. MAT
MAT工具全稱為Memory Analyzer Tool,一款詳細分析Java堆內存的工具,該工具非常強大,為了使用該工具,我們需要hprof文件.
HPROF文件存儲的是特定時間點,java進程的內存快照。有不同的格式來存儲這些數據,總的來說包含了快照被觸發時java對象和類在heap中的情況。由于快照只是一瞬間的事情,所以heap dump中無法包含一個對象在何時、何地(哪個方法中)被分配這樣的信息。
幾個關鍵概念:
- Histogram:列出內存中的對象,對象的個數以及大小
- Dominator Tree:列出最大的對象以及其依賴存活的Object (大小是以Retained Heap為標準排序的)
- Top Consumers : 通過圖形列出最大的object
- Duplicate Class:通過MAT自動分析泄漏的原因
- Shallow heap : 對象本身占用內存的大小,不包含其引用的對象。
(常規對象(非數組)的Shallow size有其成員變量的數量和類型決定。數組的shallow size有數組元素的類型(對象類型、基本類型)和數組長度決定. 因為不像c++的對象本身可以存放大量內存,java的對象成員都是些引用。真正的內存都在堆上,看起來是一堆原生的byte[], char[], int[],所以我們如果只看對象本身的內存,那么數量都很小。所以我們看到Histogram圖是以Shallow size進行排序的,排在第一位第二位的是byte,char 。) - Retained Heap : 它表示如果一個對象被釋放掉,那會因為該對象的釋放而減少引用進而被釋放的所有的對象(包括被遞歸釋放的)所占用的heap大小。
(于是,如果一個對象的某個成員new了一大塊int數組,那這個int數組也可以計算到這個對象中。相對于shallow heap,Retained heap可以更精確的反映一個對象實際占用的大?。ㄒ驗槿绻搶ο筢尫?,retained heap都可以被釋放)。) - outgoing references :表示該對象的出節點(被該對象引用的對象)。
- incoming references :表示該對象的入節點(引用到該對象的對象)。
- GC Root: GC發現通過任何reference chain(引用鏈)無法訪問某個對象的時候,該對象即被回收。所以JVM就是GC Roots。
- Unreachable指的是可以被垃圾回收器回收的對象,但是由于沒有GC發生,所以沒有釋放,這時抓的內存使用中的Unreachable就是這些對象。
1. 預覽信息
打開dump 文件,通常我們需要關注一下幾個重要信息, 內存占用餅圖,Actions部分的Histogram, Top Consumers
.
我們打開Top Consumers,會生成一個報告,我們可以Biggets Objects overview, 能夠看到主要內存占用者
點擊下面的biggest Objects 可以查看具體的地址。
還有Biggest Top Level Dominator Classes , 可以看到主要占用內存的都是些什么東東。
2. dump分析
2.1 Histogram
MAT中Histogram的主要作用是查看一個instance的數量,一般用來查看自己創建的類的實例的個數。 可以分不同維度來查看對象的Dominator Tree視圖,Group by class、Group by class loader、Group by package 和Histogram類似,時間久了,通過多次對比也可以把溢出對象找出來。 Histogram 中可以分Group,Thread 區分信息。 通常為:選中某一項-> show objects and class -> by incoming reference->merge shortest path to gc root -> exclude weadk reference
等流程來查看具體情況。
可以在上面過濾相關包名,查看到具體類型, 關注objects個數, 表示內存dump 中有多少個相關類型對象, 比如不改存在的 對象存在了,或者有的對象內存中有太多的份數, 這樣就可以進行一個全面分析。
也可以選擇Group by package ,這樣方便根據package來進行分析。
也可以選擇thread來進行分析, 這樣查看占用內存最多的線程,這些線程可能為有內存問題的線程。
點擊右鍵常用的幾個選項:
- List Objects -> with incoming references 查看這個對象被哪些外部對象引用
- List Objects-> with outcoming references 查看這個對象持有的外部對象引用
- Path to GC Roots -> exclude ... references 查看這個對象的GC Root,不包含xxx引用,剩下的基本就是強引用了。因為只有強引用一直存在,gc就一直無法回收該對象,從而也就出現內存泄露。
- Merge shortest path to GC root 找到從GC根結點到一個對象或者一組對象的共同路徑。從這里可以查看到對象的引用關系。
2.2 Debug Bitmap
圖片一直是內存占用的一個大頭,也是引起內存泄露,OOM的常客。所以對圖片的分析是需要非常了解,這樣才能更好的優化項目。*注意:圖片在內存中占用的大?。篈RGB_8888 類型的圖片 為 內存中圖片寬度*內存中圖片高度4, 此處需要注意原始圖片寬高和內存圖片寬高不一致,包括拉伸和壓縮,尤其是圖片位置放錯,比如1080p設備,xxxhdpi下面沒有圖片,會去別的目錄下尋找圖片,此時將會對圖片拉伸。 **
下面我們來看一下圖片的處理。通常dump信息中圖片表現為兩種類型,Bitmap, byte[]。我們需要知道該圖片是哪張圖片,這樣才能好優化相關的圖片代碼。
-
Bitmap類型
在mat中通常能夠看到bitmap類型,占用了大量的內存,如下面這張圖片,在內存中占用2M。 我們可以打開,查看mBuffer變量。
Paste_Image.png
選中mBuffer-> 右鍵選中Copy-> 選擇Save Value To File -> 生成一個xxx.data 文件。
-
Byte[] 類型
如下,查看byte的 in comming, 即可看到它是一個bitmap,此時如下圖,我們可以直接將該byte數據寫入xxx.data 文件。
Paste_Image.png
下一步是選中對應的bitmap,打開Inspector 窗口,查看bitmap的尺寸,并且使用GIMP工具(可以安裝一個,開源的)打開剛才的data文件,圖像類型選擇RGB Alpha, 寬度和高度填入圖像的寬高,打開即可。
2.3 堆對比
通常為了分析內存是否泄露,內存是否持續增長但沒有釋放等問題,我們需要dump兩次來進行內存堆的對比。
打開兩個或多個dump文件,打開Navigation History視圖,點擊Historgam,選擇Add to Comp are Basket,最后選中Compare the Result 。
在對比結果中,主要分析類型或者對象的數量是否有變化, 內存是否有變化。
通過以上手段,我們可以定位到大部分內存問題。