了解 meminfo

遇到内存问题时,一般首先会用 adb shell dumpsys meminfo -a [进程名/包名/进程pid]命令输出某个应用某个时刻的整体内存值,或通过持续调用该命令并输出以观察内存的变化(类似于 AS Profiler 中 View Live Telemetry 那样)。一般dumpsys meminfo命令输出如下:


其中的关键指标说明:

字段名 说明
Pss Total 实际按比例计算的物理内存使用量。任何独占的内存页直接计算它的PSS值,而和其它进程共享的页则按照共享的比例计算PSS值
Private dirty 仅该进程独占且已修改的页。代表真正独占且无法被共享的物理内存
Private clean 仅该进程映射、但尚未写入的页(可被释放或重映射)
SwapPss dirty 表示这部分物理内存已被换出(swap out),此字段仅在设备启用 zram/swap 时有效
Rss total 实际驻留物理内存的页总量(包含共享)。不做分摊,比 PSS 更大
Heap size 逻辑堆容量(最大总共可分配空间)
Heap alloc 当前堆中已分配(使用)的空间
Heap free 堆中总的剩余空间

各区域说明:

分类名 含义 说明主要用途 / 常见问题方向
Native Heap C/C++ 层堆内存 使用 malloc/new/calloc 等分配的内存
Dalvik Heap Java 虚拟机堆(由 ART / Dalvik 管理) Java 对象(String、List、Bitmap 等)
Dalvik Other Dalvik 虚拟机的其他内存 Dex cache、JIT 代码、GC 元数据等
Stack 各线程的栈内存 每个线程约 512KB~1MB,线程多会增大
Ashmem 通过 /dev/ashmem分配 匿名共享内存
Gfx dev GPU 图形设备内存 OpenGL / Vulkan 缓冲、纹理、EGL surface
Other dev 其他设备驱动相关内存 例如音频 buffer、摄像头驱动缓冲区
.so mmap 通过 mmap 映射的共享库 C/C++ 代码段和只读数据
.jar mmap 通过 mmap 映射的 jar 文件 一般为 framework.jar 等
.apk mmap 通过 mmap 映射的 apk 文件 资源访问(图片、xml、assets)
.ttf mmap 字体文件映射 字体缓存,少量常驻
.dex mmap Dex 文件映射 ClassLoader 加载 dex 内存
.oat mmap OAT 文件映射 运行时优化代码,常为只读
.art mmap ART 虚拟机自身映射 ART runtime metadata
Other mmap 其他无法归类的 mmap 区域 一般是内存缓存或中间层分配
EGL mtrack EGL 映射跟踪 GPU 图形缓冲区
GL mtrack OpenGL 内存跟踪 GL 纹理、FrameBuffer、Shader cache
Unknown 未能分类的内存区域 可能是匿名 mmap、Binder、临时区域

app Summary 各个项目的计算规则:

类别名 内存构成(计算规则)
Java Heap Dalvik Heap 的 Private Dirty + .art mmap 的 Private Dirty + .art mmap 的 Private Clean
Native Heap Native Heap 的 Private Dirty
Code .so mmap.jar mmap.apk mmap.ttf mmap.dex mmap.oat mmap.ZygoteJIT.AppJIT上述 8 项的 Private Dirty + Private Clean 总和
Stack Stack 的 Private Dirty
Graphics Gfx devEGL mtrackGL mtrack上述 3 项的 Private Dirty + Private Clean 总和
Private Other Total Private Dirty + Total Private Clean - Summary Java heap - Summary Native Heap - Summary Code - Summary Stack -Summary Graphics
System Total Pss - Total Private Dirty - Total Private Clean
TOTAL PSS Summary Java Heap + Summary Native Heap + Summary Code + Summary Stack + Summary Graphics + Summary Private Other + Summary System也等于 Native Heap 、Dalvik Heap、Dalvik Other、Stack、Ashmem、Gfx dev、Other dev、.so mmap、.jar mmap、.apk mmap、.ttf mmap、.dex mmap、.oat mmap、.art mmap、Other mmap、EGL mtrack、GL mtrack、Unknown 的 Pss Total 和 SwapPss Dirty 之和
TOTAL SWAP PSS Native Heap 、Dalvik Heap、Dalvik Other、Stack、Ashmem、Gfx dev、Other dev、.so mmap、.jar mmap、.apk mmap、.ttf mmap、.dex mmap、.oat mmap、.art mmap、Other mmap、EGL mtrack、GL mtrack、Unknown 的 SwapPss Dirty 之和

