Android性能优化之内存泄漏优化(工具篇)

Android性能优化之内存泄漏优化(工具篇)

  • [一、Android Memory Profiler](#一、Android Memory Profiler)
  • [二、Memory Analyzer (MAT)](#二、Memory Analyzer (MAT))
  • 三、LeakCanary

Android 内存泄漏指的是应用程序在运行过程中,因为一些原因导致不再使用的对象无法被垃圾回收器回收,从而使得这些对象占用的内存无法释放,最终导致内存占用逐渐增加,可能导致应用程序的性能下降、响应变慢甚至崩溃。

一、Android Memory Profiler

官方文档:https://developer.android.com/studio/profile/memory-profiler

内存性能分析器是 Android Profiler 中的一个组件,可帮助您识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,让您可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。

如需打开内存分析器,请按以下步骤操作:

  1. 依次点击 View > Tool Windows > Profiler(您也可以点击工具栏中的 Profile 图标 )。
  2. Android Profiler 工具栏中选择要分析的设备和应用进程。如果您已通过 USB 连接设备但系统未列出该设备,请确保您已启用 USB 调试。
  3. 点击 MEMORY 时间轴上的任意位置以打开内存分析器。
    或者,您可以从命令行使用 dumpsys 检查您的应用内存,还可以在 logcat 中查看 GC 事件。

如果存在内存泄漏,应用在转到后台运行时,仍可能保留相应内存。此行为会导致系统强制执行不必要的垃圾回收事件,因而拖慢系统其余部分的内存性能。最终,系统将被迫终止您的应用进程以回收内存。然后,当用户返回您的应用时,它必须完全重启。

为帮助防止这些问题,您应使用内存分析器执行以下操作:

  1. 在时间轴上查找可能会导致性能问题的不理想的内存分配模式。
  2. 转储 Java 堆以查看在任何给定时间有哪些对象在占用内存。在一个较长的时间段内进行多次堆转储有助于识别内存泄漏。
  3. 记录正常条件和极端条件下用户交互期间的内存分配情况,从而准确识别您的代码是否在短时间内分配了过多对象,或所分配的对象是否出现了泄漏。


捕获堆转储

堆转储显示在您捕获堆转储时您的应用中哪些对象正在使用内存。特别是在长时间的用户会话后,堆转储会显示您认为不应再位于内存中却仍在内存中的对象,从而帮助识别内存泄漏。

捕获堆转储后,您可以查看以下信息:

  • 您的应用分配了哪些类型的对象,以及每种对象有多少。
  • 每个对象当前使用多少内存。
  • 在代码中的什么位置保持着对每个对象的引用。
  • 对象所分配到的调用堆栈。(目前,对于 Android 7.1 及更低版本,只有在记录分配期间捕获堆转储时,才会显示调用堆栈的堆转储。)

如需捕获堆转储,请点击 Capture heap dump,然后选择 Record。在转储堆期间,Java 内存量可能会暂时增加。 这很正常,因为堆转储与您的应用发生在同一进程中,并需要一些内存以收集数据。

在分析器捕获堆转储后,内存分析器界面将转换到显示堆转储的单独屏幕。

如需检查您的堆,请按以下步骤操作:

  • 浏览列表以查找堆计数异常大且可能存在泄漏的对象。为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。然后,点击一个类名称。此时右侧将出现 Instance View 窗格,显示该类的每个实例。

    此外,您也可以快速找到对象,方法是点击 Filter 图标 ,或按 Ctrl+F 键(在 Mac 上,按 Command+F 键),然后在搜索字段中输入类或软件包名称。如果从下拉菜单中选择 Arrange by callstack,还可以按方法名称搜索。如需使用正则表达式,请勾选 Regex 旁边的复选框。如果您的搜索查询区分大小写,请勾选 Match case 旁边的复选框。

  • 在 Instance View 窗格中,点击一个实例。此时下方将出现 References 标签页,显示对该对象的每个引用。

    或者,点击实例名称旁边的箭头以查看其所有字段,然后点击一个字段名称以查看其所有引用。如需查看某个字段的实例详细信息,请右键点击该字段并选择 Go to Instance。

  • 在 References 标签页中,如果您发现某个引用可能在泄漏内存,请右键点击它并选择 Go to Instance。这样会从堆转储中选择相应的实例,从而向您显示它自己的实例数据。

在您的堆转储中,请注意由下列任意情况引起的内存泄漏:

  • 长时间引用 Activity、Context、View、Drawable 和其他对象,可能会保持对 Activity 或 Context 容器的引用。
  • 可以保持 Activity 实例的非静态内部类,如 Runnable。
  • 对象保持时间比所需时间长的缓存。

二、Memory Analyzer (MAT)

官网:https://eclipse.dev/mat/

  • 概述(Overview)

    获取堆转储的概览:顶部是对象的大小和总数,然后是包含最大对象和链接的饼图,以便继续分析。

  • 直方图(Histogram)

    直方图列出了按类别分组的对象。内存分析器可以非常快速地估算出保留的大小。这是一个很好的指示,可以指示继续分析的位置。

- 主宰树(Dominator Tree)

Dominator Tree 列出了最大的对象。我们可以将其称为"Keep-Alive Tree",因为下一级显示了那些立即被阻止进行垃圾回收的对象。右键单击以深入查看:查看传出和传入引用,或查看 GC 根的路径以查看使对象保持活动的引用链。

  • GC Roots 路径(Path to GC Roots)
    GC Roots 的路径显示了阻止对象被垃圾回收的引用链。标有黄点的对象是垃圾回收 (GC) Roots,即假定为活着的对象。通常,GC Roots 是当前位于线程或系统类的调用堆栈上的对象。
  • 按类加载器分组的支配树( Dominator Tree Grouped by Class Loader)
    任何合适的架构都会通过不同的类加载器加载组件。内存分析器的许多视图允许您按类加载器对对象进行分组,从而轻松地按组件分析内存。要将类加载器映射到有意义的组件名称(例如插件 ID),可以使用插件名称解析器。

三、LeakCanary

官网:https://square.github.io/leakcanary/

Gradle 配置

groovy 复制代码
dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}

不需要更改代码!需要是debug Build Variants 编译的APK即可。

LeakCanary 自动检测以下对象的泄漏:

  • destroyed Activity instances
  • destroyed Fragment instances
  • destroyed fragment View instances
  • cleared ViewModel instances
  • destroyed Service instance

当应用程序存在内存泄漏时,一般的源自我们频繁的一些操作,导致一些资源没有及时回收。当LeakCanary检测到内存泄漏时,会自动Capture heap dump,并且有通知提示,我们可以直接在手机上查看分析结果。

也可以通过 Logcat 过滤关键字 LeakCanary

bash 复制代码
server@dev-fj-srv:$ adb logcat -s LeakCanary
--------- beginning of main
07-19 17:53:33.279 17243 17261 D LeakCanary: LeakCanary is running and ready to detect memory leaks.
07-19 17:53:35.348 17243 17243 D LeakCanary: Watching instance of androidx.coordinatorlayout.widget.CoordinatorLayout (com.android.deskclock.stopwatch.StopwatchFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key ac040ec7-3aa5-479f-9713-f0d13d337cf1
07-19 17:53:36.306 17243 17243 D LeakCanary: Watching instance of android.widget.LinearLayout (com.android.deskclock.AlarmClockFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key 9108d714-d6bf-4f44-baf7-e24a256ecf98
07-19 17:53:36.326 17243 17243 D LeakCanary: Watching instance of androidx.coordinatorlayout.widget.CoordinatorLayout (com.android.deskclock.ClockFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key 706ac657-99b7-4938-94e9-84a5877dc58b
07-19 17:53:36.662 17243 17243 D LeakCanary: Watching instance of androidx.coordinatorlayout.widget.CoordinatorLayout (com.android.deskclock.stopwatch.StopwatchFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key f295fa5b-486f-407c-a352-0091c8bab6fc
07-19 17:53:36.802 17243 17243 D LeakCanary: Watching instance of com.android.deskclock.timer.TimerItem (com.android.deskclock.timer.TimerItemFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key ff32d891-06a9-4ec1-9e3d-6225aabb4edb
07-19 17:53:36.803 17243 17243 D LeakCanary: Watching instance of android.widget.FrameLayout (com.android.deskclock.timer.TimerFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key 464d6297-f4b7-4eaa-820c-f054e71c3827

发生泄漏时的日志

bash 复制代码
07-15 11:04:29.838 31292 31384 D LeakCanary: 2 APPLICATION LEAKS
07-15 11:04:29.838 31292 31384 D LeakCanary: 
07-15 11:04:29.838 31292 31384 D LeakCanary: References underlined with "~~~" are likely causes.
07-15 11:04:29.838 31292 31384 D LeakCanary: Learn more at https://squ.re/leaks.
07-15 11:04:29.838 31292 31384 D LeakCanary: 
07-15 11:04:29.838 31292 31384 D LeakCanary: 295210 bytes retained by leaking objects
07-15 11:04:29.838 31292 31384 D LeakCanary: Signature: 23d55def87c64b7c2617db75bc4bd2fb6fa36103
07-15 11:04:29.838 31292 31384 D LeakCanary: ┬───
07-15 11:04:29.838 31292 31384 D LeakCanary: │ GC Root: Thread object
07-15 11:04:29.838 31292 31384 D LeakCanary: │
07-15 11:04:29.838 31292 31384 D LeakCanary: ├─ com.android.internal.os.BackgroundThread instance
07-15 11:04:29.838 31292 31384 D LeakCanary: │    Leaking: NO (MessageQueue↓ is not leaking)
07-15 11:04:29.838 31292 31384 D LeakCanary: │    Retaining 293.6 kB in 4729 objects
07-15 11:04:29.838 31292 31384 D LeakCanary: │    Thread name: 'android.bg'
07-15 11:04:29.838 31292 31384 D LeakCanary: │    ↓ HandlerThread.mLooper
07-15 11:04:29.838 31292 31384 D LeakCanary: ├─ android.os.Looper instance
07-15 11:04:29.838 31292 31384 D LeakCanary: │    Leaking: NO (MessageQueue↓ is not leaking)
07-15 11:04:29.838 31292 31384 D LeakCanary: │    ↓ Looper.mQueue
07-15 11:04:29.838 31292 31384 D LeakCanary: ├─ android.os.MessageQueue instance
07-15 11:04:29.838 31292 31384 D LeakCanary: │    Leaking: NO (MessageQueue#mQuitting is false)
07-15 11:04:29.838 31292 31384 D LeakCanary: │    HandlerThread: "android.bg"
07-15 11:04:29.838 31292 31384 D LeakCanary: │    ↓ MessageQueue[0]
07-15 11:04:29.838 31292 31384 D LeakCanary: │                  ~~~
07-15 11:04:29.838 31292 31384 D LeakCanary: ├─ android.os.Message instance
07-15 11:04:29.838 31292 31384 D LeakCanary: │    Leaking: UNKNOWN
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Retaining 295.3 kB in 4733 objects
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Message.what = 0
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Message.when = 422519759 (6418 ms before heap dump)
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Message.obj = null
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Message.callback = instance @322313336 of android.media.MediaScannerConnection$$ExternalSyntheticLambda0
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Message.target = instance @319818712 of android.os.Handler
07-15 11:04:29.839 31292 31384 D LeakCanary: │    ↓ Message.callback
07-15 11:04:29.839 31292 31384 D LeakCanary: │              ~~~~~~~~
07-15 11:04:29.839 31292 31384 D LeakCanary: ├─ android.media.MediaScannerConnection$$ExternalSyntheticLambda0 instance
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Leaking: UNKNOWN
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Retaining 295.2 kB in 4732 objects
07-15 11:04:29.839 31292 31384 D LeakCanary: │    f$0 instance of com.xxx.filemanager.activity.FileExplorerActivity with mDestroyed = true
07-15 11:04:29.839 31292 31384 D LeakCanary: │    ↓ MediaScannerConnection$$ExternalSyntheticLambda0.f$0
07-15 11:04:29.839 31292 31384 D LeakCanary: │                                                       ~~~
07-15 11:04:29.839 31292 31384 D LeakCanary: ╰→ com.xxx.filemanager.activity.FileExplorerActivity instance
07-15 11:04:29.839 31292 31384 D LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because com.xxx.filemanager.activity.FileExplorerActivity
07-15 11:04:29.839 31292 31384 D LeakCanary: ​     received Activity#onDestroy() callback and Activity#mDestroyed is true)
07-15 11:04:29.839 31292 31384 D LeakCanary: ​     Retaining 295.2 kB in 4730 objects
07-15 11:04:29.839 31292 31384 D LeakCanary: ​     key = 9bf6a560-07b5-4e49-9836-d585492293bc
07-15 11:04:29.839 31292 31384 D LeakCanary: ​     watchDurationMillis = 5248
07-15 11:04:29.839 31292 31384 D LeakCanary: ​     retainedDurationMillis = 247
07-15 11:04:29.839 31292 31384 D LeakCanary: ​     mApplication instance of com.xxx.filemanager.FMApplication
07-15 11:04:29.839 31292 31384 D LeakCanary: ​     mBase instance of android.app.ContextImpl
07-15 11:04:29.839 31292 31384 D LeakCanary: 
07-15 11:04:29.839 31292 31384 D LeakCanary: 280683 bytes retained by leaking objects
07-15 11:04:29.839 31292 31384 D LeakCanary: Signature: 6f2f983de93277fc32f3e5da91ae6581b246e338
07-15 11:04:29.839 31292 31384 D LeakCanary: ┬───
07-15 11:04:29.839 31292 31384 D LeakCanary: │ GC Root: Thread object
07-15 11:04:29.839 31292 31384 D LeakCanary: │
07-15 11:04:29.839 31292 31384 D LeakCanary: ├─ com.android.internal.os.BackgroundThread instance
07-15 11:04:29.839 31292 31384 D LeakCanary: │    Leaking: UNKNOWN
07-15 11:04:29.840 31292 31384 D LeakCanary: │    Retaining 293.6 kB in 4729 objects
07-15 11:04:29.840 31292 31384 D LeakCanary: │    Thread name: 'android.bg'
07-15 11:04:29.840 31292 31384 D LeakCanary: │    ↓ BackgroundThread<Java Local>
07-15 11:04:29.840 31292 31384 D LeakCanary: │                      ~~~~~~~~~~~~
07-15 11:04:29.840 31292 31384 D LeakCanary: ╰→ com.xxx.filemanager.activity.FileExplorerActivity instance
07-15 11:04:29.840 31292 31384 D LeakCanary: ​     Leaking: YES (ObjectWatcher was watching this because com.freeme.filemanager.activity.FileExplorerActivity
07-15 11:04:29.840 31292 31384 D LeakCanary: ​     received Activity#onDestroy() callback and Activity#mDestroyed is true)
07-15 11:04:29.840 31292 31384 D LeakCanary: ​     Retaining 280.7 kB in 4592 objects
07-15 11:04:29.840 31292 31384 D LeakCanary: ​     key = c6233ed5-0480-4c73-8eb1-addd3d520b66
07-15 11:04:29.840 31292 31384 D LeakCanary: ​     watchDurationMillis = 6531
07-15 11:04:29.840 31292 31384 D LeakCanary: ​     retainedDurationMillis = 1531
07-15 11:04:29.840 31292 31384 D LeakCanary: ​     mApplication instance of com.freeme.filemanager.FMApplication
07-15 11:04:29.840 31292 31384 D LeakCanary: ​     mBase instance of android.app.ContextImpl
07-15 11:04:29.840 31292 31384 D LeakCanary: ====================================
相关推荐
带电的小王1 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡1 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道2 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
码农老起2 小时前
企业如何通过TDSQL实现高效数据库迁移与性能优化
数据库·性能优化
java_heartLake2 小时前
Vue3之性能优化
javascript·vue.js·性能优化
arnold662 小时前
探索 ElasticSearch:性能优化之道
大数据·elasticsearch·性能优化
阿甘知识库3 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道3 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe3 小时前
Android Hook - 动态加载so库
android
OopspoO4 小时前
qcow2镜像大小压缩
学习·性能优化