本篇文章已授權(quán)微信公眾號 guolin_blog(郭霖)獨家發(fā)布
一、內(nèi)存的劃分
| 分類的標(biāo)準(zhǔn) | procrank | dumpsys meminfo | android studio profile | JMM |
|---|---|---|---|---|
| 劃分區(qū)域 | VSS/RSS/PSS/USS | Native/Dalvik/Cursor/Ashmem.. | java/native/graphics/stack/code/other | 方法區(qū)/java堆/java棧/native棧/程序計數(shù)器 |
1、procrank 是一個 adb 的 root 指令,可以查詢內(nèi)存的劃分:

-
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)存)
那么最值得關(guān)注的是
PSS和USS,我們可以用dumpsys meminfo來查詢(無需root權(quán)限)
2、dumpsys meminfo 查詢 pss 劃分

重點字段解讀:
-
Native Heap-Native堆內(nèi)存 -
Dalvik Heap-Dalvik虛擬機內(nèi)存,Dalvik虛擬機代碼在libdvm.so主要負責(zé)運行時dex解析成機器碼(android 5.0+ ART中已經(jīng)取消Dalvik虛擬機,這里任然出現(xiàn)Dalvik目測是沒改過來) -
.art mmap-Android RunTime映射內(nèi)存,art代碼在android_runtime.so,mmap(是linux C的一個函數(shù)接口,用來做內(nèi)存映射) -
Private Dirty- 進程獨占的內(nèi)存,內(nèi)存已經(jīng)被本進程修改過,只能被自己進程使用 -
Private clean- 進程獨占的內(nèi)存,內(nèi)存是映射過來的,沒有做修改,可以置換給到其他進程使用 -
java heap-Dalvik heap(dirty+clean) +art heap(dirty+clean)
通過上面圖片可得
launcher app占用的內(nèi)存是 250M,大部分內(nèi)存在Native Heap、code、graphics,那如何分析和解決,我們下面講。
3、android studio profile 是 ide 提供出來的分類:

-
Total- 整個應(yīng)用占據(jù)的總內(nèi)存 -
Java-java堆占據(jù)的內(nèi)存 -
Native-Native層調(diào)用malloc/new(C/C++)占據(jù)的內(nèi)存 -
Graphics- 圖形緩沖區(qū)隊列向屏幕顯示像素,(如果沒有用到OpenGL或者不是游戲,可以直接忽略) -
Stack- 線程棧占據(jù)的內(nèi)存 -
Code-dex+so庫占據(jù)的內(nèi)存 -
Others- 未解之謎
4、JMM 分類
- 方法區(qū) - 存放常量和靜態(tài)變量區(qū)域
-
java堆 -new出來的對象內(nèi)存都放在這里面 -
java棧 - 方法中執(zhí)行的基本類型變量 和 變量引用都在棧中。(類中的內(nèi)部成員屬性的引用在堆中) -
native棧 - 同java棧 ,指針引用都在native棧 中 - 程序計數(shù)器 - 作用是多線程切換記錄上一個線程執(zhí)行到的點。譬如:
A線程 切換到B線程。程序計數(shù)器要記錄A線程 已經(jīng)執(zhí)行到哪一行代碼。接著cpu切換到B線程,再切換回來A線程的時候,cpu才知道 從A線程哪一行代碼繼續(xù)執(zhí)行。
java 棧、native 棧、程序計數(shù)器是線程私有
java 堆、方法區(qū)是線程共有的。
二、java 內(nèi)存優(yōu)化
| Java 內(nèi)存優(yōu)化 | 內(nèi)存泄漏 | 內(nèi)存抖動 | 大內(nèi)存對象使用 |
|---|---|---|---|
| 發(fā)生的場景 | 單例、匿名內(nèi)部類、接口忘記釋放 ... | String拼接、循環(huán)內(nèi)重復(fù)生成對象 ... | HashMap、ArrayList ... |
1、Java 檢查泄漏 - LeakCanary 使用
1.1、 LeakCanary 結(jié)果分析
LeakCanary 可以檢查 Activity Fragment View 界面的泄漏問題。通過接入 LeakCanary
(接入庫地址) 跑上 monkey 接著靜等 java 內(nèi)存泄漏的出現(xiàn):