另外,也可以通过dumpsys meminfo的输出关注 Views、AppContexts、Activities 的数量,以判断是否有内存泄漏。

如果发现 meminfo 中输出的 Views 、Activities 异常增多时,需要关注是不是出现了 View 、Activities 的泄漏。

分析引用库

在分析App不同版本的内存差异的时候,如果遇到 Code 段内存变化较大,可能是因为新版本引入了新的三方库,导致 dex 大小增加,进而导致在运行时的 Code 段内存增大(.dex mmap 或 .oat mmap 增加)。

这时候可以对比前后版本引用的库有哪些差异,以此作为依据,方便后续分析具体是哪些功能模块导致的 Code 段内存增加:

  1. 通过 gradlew app:dependencies --configuration releaseCompileClasspath 获取 release 版引用的三方库列表
  2. 通过脚本将上一步的输出整理为表格,便于分析
  3. 根据结果可以大致知道新增了哪些三方库,dex文件增加大概是哪些三方库导致的
  4. 也可以通过分析哪些三方库是非必须的(这需要对业务比较了解)

引用库对比在包大小分析中也很有用

分析 java heap

debuggable 的应用 (release 包可以通过在manifest中设置 debuggable = true) 可以通过Android Studio Profiler 工具 dump java heap。

如果是 root 过的手机,可以通过如下命令来 dump java heap。这种方式方便在执行自动化脚本的时候自动导出 java 堆。

1
2
3
4
# pid 为进程id, 最后一个参数为dump转储文件存放在手机上的路径,一般可以写shell有权限写的路径。
adb shell am dumpheap [pid] /data/local/tmp/xxx.hprof
# 执行完上述命令后,可以通过 adb pull 命令将手机上的 dump 文件传到电脑上
adb pull /data/local/tmp/xxx.hprof ~/xxx.hprof

针对 release 包导出的 java heap 转储文件,大多数类都会被混淆:

可以借助 leakcanary-shark 来解析 hprof 文件。同时,可以通过传入 mapping 文件,将对应的类、方法等名称还原:

1
2
3
4
5
6
7
8
// hprofFile 为 java heap 转储文件
Hprof.open(hprofFile).use { hprof ->
// mapping 为release 包对应的 mapping 文件
val graph = HprofHeapGraph.indexHprof(hprof, ProguardMappingReader(File(mapping).inputStream()).readProguardMapping())
// 之后就可以使用 graph 的相关 api 查询的 java 对象实例个数等信息,
// 具体可参考: https://github.com/square/leakcanary/blob/main/shark/shark-graph/src/main/java/shark/HeapGraph.kt
graph.classes.forEach {...}
}

使用 leakcanary-shark 还可以做很多分析:

  1. 开机内存分析:开机之后 dump java 堆,然后使用 leakcanary-shark 分析开机时加载了哪些类,对应类加载了多少个实例。

  2. View、Activity 等对象泄漏分析:前面提到 meminfo 会输出当前应用的 View、Activity 个数,使用 Java heap ,借助 leakcanary-shark 可以方便输出 View、Activity 的个数以及其到 GC Root 引用链(借助findShortestPathsFromGcRoots),或者分析某些对象是否是泄漏的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Marks any instance of com.example.ThingWithLifecycle with
// ThingWithLifecycle.destroyed=true as leaking
val leakingObjectFilter =
object : LeakingObjectFilter {
override fun isLeakingObject(heapObject: HeapObject): Boolean {
return if (
heapObject is HeapInstance && heapObject instanceOf "com.example.ThingWithLifecycle"
) {
val destroyedField = heapObject["com.example.ThingWithLifecycle", "destroyed"]!!
destroyedField.value.asBoolean!!
} else false
}
}

