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: ====================================
相关推荐
Yeats_Liao24 分钟前
Spring 定时任务:@Scheduled 注解四大参数解析
android·java·spring
雾里看山2 小时前
【MySQL】 库的操作
android·数据库·笔记·mysql
水瓶丫头站住11 小时前
安卓APP如何适配不同的手机分辨率
android·智能手机
xvch11 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
xvch15 小时前
Kotlin 2.1.0 入门教程(七)
android·kotlin
望风的懒蜗牛15 小时前
编译Android平台使用的FFmpeg库
android
浩宇软件开发16 小时前
Android开发,待办事项提醒App的设计与实现(个人中心页)
android·android studio·android开发
ac-er888816 小时前
Yii框架中的多语言支持:如何实现国际化
android·开发语言·php
苏金标17 小时前
The maximum compatible Gradle JVM version is 17.
android
zhangphil17 小时前
Android BitmapShader简洁实现马赛克,Kotlin(一)
android·kotlin