通過上圖可以知道
SearchActivity被HistorySource.mContext持有,HistorySource是一個單例,然后最頂層的Thread.contextClassLoader就是GC root(注意:靜態(tài)變量不是GC root),Thread.contextClassLoader是PathClassLoader類,只要把SearchActivity的context換成Application那就解決了。
1.2、Android 中 GC root 有哪些:
- 被
System ClassLoader加載過的類,繼而生成的對象,譬如rt.jar中的類 PathClassLoader、DexClassLoader- 活著的線程
Thread - 函數(shù)方法中的局部變量(跑在線程中的)
-
JNI中的全局變量和局部變量
1.3、LeakCanary 的核心原理:
- 通過
registerActivityLifecycleCallbacks()監(jiān)聽 各個Activity的退出 -
Activity退出后 ,拿到Activity的對象封裝成KeyedWeakReference弱引用對象。 - 通過手動
Runtime.getRuntime().gc();垃圾回收 - 通過
removeWeaklyReachableReferences()手動移除已經(jīng)被回收的對象 - 通過
gone()函數(shù)判斷是否被移除,如果移除了,說明Activity已經(jīng)沒有其他強引用 在引用它,沒有泄露 - 如果沒有移除,通過
android原生接口Debug.dumpHprofData(),把Hprof文件搞下來,通過haha這個第三方庫去解析是否有指定Activity的殘留。(haha是分析Hprof的java庫)
小結(jié):
那么LeakCanary 只能解決界面上的泄漏,其他內(nèi)存上的優(yōu)化是做不到的,譬如:線程池的泄漏,內(nèi)存的抖動,大對象的濫用.. 那么就需要更為強大的工具 MAT 了
2、內(nèi)存檢測工具 MAT
MAT 是分析內(nèi)存文件 hprof 的工具。(MAT 工具地址)
2.1 、抓取步驟
跑幾分鐘 monkey 后,退回應(yīng)用主界面,手動多次點擊GC 按鈕,把可回收的回收掉,為了剔除臟數(shù)據(jù)。通過 Android Studio 的 Profile 把 內(nèi)存文件 hprof 給 dump 下來。

- 進入
Android SDK目錄:G:\AndroidSDK\platform-tools - 把
dump下載的文件memory-20190828T162317.hprof 拖進 platform-tools文件夾 - 敲入
cmd命令hprof-conv memory-20190828T162317.hprof 1.hprof轉(zhuǎn)成可被MAT識別的1.hprof文件 - 使用
MAT打開1.hprof
2.2 、分析內(nèi)存:
完成以上步驟之后的結(jié)果圖

- 直接點擊左上角
Histogram查看內(nèi)存分布 -
objects- 對象數(shù)目 -
shallow heap- 對象自身實際占用的堆大小 -
retained heap- 對象被回收后能釋放多少內(nèi)存 -
Inspector- 可以看到對象的GC Root是誰

-
with outgoing references- 表示的是 當(dāng)前對象,引用了內(nèi)部哪些成員對象 -
with incoming references- 表示的是 當(dāng)前對象,被外部哪些對象應(yīng)用了(重點操作)

-
merge shortest paths to gc roots- 從GC roots到一個或一組對象的公共路徑 -
exclude all phantom/weak/soft etc. references- 排除一些類型的引用(如軟引用、弱引用、虛引用),留下強引用