val leakingObjectFinder = FilteringLeakingObjectFinder(listOf(leakingObjectFilter))

fun main(args: Array<String>) {
val heapDumpFile = File(args[0])
val heapAnalysis =
Hprof.open(heapDumpFile).use { hprof ->
val heapGraph = HprofHeapGraph.indexHprof(hprof)
val heapAnalyzer = HeapAnalyzer({})
heapAnalyzer.analyze(
heapDumpFile = heapDumpFile,
graph = heapGraph,
leakingObjectFinder = leakingObjectFinder,
)
}
println(heapAnalysis)
}

当然,直接借助 Android Studio 也可以分析 java heap 转储文件:一般关注 Allocations、Shallow size、Retained size、Native size、Depth 这些属性:


这些属性的意义:

Allocations: 只在类级别有。表示当前类有多少个存活的实例(对象)。

Shallow size: 类级别和实例级别都有,且:

  • 类级别的 Shallow size 为某个类的所有实例的 Shallow size 之和。
  • 实例级别的 Shallow size 为单个实例本身占用内存的大小。计算规则:当前对象所有非静态属性(包括从父类继承的属性)所占用的字节数总和。以一个Bitmap对象举例,其 Shallow size 为 62:

属性 类型 内存 说明
shadow$klass Class<?>(引用类型) 4 Android java.lang.Object 类特有的属性
shadow$monitor int 4 Android java.lang.Object 类特有的属性
mBitmapExt、mNinePatchChunk、mNinePatchInsets、mHardwareBuffer、mColorSpace、mGainmap 引用类型 6 * 4 = 24 非值类型(引用类型)的属性都是占用 4 字节
mId、mNativePtr long 8 * 2 = 16 int,float 占4字节,long,double 占8字节,short,char占2个字节, byte,boolean 占1字节
mDensity、mHeight、mWidth int 4 * 3 = 12
mRecycled、mRequestPremultiplied boolean 1 * 2 = 2

Retained size: 类级别和实例级别都有,且:

类级别的 Retained size 是指该类的 Class 对象的 Retained size, 注意,它并不等于某个类的所有实例的 Retained size 之和。仍然以 Bitmap 举例:

上图中类级别 Bitmap 的 Retained size 是 783。它等于对应的 Class 实例的 Retained size(可以选中某个Bitmap实例后,在其shadow$_klass_属性上右键->Go to Instance找到对应的 Class 实例 ):

  • 而实例级别的 Retained size 则表示单个对象被GC时所能回收到内存的总和,他的值总是 >= 单个实例的 Shallow size。举个例子:

图1
图2

上面 2 个图中蓝色节点代表仅仅只有通过 obj1 才能直接或间接访问的对象。第一个图的 obj3 不是蓝色节点,因为其可以通过 GC Roots 访问。所以对于第一个图,obj1 的 retained size 是 obj1、obj2、obj4 的 shallow size 总和;第二个图的 retained size 是 obj1、obj2、obj3、obj4 的 shallow size 总和。obj2 的 retained size 可以通过相同的方式计算。

Native size: 类级别和实例级别都有,实例级别的表示单个对象所引用的 native 内存大小。类级别则是所有实例引用的 native 内存总和。

Depth: 从任意 GC Root 到选定实例的最短路径的深度,为 0 则表示可以作为GC Root, 为 1 则被 GC root 直接应用,以次类推。

在使用 Android Studio 对 java heap 进行分析时:

  1. Allocations 数比较大,且非 JDK 相关的对象需要关注。
  2. Allocations 少但 Shallow size 比较大的对象,说明是大对象,需要重点关注。
  3. Native Size 比较大的对象(一般为Bitmap),也需要重点关注

分析 smpas

当要分析 .xxx mmap 等占用内存较多或者想要再具体了解 Native heap 、Java heap 内存都是那些模块占用了的时候,smaps 就比较有用。

