1、內存了解
在Android App的性能優化的各個部分里,內存方面的知識較多且不易理解,內存的問題絕對是最令人頭疼的一部分,需要對內存基礎知識、內存分配、內存管理機制等非常熟悉,才能排查題。
1.1 了解進程的地址空間
在32位操作系統中,進程的地址空間為0到4GB,這里主要說明一下Stack和Heap:
Stack空間(進棧和出棧):
由操作系統控制,其中主要存儲函數地址、函數參數、局部變量等等,所以Stack空間不需要很大,一般為幾MB大小。
Heap空間:
它的使用由程序員控制,程序員可以使用malloc、new、free、delete等函數調用來操作這片地址空間。Heap為程序完成各種復雜任務提供內存空間,所以空間比較大,一般為幾百MB到幾GB。正是因為Heap空間由程序員管理,所以容易出現使用不當導致嚴重問題。
1.2 Android的內存管理
Android系統的ART和Dalvik虛擬機扮演了常規的內存垃圾自動回收的角色, 使用paging 和 memory-mapping來管理內存,這意味著不管是因為創建對象還是使用使用內存頁面造成的任何被修改的內存,都會一直存在于內存中,App唯一釋放內存的方法就是釋放App持有的對象引用,使GC可以回收。
1.2.1 Android的應用進程按共享/私有分類如下:
首先來了解什么是共享內存
Android系統的ART和Dalvik虛擬機扮演了常規的內存垃圾自動回收的角色, 使用paging 和 memory-mapping來管理內存,這意味著不管是因為創建對象還是使用使用內存頁面造成的任何被修改的內存,都會一直存在于內存中,App唯一釋放內存的方法就是釋放App持有的對象引用,使GC可以回收。
1.2.1 Android的應用進程按共享/私有分類如下:
首先來了解什么是共享內存
Android應用的進程都是從一個叫做Zygote的進程fork出來的。Zygote進程在系統啟動并且載入通用的framework的代碼與資源之后開始啟動。為了啟動一個新的程序進程,系統會fork Zygote進程生成一個新的進程,然后在新的進程中加載并運行應用程序的代碼。這使得大多數的RAM pages被用來分配給framework的代碼,同時使得RAM資源能夠在應用的所有進程之間進行共享。
大多數static的數據被mmapped到一個進程中。這不僅僅使得同樣的數據能夠在進程間進行共享,而且使得它能夠在需要的時候被paged out。常見的static數據包括Dalvik Code,app resources,so文件等。
大多數情況下,Android通過顯式的分配共享內存區域(例如ashmem或者gralloc)來實現動態RAM區域能夠在不同進程之間進行共享的機制。例如,Window Surface在App與Screen Compositor之間使用共享的內存,Cursor Buffers在Content Provider與Clients之間共享內存。
image
共享內存:Dalvik虛擬機代碼、應用框架的代碼、應用框架的資源應用框架的SO庫。
私有內存:應用的代碼、應用的資源、應用的SO庫
共享/私有內存:堆內存,其他部分
1.2.2 android進程中內存分類如下:
native heap:是lib層C/C++庫所占用的內存(Native代碼分配的內存,虛擬機和Android框架本身也會分配),不包含dalvik實例的linux進程,/system/bin/目錄下面的程序文件運行后都是以native進程形式存在的。
Dalvik heap:Dalvik虛擬機使用的內存,包含dalvik-heap和dalvik-zygote,堆內存,是java實例對象的空間以上兩個heap空間完全由程序員控制,是最主要的兩塊內存,另外還有下面3種:
Dalvik Other:類的數據結構和索引
so mmap:Native代碼和常量
dex mmap:Java代碼和常量
1.2.3 查看內存占用
通過命令行adb shell dumpsys meminfo packagename查看內存詳細占用情況。
其中幾個關鍵的數據:
-- Private(Clean和Dirty的):應用進程單獨使用的內存,代表著系統殺死你的進程后可以實際回收的內存總量。通常需要特別關注其中更為昂貴的dirty部分,它不僅只被你的進程使用而且會持續占用內存而不能被從內存中置換出存儲。申請的全部Dalvik和本地heap內存都是Dirty的,和Zygote共享的Dalvik和本地heap內存也都是Dirty的。
-- Dalvik Heap:Dalvik虛擬機使用的內存,包含dalvik-heap和dalvik-zygote,堆內存,所有的Java對象實例都放在這里。
-- Heap Alloc:累加了Dalvik和Native的heap。
-- PSS:這是加入與其他進程共享的分頁內存后你的應用占用的內存量,你的進程單獨使用的全部內存也會加入這個值里,多進程共享的內存按照共享比例添加到PSS值中。如一個內存分頁被兩個進程共享,每個進程的PSS值會包括此內存分頁大小的一半在內。
-- Dalvik Pss內存 = 私有內存Private Dirty + (共享內存Shared Dirty / 共享進程數)
-- TOTAL:上面全部條目的累加值,全局的展示了你的進程占用的內存情況。
-- ViewRootImpl:應用進程里的活動窗口視圖個數,可以用來監測對話框或者其他窗口的內存泄露。
-- AppContexts及Activities:應用進程里Context和Activity的對象個數,可以用來監測Activity的內存泄露。
1.3 內存回收
在Android的高級系統版本里面針對Heap空間有一個Generational Heap Memory的模型,最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最后累積一定時間再移動到Permanent Generation區域。系統會根據內存中不同的內存數據類型分別執行不同的gc操作。例如,剛分配到Young Generation區域的對象通常更容易被銷毀回收,同時在Young Generation區域的gc操作速度會比Old Generation區域的gc操作速度更快。
2、內存測試方法
基于上面的理論學習,可以知道內存問題基本上就是三種:內存抖動、內存泄漏、內存溢出。我們測試內存的時候也主要關注這三個測試點。至于用什么方法進行測試,下面簡單列舉一下工具,基本上網上都有關于工具使用很詳細的教程,在此不再詳細述說。
2.1 檢測內存抖動
內存抖動:大量的對象被創建又在短時間內馬上被釋放。
瞬間產生大量的對象會嚴重占用Young Generation的內存區域,當達到閥值,剩余空間不夠的時候,也會觸發GC。系統花費在GC上的時間越多,進行界面繪制或流音頻處理的時間就越短。即使每次分配的對象占用了很少的內存,但是他們疊加在一起會增加Heap的壓力,從而觸發更多其他類型的GC。這個操作有可能會影響到幀率,并使得用戶感知到性能問題。
Memory Monitor:查看整個app所占用的內存,以及發生GC的時刻,短時間內發生大量的GC操作是一個危險的信號(用于發現有沒有內存泄漏和嚴重內存抖動)。
存在內存抖動
2.2 檢測內存泄漏
內存泄露可以引發很多的問題:
1.程序卡頓,響應速度慢(內存占用高時JVM虛擬機會頻繁觸發GC)
2.莫名消失(當你的程序所占內存越大,它在后臺的時候就越可能被干掉。反之內存占用越小,在后臺存在的時間就越長)
3.直接崩潰(OutOfMemoryError)
內存泄漏無疑會嚴重影響用戶體驗,一些本應該廢棄的資源和對象無法被釋放,導致手機內存的浪費,app使用的卡頓,那么如何排查內存泄漏呢?
一個terminal指令:
adb shell dumpsys meminfo (package name)
這條指令是用來查詢這個進程所占用的內存的具體詳情的,通過這條指令可以看到當前app在手機中占用的具體的堆內存大小,view的數量,activity的數量等等。
其中activity數目是非常關鍵的一個信息,可以幫助我們快速地檢測出內存泄漏。我們可以反復地進入退出需要測試的目標activity,如果在反復進入退出之后,用terminal執行上面的語句查詢當前的內存情況,如果發現activity數量一直在增長,如上圖所示,APP退出后,再進入相當界面時Views和activity數量成倍地增長,則很大可能存在內存泄漏。
另外以下4個是用于定位的內存抖動和內存泄漏發生的具體位置:
- Allocation Tracker:
使用此工具來追蹤內存的分配.
但是事實上,通過觀察這個內存曲線的增長來或者是觀察allocate tracker中的allocate data數值的增長來檢測是否有內存泄漏問題,不太靠譜,因為往往內存泄漏發生了,但是GC仍然可以通過回收其他對象的方式騰出空間,導致這個數據的變化基本看不出來,甚至是減小的。
- Heap Tool:
查看當前內存快照,便于對比分析哪些對象有可能是泄漏了的。
- Memory monitor :
如果是Dalvik內存泄漏,也可以使用Android Device Monitor dump出一份hprof文件(別忘了先手工Cause GC),生成hprof文件進行測試分析。用hprof分析工具,可以檢測到泄漏的activities、分析出重復定義的字符串。
這里能夠實時地顯示應用程序占用的內存,很方便我們查看??偟膩碚f,就是使用monitor memory功能監測app主進程占用的內存,觸發GC操作,而后觀察內存的占用情況,如果在使用的過程中內存不斷增加,沒有回落,很有可能發生了內存泄漏,這時候就需要對生成的HPROF文件進行深入分析了。
使用HPROF文件分析工具標準步驟如下:
(1)打開Captures窗口,雙擊你想要查看的HPROF文件,打開HPROF文件查看工具界面;
(2)點擊Android Studio主窗口右邊欄上的Analyzer Tasks,默認HPROF文件分析工具會出現在HPROF文件查看工具的右邊。Analyzer Tasks列表中選擇你想分析的選項;
(3)點擊開始分析的按鈕;
(4)查看分析結果,點擊結果中條目可在HPROF文件分析工具中查看詳情。一般查看Retained Size占用最大的類,分析是否有內存泄漏。
附錄:
Class name
類名Total Count
該類的實例總數Heap Count
所選擇的堆中該類的實例的數量Sizeof
單個實例所占空間大小(如果每個實例所占空間大小不一樣則顯示0)Shallow Size
堆里所有實例大小總和(Heap Count * Sizeof)Retained Size
當該對象被GC回收時,所釋放掉的內存大小Instance
具體的實例Reference Tree
所選實例的引用,以及指向該引用的引用。Depth
GC根節點到所選實例的最短路徑的深度Shallow Size
所選實例的大小Dominating Size
所選實例所支配的內存大小
MAT
上述只是可以粗略的看出是不是有問題,而要知道問題出在哪里就需要借助MAT了。將生成的.hprof文件進行轉換,然后使用MAT打開來分析應用的內存使用情況。通常在使用MAT打開hprof文件后,能夠在首頁看到Top Comnsumers和 component Report等功能,我們可以快速定位一些大塊的內存消耗。
但我們在分析時會發現系統資源類占據了很大一部分內存,因此為去除這部分對分析的干擾,我們在使用AndroidSDK提供的hprof-conv轉換時需要增加一個參數:
hporf- conv [-z] <infile><outfile> -z:exclude non-app heaps,such as Zygote
如果hprof文件是已經轉換過的,則可以使用OQL:
//在數據中尋找應用的Application類對象,將對象地址轉換為十進制后輸入以下查詢語句:
select * from instanceof java.langObject s where s.@objectAddress> 1107296256
//(后面那串數字應該是Application類對象的地址)
采用這兩種方法后,再使用MAT來分析就可以比較容易發現自身代碼的內存問題。
MAT 是探索 Java 堆并發現問題和好幫手,能夠迅速發現常見的圖片和大數組等問題;
內存碎片問題一般隱藏在對象的地址中;
如需要測試非 Dalvik部分,有必要了解 Linux 的進程和內存原理、內存共享機制,熟悉常用命令行工具;
內存分配的最小單位是頁面,通常為4KB,這個限制會引發各種問題;
2.3 檢測OOM
Android系統的每個進程都有一個最大內存限制(這個閾值可以是48M、24M、16M等,視機型而定),如果申請的內存資源超過這個限制,系統就會拋出OOM錯誤。
PS:可以通過adb命令查看閾值
adb shell getprop | grep dalvik.vm.heapgrowthlimit
[dalvik.vm.heapgrowthlimit]: [192m]
Android 2.x系統,當dalvik allocated + external allocated + 新分配的大小 >= dalvik heap 最大值時候就會發生OOM。其中bitmap是放于external中 。
Android 4.x系統,廢除了external的計數器,類似bitmap的分配改到dalvik的java heap中申請,只要allocated + 新分配的內存 >= dalvik heap 最大值的時候就會發生OOM(art運行環境的統計規則還是和dalvik保持一致)
內存溢出是程序運行到某一階段的最終結果,直接原因是剩余的內存不能滿足內存的申請,但是再分析間接原因內存為什么沒有了:
內存泄漏的存在可能導致可用內存越來越少;
內存申請的峰值超過了系統時間點剩余的內存;(例如:某手機單個進程可用最大內存為192M,目前分配內存80M,此時申請5M內存,但是當前時間點整個系統可用內存只有3M,此時沒有超出單個進程可用最大內存,但是OOM也會發生)
2.4 常見內存測試場景
2.4.1 按各部分內存的用途設計場景
(1)比較操作前后或不同版本的內存變化
(2)顯示多張圖片的前臺進程
(3)多個場景來回切換
(4)長時間運行進程的內存增長
2.4.1 根據比較結果,確定問題方向
(1)Dalvik Heap內存
持續增長
內存泄露 -> LeakCanary / MAT
頻繁GC,大幅度波動
大量的分配和釋放 -> Allocation Tracker
比以前版本穩定增長
新功能及代碼改動 -> Heap Dump / MAT
Heap Alloc不變,PSS增加
可能存在內存碎片 -> Heap Dump / MAT
(2)非Heap內存
Dalvik Other
類信息
載入Class數正相關
mmaps
可執行代碼
常量
3、XX銀行APP性能評測-內存測試結果分析
3.1 總覽
從內存占用對比看,行業競品均值為351.3M,90分位約262.6M,75分位約339.5M,中位數約426.4M,25分位約605.7M?!鹃派藼ank】內存占用均值為246M,表現良好,打敗了行業90%以上的競品,請繼續保持哦。
3.2 啟動首頁加載內存問題分析--存在內存抖動
這里選取了啟動加載場景來進行內存問題的分析。
實際上從上面的總覽數據分析看內存占用不同場景對比很難發現內存問題,但是同一個場景內存占用曲線圖是可以發現問題的,如果曲線圖有鋸齒形的抖動且持續上升,基本上可能存在內存問題,如下圖可得出首頁存在內存抖動問題
另外還需要以日志輔助分析內存問題:從日志上看,1秒甚至幾百ms內就有一次GC,而且是主動GC,說明在頻繁申請內存,總阻塞耗時約5001 ms
注釋:
GC Reason:GC觸發原因
GC_CONCURRENT:當已分配內存達到某一值時,觸發并發GC;
GC_FOR_MALLOC:當嘗試在堆上分配內存不足時觸發的GC;系統必須停止應用程序并回收內存;
-GC_HPROF_DUMP_HEAP: 當需要創建HPROF文件來分析堆內存時觸發的GC;
-GC_EXPLICIT:當明確的調用GC時,例如調用System.gc()或者通過DDMS工具顯式地告訴系統進行GC操作等;
Amount freed GC:回收的內存大小
Heap stats:堆上的空閑內存百分比 (已用內存)/(堆上總內存)
Pause time:這次GC操作導致應用程序暫停的時間。關于這個暫停的時間,在2.3之前GC操作是不能并發進行的,也就是系統正在進行GC,那么應用程序就只能阻塞住等待GC結束。而自2.3之后,GC操作改成了并發的方式進行,就是說GC的過程中不會影響到應用程序的正常運行,但是在GC操作的開始和結束的時候會短暫阻塞一段時間。
4、App端內存問題排查思路:
(1)Service停止使用時,是否被銷毀
(2) 當界面變為不可見時,是否釋放當前界面的資源
(3)內存變少時,是否有釋放內存
(4) bitmap使用完之后是否被回收
(5)是否有大量第三方庫的消耗