- 為了避免查看太多并不是強相關(guān)的對象,直接從本應(yīng)用的
java類入手,MAT也提供正則式過濾,直接 輸入.*com.vd.*(本應(yīng)用 ``````packageName```) 去過濾,結(jié)果就非常明顯,整個應(yīng)用自己寫的對象占用的內(nèi)存都在這里。從大的對象下手,是否這個對象有存在的意義,是否需要占這么大的一個內(nèi)存。是否可以對其做相應(yīng)的處理。

-
MAT提供了更加方便的OQL查詢,可以找到指定一個名字的對象,包括可以根據(jù)本身java對象的成員屬性來做條件語句。譬如上圖我找長寬都大于100px的圖片都有哪些。可以把大圖片揪出來。
小結(jié)
可先用LeakCanary 跑出明顯的內(nèi)存泄漏,再用MAT 檢查整個應(yīng)用的內(nèi)存分布狀況,去優(yōu)化該優(yōu)化的 Java 堆內(nèi)存。
三、native 內(nèi)存優(yōu)化
| native 內(nèi)存優(yōu)化 | malloc_debug | heapsnap | DDMS |
|---|---|---|---|
| root權(quán)限 | 需要 | 需要 | 不需要 |
| 環(huán)境 | python | jni | 需要使用sdk18 的 tools/ddms.bat(sdk 18之后就被剔除了) |
-
malloc_debug是官方推薦的一種方法,目前效果還不錯 -
heapsnap是一個可以跑在Adnroid的C庫 github 開源庫 ,目前只能查詢內(nèi)存泄漏。而且編譯不過,原因是缺少了一些庫。在它基礎(chǔ)上我整合了一份編譯成功,有興趣點擊這里。 -
DDMS目前被遺棄,在android 9.0沒整成功,放棄。
1、malloc_debug 步驟
- 開啟
malloc debug模式,打開cmd窗口輸入
//查詢所有內(nèi)存
adb shell setprop wrap.packagename '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
//查詢內(nèi)存泄漏
adb shell setprop wrap.packagename '"LIBC_DEBUG_MALLOC_OPTIONS=leak_track logwrapper"'

- 關(guān)掉自身應(yīng)用,再打開,
monkey跑起來 - 通過
adb shell dumpsys meminfo com.all.videodownloader.videodownload查到pid為 2968

- 通過
adb shell am dumpheap -n <PID_TO_DUMP> /data/local/tmp/heap.txt把文件抓取出來到/data/local/tmp/heap.txt

- 把
native內(nèi)存文件 拷貝出來,等下分析

2、使用 python 分析
2.1、搭建環(huán)境
- 下載 native_heapdump_viewer.py
-
python編譯器我選擇了 PyCharm - 新建項目,把
native_heapdump_viewer.py和heap.txt,放到同一個目錄,如下圖
image.png
2.2、修改 python 代碼
- 修改
native_heapdump_viewer.py代碼中NDK配置地方:
resByte = subprocess.check_output(["G:/AndroidNDK/android-ndk-r17/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin/aarch64-linux-android-objdump", "-w", "-j", ".text", "-h", sofile])
p = subprocess.Popen(["G:/AndroidNDK/android-ndk-r17/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin/aarch64-linux-android-addr2line", "-C", "-j", ".text", "-e", sofile, "-f"], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
替換def __init__(self):函數(shù)中的部分代碼,把下面代碼:
if len(extra_args) != 1:
print(self._usage)
sys.exit(1)
替換為:
self.symboldir = "C:/Users/chaojiong.zhang/Documents/AndroidStudio/DeviceExplorer/xiaomi-mi_8-4b429b4"
extra_args.append("dump.txt")
-
self.symboldir- 就是dump.txt里面的內(nèi)存地址都需要 通過so庫來查找對應(yīng)的是哪一個函數(shù)。而so存放的父路徑地址就是self.symboldir,那么也就是說需要把 手機上的/system/lib64、/vendor/lib64/整個 文件夾pull 下來到電腦上,譬如這里是pull到
C:/Users/chaojiong.zhang/Documents/AndroidStudio/DeviceExplorer/xiaomi-mi_8-4b429b4
image.png 在
def main(): 函數(shù)插入部分代碼在函數(shù)第一行插入和最后一行插入以下代碼,目的是直接把結(jié)果log輸出到test.txt可以直接查看。
def main():
sys.stdout = open("test.txt", "w")
//...
sys.stdout.close()
-
跑起來
image.png
3、malloc_debug 內(nèi)存文件分析
3.1、字段解讀
-
BYTES- 占用的內(nèi)存大小單位byte -
%TOTAL- 占總native內(nèi)存百分比 -
%PARENT- 占父幀內(nèi)存百分比 -
COUNT- 調(diào)用了多少次 -
ADDR- 內(nèi)存地址 -
LIBRARY- 占用的內(nèi)存所屬哪一個so庫 -
FUNCTION- 占用的內(nèi)存所屬哪一個方法 -
LOCATION- 占用的內(nèi)存所屬哪一行
3.2、內(nèi)存信息分析一
10285756 58.29% 99.95% 49 eac0b276 /system/lib/libhwui.so android::Bitmap::allocateHeapBitmap(SkBitmap*)
可以看得出來 allocateHeapBitmap方法占用了,10M 左右的內(nèi)存,占總 native 內(nèi)存 58.29%,占父幀 99.95% (意思是:A-> B ,A方法調(diào)用B方法,A方法總共占用了 10M,其中9.9M 是在B方法中申請的,那么 %PARENT 就是 99%),調(diào)用了49 次,動作發(fā)生在 libhwui.so 中的 android::Bitmap::allocateHeapBitmap方法。下面是 allocateHeapBitmap 被調(diào)用的流程:
BitmapFactory.decodeResource -> BitmapFactory.nativeDecodeStream ->BitmapFactory.cpp 中 nativeDecodeStream() -> doDecode() -> SkBitmap.tryAllocPixels() -> ... -> android::Bitmap::allocateHeapBitmap()
Bitmap.createBitmap -> nativeCreate() -> Bitmap.cpp 中的 nativeCreate() -> GraphicsJNI.cpp zhong de allocateJavaPixelRef() -> ... -> android::Bitmap::allocateHeapBitmap()
也就是說java層的 bitmap 創(chuàng)建都會跑到 allocateHeapBitmap 這個函數(shù)。那么上面這個占用了 10M 的 allocateHeapBitmap,究竟是 java 層哪個類調(diào)用下來的,這個目前是無解(包括最近華為的方舟環(huán)境平臺 DevEco 也不行),只能在 java 層去全盤查詢了,哪些圖片使用了較多的內(nèi)存。
3.3、內(nèi)存信息分析二

-
WebViewGoogle.apk占用了10M的內(nèi)存,WebViewGoogle.apk就是應(yīng)用使用的WebView,android 5.0之前作為模塊存在于frameworks/base目錄下,并提供接口。android 5.0之后變成了編譯為一個獨立的apk,包名是com.android.webview。檢查所有的WebView使用情況,譬如:如果場景允許,使用完畢是否有 調(diào)用WebView.clearCache() -
boot-framework.oat占了5M,Android framework代碼通過dex2oat轉(zhuǎn)成的oat二進制文件(機器碼),無需優(yōu)化 -
libandroid_runtime.so占了5M,虛擬機內(nèi)存,屬于按比例劃分共享庫,無需優(yōu)化
小結(jié)
native 內(nèi)存目前無法很清晰的定位到對應(yīng)的java 層代碼,無解。只能看個大概,然后有目的性去排查某個類,或者某個模塊。
四、graphics 內(nèi)存優(yōu)化
若應(yīng)用沒有自己接入 OpenGL/ GL surfaces/ GL textures開源庫,來繪制圖形,可不必理會。畢竟已經(jīng)超出 android應(yīng)用工程師的范圍了。
五、stack 內(nèi)存優(yōu)化
1、解決棧溢出
1.1、死循環(huán)問題
-
JDK 1.8之前的HaskMap,避免使用多線程造成死循環(huán)問題。
1.2、遞歸問題
- 避免深層次的遞歸問題,較深層次的遞歸可采用尾遞歸的方法。
- 遞歸的退出,最好用標(biāo)識位退出?;蛘咄ㄟ^線程
interrupt(),isInterrupted()去退出遞歸,確保遞歸正確退出。遞歸中如果有Thread.sleep,要注意中斷被消費問題。
1.3、Intenet 問題
- 對于
Intent傳遞大對象,或者ArrayList<Info>,Intent的上限是505K。
解決方案:
- 一般通過
static持有需要傳遞的對象解決。 - 把跳轉(zhuǎn)的頁面寫成
fragment,數(shù)據(jù)可以不需要傳遞也可獲取 - 通過
EventBus RxBus(原理都是通過全局單例來傳遞) - 通過 ObjectCache 把對象轉(zhuǎn)成
json串,保存到本地,獲取時候序列化為對象。
2、解決重復(fù)生成局部變量
2.1、避免在循環(huán)內(nèi)重復(fù)生成局部變量:
private void memoryShake() {
ArrayList<Integer> shakes = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Integer shake = new Integer(i);
shakes.add(shake);
}
}
private void memoryShake1() {
ArrayList<Integer> shakes = new ArrayList<>();
Integer shake;
for (int i = 0; i < 100; i++) {
shake = new Integer(i);
shakes.add(shake);
}
}
memoryShake() 會在 循環(huán)內(nèi)生成 100 個 shake 局部變量 + 100 個局部變量的引用,
memoryShake1()會在 循環(huán)內(nèi)生成 100 個 shake 局部變量 + 1 個局部變量的引用,一個對象引用在 64bit 的環(huán)境是 8byte 。100*8 = 800 byte = 0.8KB
2.2、String 使用問題
循環(huán)內(nèi)字符的拼接不要使用 + 符號,(使用 + 符號,編譯成字節(jié)碼后,循環(huán)內(nèi)會生成StringBuilder 對象去拼接)。
正確應(yīng)該使用StringBuffer (線程安全)或者 StringBuilder(線程不安全)。
六、code 內(nèi)存優(yōu)化
code 內(nèi)存消耗主要是: so 庫,dex ,ttf。
以上三種文件都是要加載到運行內(nèi)存才能被解析運行,所以它們的體積要算進自身的應(yīng)用內(nèi)存中。
so庫,可以通過STRIP去掉一些符號表 和調(diào)試信息,在Android.mk加入LOCAL_STRIP_MODULE:= true,即可。dex,是java代碼編譯成的字節(jié)碼,沒混淆的apk中的dex會大很多,混淆后的dex會小很多,所以debug包的內(nèi)存占用會大于release包。Android Studio 3.3帶了了一個新特新R8壓縮,可以在gradle.properties加入android.enableR8=true,減小dex包的體積(完美兼容現(xiàn)有混淆)。當(dāng)然還要剔除自身應(yīng)用的無用代碼,可使用
Android Studio Menu > Refactor > Remove Unused Resources進行排查,這里不再詳細展開。ttf- 如果應(yīng)用中只用到部分字體,可通過 FontZip 提取使用的字體。
七、other 內(nèi)存優(yōu)化
目前不清楚這部分是哪部分內(nèi)存,無從下手,不過一般 other 的內(nèi)存占用比例都是比較小,可不必理會。