可以通过如下命令抓取 smaps,前提是手机需要root:

1
2
# pid 为进程id
adb shell cat /proc/$pid/smaps > ./smaps.log

关于 smaps 文件结构的解读,可以参考这个文档:https://github.com/Gracker/Android-App-Memory-Analysis/blob/master/docs/zh/smaps_interpretation_guide.md

抓取之后可以通过 smaps_parser.py 格式化输出,格式如下(原输出太长,有删减):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
Dalvik (Dalvik虚拟机运行时内存) : 45.765 MB
PSS: 45.765 MB
[anon:dalvik-main space (region space)] : 34712 kB
[anon:dalvik-free list large object space] : 5211 kB
[anon:dalvik-non moving space] : 4956 kB
[anon:dalvik-zygote space] : 886 kB
SwapPSS: 0.052 MB
[anon:dalvik-zygote space] : 42 kB
[anon:dalvik-free list large object space] : 10 kB

Dalvik Other (Dalvik虚拟机额外内存) : 15.952 MB
PSS: 15.952 MB
[anon:dalvik-LinearAlloc] : 15196 kB
[anon:dalvik-region space live bitmap] : 304 kB
[anon:dalvik-local ref table] : 156 kB
[anon:dalvik-allocspace non moving space live-bitmap 1] : 20 kB
SwapPSS: 0.000 MB

Stack (线程栈内存) : 11.092 MB
PSS: 11.092 MB
[anon:stack_and_tls:14216] : 228 kB
[stack] : 164 kB
[anon:stack_and_tls:14909] : 136 kB
[anon:stack_and_tls:14908] : 136 kB
[anon:stack_and_tls:15051] : 132 kB
SwapPSS: 0.000 MB

Ashmem (匿名共享内存) : 0.174 MB
PSS: 0.174 MB
/dev/ashmem/shared_memory/9972DCF27EAD19B1851F16B6AC616BED (deleted) : 128 kB
/dev/ashmem/shared_memory/76DA17E14D90B7C2CCCFA8ADCF6E788F (deleted) : 32 kB
/dev/ashmem/fontMap (deleted) : 12 kB
/dev/ashmem/GFXStats-14123 (deleted) : 2 kB
SwapPSS: 0.000 MB

Other dev (其他设备内存) : 0.105 MB
PSS: 0.105 MB
/dev/binderfs/binder : 64 kB
/dev/zero (deleted) : 32 kB
/dev/binderfs/hwbinder : 8 kB
/dev/__properties__/u:object_r:agp_support_anim_pause_render:s0 : 1 kB
SwapPSS: 0.000 MB

.so mmap (动态链接库映射内存) : 9.248 MB
PSS: 9.248 MB
/vendor/lib64/libllvm-qgl.so : 1518 kB
/system/lib64/libhwui.so : 531 kB
/vendor/lib64/egl/libGLESv2_adreno.so : 333 kB
/system/lib64/libandroid_runtime.so : 139 kB
SwapPSS: 0.001 MB
/system/lib64/libagp.so : 1 kB

.jar mmap (JAR文件映射内存) : 2.175 MB
PSS: 2.175 MB
/system/framework/framework.jar : 1729 kB
/system/framework/framework-magic.jar : 203 kB
/system/framework/ims-common.jar : 8 kB
/system/framework/hwcustTelephony-common.jar : 8 kB
SwapPSS: 0.000 MB

.apk mmap (APK文件映射内存) : 129.413 MB
PSS: 129.413 MB
/data/app/~~xxxxx==/com.xxxx.xxxx-xxxx==/base.apk : 109293 kB
/product/app/WebViewGoogle/WebViewGoogle.apk : 19127 kB
/system/framework/framework-res-hnext.apk : 122 kB
SwapPSS: 0.000 MB

.ttf mmap (字体文件映射内存) : 9.257 MB
PSS: 9.257 MB
/system/fonts/HONORSansVFCN.ttf : 7376 kB
SwapPSS: 0.000 MB

