Android 内存分析方法记录三
///////////////////////////////////////////////////////////////////////////////////////////////////////////遇到内存问题时一般首先会用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 HeapC/C 层堆内存使用 malloc/new/calloc 等分配的内存Dalvik HeapJava 虚拟机堆由 ART / Dalvik 管理Java 对象String、List、Bitmap 等Dalvik OtherDalvik 虚拟机的其他内存Dex cache、JIT 代码、GC 元数据等Stack各线程的栈内存每个线程约 512KB1MB线程多会增大Ashmem通过 /dev/ashmem分配匿名共享内存Gfx devGPU 图形设备内存OpenGL / Vulkan 缓冲、纹理、EGL surfaceOther dev其他设备驱动相关内存例如音频 buffer、摄像头驱动缓冲区.so mmap通过 mmap 映射的共享库C/C 代码段和只读数据.jar mmap通过 mmap 映射的 jar 文件一般为 framework.jar 等.apk mmap通过 mmap 映射的 apk 文件资源访问图片、xml、assets.ttf mmap字体文件映射字体缓存少量常驻.dex mmapDex 文件映射ClassLoader 加载 dex 内存.oat mmapOAT 文件映射运行时优化代码常为只读.art mmapART 虚拟机自身映射ART runtime metadataOther mmap其他无法归类的 mmap 区域一般是内存缓存或中间层分配EGL mtrackEGL 映射跟踪GPU 图形缓冲区GL mtrackOpenGL 内存跟踪GL 纹理、FrameBuffer、Shader cacheUnknown未能分类的内存区域可能是匿名 mmap、Binder、临时区域app Summary 各个项目的计算规则类别名内存构成计算规则Java HeapDalvik Heap 的 Private Dirty .art mmap 的 Private Dirty .art mmap 的 Private CleanNative HeapNative Heap 的 Private DirtyCode.so mmap.jar mmap.apk mmap.ttf mmap.dex mmap.oat mmap.ZygoteJIT.AppJIT上述 8 项的 Private Dirty Private Clean 总和StackStack 的 Private DirtyGraphicsGfx devEGL mtrackGL mtrack上述 3 项的 Private Dirty Private Clean 总和Private OtherTotal Private Dirty Total Private Clean - Summary Java heap - Summary Native Heap - Summary Code - Summary Stack -Summary GraphicsSystemTotal Pss - Total Private Dirty - Total Private CleanTOTAL PSSSummary 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 PSSNative 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 段内存增加通过gradlew app:dependencies --configuration releaseCompileClasspath获取 release 版引用的三方库列表通过脚本将上一步的输出整理为表格便于分析根据结果可以大致知道新增了哪些三方库dex文件增加大概是哪些三方库导致的也可以通过分析哪些三方库是非必须的这需要对业务比较了解引用库对比在包大小分析中也很有用分析 java heapdebuggable 的应用 (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 还可以做很多分析开机内存分析开机之后 dump java 堆然后使用 leakcanary-shark 分析开机时加载了哪些类对应类加载了多少个实例。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.destroyedtrue 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: ArrayString) { 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$klassClass?引用类型4Android java.lang.Object 类特有的属性shadow$monitorint4Android java.lang.Object 类特有的属性mBitmapExt、mNinePatchChunk、mNinePatchInsets、mHardwareBuffer、mColorSpace、mGainmap引用类型6 * 4 24非值类型引用类型的属性都是占用 4 字节mId、mNativePtrlong8 * 2 16int,float 占4字节long,double 占8字节short,char占2个字节, byte,boolean 占1字节mDensity、mHeight、mWidthint4 * 3 12mRecycled、mRequestPremultipliedboolean1 * 2 2Retained 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。举个例子上面 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 进行分析时Allocations 数比较大且非 JDK 相关的对象需要关注。Allocations 少但 Shallow size 比较大的对象说明是大对象需要重点关注。Native Size 比较大的对象一般为Bitmap也需要重点关注分析 smpas当要分析 .xxx mmap 等占用内存较多或者想要再具体了解 Native heap 、Java heap 内存都是那些模块占用了的时候smaps 就比较有用。可以通过如下命令抓取 smaps前提是手机需要root1 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 92Dalvik (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/productappWebViewGoogleWebViewGoogle.apkclasses.vdex : 1225 kB /data/dalvik-cache/arm64/productappWebViewGoogleWebViewGoogle.apkclasses.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 40u.input_hihonor sysTid11298 #00 pc 00000000000c1188 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait8) (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_trampoline144) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7) #04 pc 0000000002038cc4 /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (android.os.MessageQueue.next292) #05 pc 00000000028f4f84 /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (android.os.Looper.loopOnce100) #06 pc 000000000205ed7c /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (android.os.Looper.loop284) #07 pc 000000000253670c /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (android.app.ActivityThread.main3852) #08 pc 0000000000382c40 /apex/com.android.art/lib64/libart.so (art_quick_invoke_static_stub640) (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_trampoline144) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7) #12 pc 0000000002ce0464 /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run132) #13 pc 0000000000760444 /apex/com.android.art/lib64/libart.so (nterp_helper7636) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7) #14 pc 00000000002eb676 /system/framework/framework.jar (com.android.internal.os.ZygoteInit.main690) Jit thread pool sysTid11308 #00 pc 0000000000083cbc /apex/com.android.runtime/lib64/bionic/libc.so (syscall28) (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_thread64) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb) HeapTaskDaemon sysTid11309 #00 pc 0000000000083cbc /apex/com.android.runtime/lib64/bionic/libc.so (syscall28) (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_trampoline144) (BuildId: eb9d19ca5eb7e6238e0a904aee97c2d7) #04 pc 0000000002406b80 /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (java.lang.Daemons$HeapTaskDaemon.runInternal192) #05 pc 000000000240408c /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (java.lang.Daemons$Daemon.run124) #06 pc 000000000211960c /memfd:jit-zygote-cache (deleted) (offset 0x2000000) (java.lang.Thread.run76) #07 pc 0000000000382974 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub612) (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::ObjPtrart::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_thread64) (BuildId: 3a5961502c37ecb11e3ee0defe2600bb)通过线程快照可以用于分析当前应用有多少个线程正在运行这些线程是属于哪些模块可以帮助辅助定位一些线程数量多导致的内存或其他性能问题比如耗电。分析 native 内存泄漏native 内存不像 java heap 那样可以 dump 整个堆进行分析要进行内存泄漏分析也不容易。这里主要介绍 3 个工具Perfetto 跟踪 Native AllocationsPerfetto 工具基本使用方式可以参考这位博主的系列文章Android Perfetto 系列 1Perfetto 工具简介 · Android Performance用 Perfetto 可以实时跟踪 Native 层的内存分配通过 malloc、calloc 之类的方法分配的内存它跟使用 Android Studio 中的 Track Memory ConsumptionNative Allocations是一样的只是可以配置的参数更多、更自由也更适合用自动化脚本处理。具体方式是下载这个 python 脚本https://raw.githubusercontent.com/google/perfetto/refs/heads/main/tools/heap_profile保存为heap_profile.py使用python3 ~/Downloads/heap_profile.py -o ~/Downloads/native_heap/命令开始抓取。其中 -o 参数表示产物输出目录需要是一个空目录在适当的时候按ctrlc终止上述命令之后产物会保存在-o参数指定的目录。然后可以用 Perfetto UI 打开对应的文件抓取的数据中会显示采样聚合的 native 内存分配调用栈可以有四个维度展示这些调用栈* Unreleased Malloc Size申请但是没有释放的内存大小 * Unreleased Malloc Count申请但是没有释放的次数相关于 申请次数 - 释放次数 * Total Malloc Size申请内存的总大小 * Total Malloc Count申请内存的总次数一般需要关注Unreleased Malloc Size这部分可能是内存泄漏当然也不绝对有可能这部分内存只是还不到释放的时机不应该释放。需要根据堆栈 case by case 分析处理。koom-native-leakkoom-native-leak 是快手开发的用于监控 Native 内存泄漏的库。它通过 hook malloc/free 等内存分配器方法来跟踪记录内存分配与回收以分析是否有内存泄漏。详情可见KOOM/koom-native-leak/README.zh-CN.md at master · KwaiAppTeam/KOOM · GitHub需要说明的是测试下来 koom-native-leak 目前只在 Android 10 API 29的设备上有效果其他版本基本抓不到任何 native 内存泄漏的 case 。MemoryLeakDetectorMemoryLeakDetector (Raphael) 是字节跳动开发的用于监控 Native 内存泄漏的库。原理同 koom-native-leak 类似都是通过 hook malloc/free 等内存分配器方法来记录内存的分配和回收。不过 MemoryLeakDetector 兼容性更好一些目前测试下来各个版本的系统都没有遇到大问题。关于 MemoryLeakDetector 详情可见官方文档GitHub - bytedance/memory-leak-detector · GitHub需要说明的是MemoryLeakDetector mmap 也会监控 mmap 等函数。但对于内存分析来说使用 mmap 申请的内存并不代表实际使用的内存在未实际使用对应的内存时是不计入 PSS 中。另外还需要注意的是无论是 MemoryLeakDetector 还是 koom-native-leak 都不能保证检测到的未释放内存 100% 就是内存泄漏也不能保证能保证所有的内存泄漏都能被检测到。//////////////////////////////////////////////////////perfetto 分析 Native 内存泄漏在 perfetto/tools at main · google/perfetto · GitHub 下载heap_profile, 保存为heap_profile.py使用python3 heap_profile.py -o empty_dir, 其中empty_dir是一个空目录的路径。记住一定是一个空目录。另外还可以设置其他一些参数直接看heap_profile.py里的代码即可代码中 Native 内存泄漏koom-native-leak: KOOM/koom-native-leak at master · KwaiAppTeam/KOOM · GitHub , Android 10(api 29) 的设备上正常再往后就抓不到了不太实用MemoryLeakDetector: GitHub - bytedance/memory-leak-detector · GitHub 字节跳动 Raphael使用比较方便。自己改了下源码支持最多 32 层栈输出GitHub - likebamboo/memory-leak-detector: 字节出品用于 native 内存泄漏 · GitHub代码中主动释放 Native 空闲内存以减少 Natvie Heap本质上是调用mallopt(M_PURGE, 0);以及malloc_trim(0);整理内存1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21#include malloc.h #include dlfcn.h /** * 释放 Native 空闲内存 */ void release_native_memory_safe() { // 优先尝试 M_PURGE (Android 12) typedef int (*mallopt_func)(int, int); mallopt_func opt (mallopt_func)dlsym(RTLD_DEFAULT, mallopt); if (opt) { opt(-101, 0); // M_PURGE } // 其次尝试 malloc_trim (Android 9) typedef int (*malloc_trim_func)(size_t); malloc_trim_func trim (malloc_trim_func)dlsym(RTLD_DEFAULT, malloc_trim); if (trim) { trim(0); } }代码中主动释放 GPU 相关缓存资源本质上是调用HardwareRenderer.trimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE);方法1 2 3 4 5 6 7 8 9val level ComponentCallbacks2.TRIM_MEMORY_COMPLETE // 触发 hwui 内存释放 val clazz Class.forName(android.view.WindowManagerGlobal) if (Build.VERSION.SDK_INT Build.VERSION_CODES.P) { val windowManagerGlobal HiddenApiBypass.invoke(clazz, null, getInstance) HiddenApiBypass.invoke(clazz, windowManagerGlobal, trimMemory, level) } else { // todo 反射调用 WindowManagerGlobal.getInstance().trimMemory(level) }From 如竹 的知识博客////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////