.dex mmap (DEX字节码文件映射内存) : 5.195 MB
PSS: 5.195 MB
/data/dalvik-cache/arm64/product@app@WebViewGoogle@WebViewGoogle.apk@classes.vdex : 1225 kB
/data/dalvik-cache/arm64/product@app@WebViewGoogle@WebViewGoogle.apk@classes.dex : 121 kB
[anon:dalvik-/system/framework/framework.jar-classes3.dex-transformed] : 20 kB
/memfd:/system/framework/arm64/boot.vdex (deleted) : 17 kB
SwapPSS: 0.000 MB

.oat mmap (编译后的安卓应用程序映射内存) : 0.018 MB
PSS: 0.018 MB
/memfd:/system/framework/arm64/boot.oat (deleted) : 18 kB
SwapPSS: 0.000 MB

.art mmap (ART运行时文件映射内存) : 7.728 MB
PSS: 7.728 MB
/memfd:/boot-image-methods.art (deleted) : 6577 kB
[anon:dalvik-/system/framework/boot.art] : 1146 kB
/memfd:/system/framework/arm64/boot.art (deleted) : 5 kB
SwapPSS: 0.026 MB
[anon:dalvik-/system/framework/boot.art] : 26 kB

针对 .xxx mmap 这类映射内存,smaps 可以很清晰地看出是哪些文件映射的内存,有多大。但是对于 java heap 尤其是 native heap 使用 smaps 就比较难分析了。

分析线程快照

debuggable 的应用可以通过 Android Studio 的 debug 工具 dump 当前应用的线程快照。但是这个只能导出 java 的线程。如果是纯 native 的线程则没法用这种方式导出


Android Studio profiler 工具在查看实时内存信息的时候也会显示当前应用的线程数量,但是没法展示当前的所有线程的快照


root 的手机可以通过如下命令 dump 当前应用的所有线程快照(包括 native 线程):

1
2
# pid 表示应用的进程id
adb shell debuggerd -b $PID > .threads_dump.log

输出内容如下(节选):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
"u.input_hihonor" sysTid=11298
#00 pc 00000000000c1188 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb)
#01 pc 0000000000011f7c /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+212) (BuildId: ca44f7bb04e1ac79b71c5ff2900fb995)
#02 pc 00000000001be784 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44) (BuildId: cf508d4b956d9fcaf87dad152decb09b)
#03 pc 0000000000399170 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#04 pc 0000000002038cc4 /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (android.os.MessageQueue.next+292)
#05 pc 00000000028f4f84 /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (android.os.Looper.loopOnce+100)
#06 pc 000000000205ed7c /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (android.os.Looper.loop+284)
#07 pc 000000000253670c /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (android.app.ActivityThread.main+3852)
#08 pc 0000000000382c40 /apex/com.android.art/lib64/libart.so (art_quick_invoke_static_stub+640) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#09 pc 000000000037e684 /apex/com.android.art/lib64/libart.so (_jobject* art::InvokeMethod<(art::PointerSize)8>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jobject*, _jobject*, unsigned long)+732) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#10 pc 00000000006b4bc8 /apex/com.android.art/lib64/libart.so (art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*) (.__uniq.165753521025965369065708152063621506277)+32) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#11 pc 0000000000399170 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#12 pc 0000000002ce0464 /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run+132)
#13 pc 0000000000760444 /apex/com.android.art/lib64/libart.so (nterp_helper+7636) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#14 pc 00000000002eb676 /system/framework/framework.jar (com.android.internal.os.ZygoteInit.main+690)

"Jit thread pool" sysTid=11308
#00 pc 0000000000083cbc /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb)
#01 pc 0000000000266780 /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+152) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#02 pc 00000000006592cc /apex/com.android.art/lib64/libart.so (art::ThreadPoolWorker::Run()+208) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#03 pc 00000000004d9298 /apex/com.android.art/lib64/libart.so (art::ThreadPoolWorker::Callback(void*)+164) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#04 pc 000000000006eafc /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+196) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb)
#05 pc 0000000000061664 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb)

"HeapTaskDaemon" sysTid=11309
#00 pc 0000000000083cbc /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb)
#01 pc 0000000000266780 /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+152) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#02 pc 00000000003123c0 /apex/com.android.art/lib64/libart.so (art::gc::TaskProcessor::RunAllTasks(art::Thread*)+912) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#03 pc 0000000000399170 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#04 pc 0000000002406b80 /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (java.lang.Daemons$HeapTaskDaemon.runInternal+192)
#05 pc 000000000240408c /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (java.lang.Daemons$Daemon.run+124)
#06 pc 000000000211960c /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (java.lang.Thread.run+76)
#07 pc 0000000000382974 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#08 pc 000000000036986c /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+132) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#09 pc 000000000093fac8 /apex/com.android.art/lib64/libart.so (art::detail::ShortyTraits<(char)86>::Type art::ArtMethod::InvokeInstance<(char)86>(art::Thread*, art::ObjPtr<art::mirror::Object>, art::detail::ShortyTraits<>::Type...)+60) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#10 pc 00000000004cf6ec /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback(void*)+1604) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7)
#11 pc 000000000006eafc /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+196) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb)
#12 pc 0000000000061664 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb)

通过线程快照可以用于分析当前应用有多少个线程正在运行,这些线程是属于哪些模块,可以帮助辅助定位一些线程数量多导致的内存或其他性能问题(比如耗电)。

分析 native 内存泄漏

native 内存不像 java heap 那样可以 dump 整个堆进行分析,要进行内存泄漏分析也不容易。这里主要介绍 3 个工具

Perfetto 跟踪 Native Allocations

Perfetto 工具基本使用方式可以参考这位博主的系列文章:https://androidperformance.com/2024/05/21/Android-Perfetto-01-What-is-perfetto/

用 Perfetto 可以实时跟踪 Native 层的内存分配(通过 malloc、calloc 之类的方法分配的内存),它跟使用 Android Studio 中的 Track Memory Consumption(Native Allocations)是一样的,只是可以配置的参数更多、更自由,也更适合用自动化脚本处理。

具体方式是:

抓取的数据中会显示采样聚合的 native 内存分配调用栈,可以有四个维度展示这些调用栈:

* Unreleased Malloc Size:申请但是没有释放的内存大小
* Unreleased Malloc Count:申请但是没有释放的次数(相关于 申请次数 - 释放次数)
* Total Malloc Size:申请内存的总大小
* Total Malloc Count:申请内存的总次数

一般需要关注Unreleased Malloc Size,这部分可能是内存泄漏,当然也不绝对,有可能这部分内存只是还不到释放的时机,不应该释放。需要根据堆栈 case by case 分析处理。

koom-native-leak

koom-native-leak 是快手开发的用于监控 Native 内存泄漏的库。它通过 hook malloc/free 等内存分配器方法来跟踪记录内存分配与回收,以分析是否有内存泄漏。

详情可见:https://github.com/KwaiAppTeam/KOOM/blob/master/koom-native-leak/README.zh-CN.md

需要说明的是:测试下来 koom-native-leak 目前只在 Android 10 (API 29)的设备上有效果,其他版本基本抓不到任何 native 内存泄漏的 case 。

MemoryLeakDetector

MemoryLeakDetector (Raphael) 是字节跳动开发的用于监控 Native 内存泄漏的库。原理同 koom-native-leak 类似,都是通过 hook malloc/free 等内存分配器方法来记录内存的分配和回收。不过 MemoryLeakDetector 兼容性更好一些,目前测试下来各个版本的系统都没有遇到大问题。

关于 MemoryLeakDetector 详情可见官方文档:https://github.com/bytedance/memory-leak-detector

需要说明的是:MemoryLeakDetector mmap 也会监控 mmap 等函数。但对于内存分析来说,使用 mmap 申请的内存,并不代表实际使用的内存,在未实际使用对应的内存时,是不计入 PSS 中。

另外,还需要注意的是无论是 MemoryLeakDetector 还是 koom-native-leak ,都不能保证检测到的未释放内存 100% 就是内存泄漏,也不能保证能保证所有的内存泄漏都能被检测到。