Android 性能优化之内存优化与泄漏分析工具LeakCanary

一、背景

        在Android应用中,除了正常的业务开发,我们也要关注性能问题。卡顿、内存溢出、内存泄漏等问题,直接的表现会反馈到用户体验上,用户体验不好导致应用被卸载或者换到其他平台。

        在性能优化,各大公司都会付出一些代价,或者安排专人负责。有些新手也想做,但是无从下手,对专业工具和专业代码使用以及分析比较吃力,排查起来也比较费劲。如果有专业的工具能够只管的把这些记录并标记好。这样新手也可以通过详情的问题去排查,那么LeaksCanary就是这款工具了。

二、性能的简单介绍(在面试的时候,也经常会被问起)

1、卡顿:卡顿的主要涉及到线程的使用,在LeaksCanary中,会给出当前线程的使用信息

2、内存溢出:是程序在申请内存时,没有足够的内存空间供其使用

3、内存泄漏:是程序在申请内存后,无法释放已申请的内存空间

三、LeaksCanary

1、介绍

        LeakCanary是Square公司为Android开发者提供的一个自动检测内存泄漏的工具,

LeakCanary本质上是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,我们可以通过集成LeakCanary提供的jar包到自己的工程中,一旦检测到内存泄漏,LeakCanary就会dump Memory信息,并通过另一个进程分析内存泄漏的信息并展示出来,随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,极大地方便了Android应用程序的开发。

2、github地址:

GitHub - square/leakcanary: A memory leak detection library for Android.https://www.baidu.com/link?url=bcGyW7ySy9cW2azMXz5h-je6EtkwbMggSIlaEiFfWbtIqf3Yx3rak5qcn3Om3WJp&wd=&eqid=87caa544001114160000000463e1f198

Android 性能优化之内存优化与泄漏分析工具LeakCanary_第1张图片

 3、接入

3.1依赖库:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.9.1'

3.2、代码引入

2.9.1的库已kotlin开发语言为核心,网络上大多数还是1.6.1。这就导致在接入的时候无法方式不同。

1.6.1版本,直接在application中,通过LeakCanary.install(application),来完成。

2.9.1接入:

在androidManifest中引入

leak_canary_watcher_auto_install:


    true

是否自动安装,如果你不想自动安装,可以自行社会一个变量引入,默认不自动安装

Android 性能优化之内存优化与泄漏分析工具LeakCanary_第2张图片

介绍:MainProcessAppWatcherInstaller

internal class MainProcessAppWatcherInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    AppWatcher.manualInstall(application)
    return true
  }

MainProcessAppWatcherInstaller安装是ContentProvider的一个派生类。安装也是通过

AppWatcher.manualInstall(application)

Android 性能优化之内存优化与泄漏分析工具LeakCanary_第3张图片Android 性能优化之内存优化与泄漏分析工具LeakCanary_第4张图片

安装方法manualInstall会做安装前的检查

如果不想通过自动安装,可以通过

Java:AppWatcher.INSTANCE.manualInstall(application);

kotlin:AppWatcher.manualInstall(application);

如果不想自己控制,可以在xml资源文件,设置为true。

注意:

        如果手动安装,最好判断一下是否已安装AppWatcher.isInstalled()否则会报错

四、卡顿问题优化:

        LeakCanary 造成卡顿的原因就是在主进程中 dump hprof 文件,文件就会涉及到IO操作,在读写时,占用大量线程,导致页面会出现卡顿情况。针对这个问题,可以通过引入多进程方案。避免在IO时,影响主进程

leakcanary-android-process

依赖库:

debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:2.9.1'

        该库的核心是RemoteLeakCanaryWorkerService,这个服务就是多进程的核心。我们使用的时候,需要提前注册这个service。

在该service中:

  override fun onCreate() {
    // Ideally we wouldn't need to install AppWatcher at all here, however
    // the installation triggers InternalsLeakCanary to store the application instance
    // which is then used by the event listeners that respond to analysis progress.
    if (!AppWatcher.isInstalled) {
      val application = super.getApplicationContext() as Application
      AppWatcher.manualInstall(
        application,
        // Nothing to watch in the :leakcanary process.
        watchersToInstall = emptyList()
      )
    }
    super.onCreate()
  }

 在oncreate中,执行安装。

其实,在核心库中,已加入了该service。我们只需要通过

Android 性能优化之内存优化与泄漏分析工具LeakCanary_第5张图片

HeapDump:核心

Android 性能优化之内存优化与泄漏分析工具LeakCanary_第6张图片 RemoteWorkManagerHeapAnalyzer

        WorkManger也是一款开源工具,用于后台工作的架构组件,需要兼顾机会和有保证的执行。机会性执行意味着WorkManager将尽快完成您的后台工作。

依赖库:

def versions_work = "2.3.3"
implementation "androidx.work:work-runtime:$versions_work"

五、线上接入:

        目前很多调试都是在debug下进行,通过专业工具去处理。但是我们一旦打包以后会移除debug这些模块。如果线上发生了这些,如何获取?

        很多开发人员通过获取句柄文件来分析,但是如果我们没有问题设备,就无法获取到dump的句柄文件。这时,如果能把句柄文件传回到日志服务器,这样就方便开发人员定位信息。

  线上使用 LeakCanary,首要要确定以下问题:

  1. 如何获取 LeakCanary 分析的结果?
  2. 结果以何种形式上报到质量平台上?
  3. 如何确定合理的监控采集时机,尽可能小的影响用户体验?

定义自己的监听:

        上面我们介绍了workmanager这个工具,他就是通过定义一个监听,来完成后台的处理,减少主进程的线程操作,降低卡顿。同样我们也可以定义自己的EventListener。

目前LeakCanary的方法中已提供了一些监听:

   val eventListeners: List = listOf(
      LogcatEventListener,
      ToastEventListener,
      LazyForwardingEventListener {
        if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
      },
      when {
          RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->
            RemoteWorkManagerHeapAnalyzer
          WorkManagerHeapAnalyzer.validWorkManagerInClasspath -> WorkManagerHeapAnalyzer
          else -> BackgroundThreadHeapAnalyzer
      }
    ),

小试牛刀:

import leakcanary.EventListener

class MyLeakCanaryEventListener:EventListener {

    override fun onEvent(event: EventListener.Event) {


    }


}


Event:

目前已提供了如下Event

1、class DumpingHeap(uniqueId: String) : Event(uniqueId)

从“LeakCanary堆转储”HandlerThread发送

2、class HeapDump( uniqueId: String, val file: File, val durationMillis: Long, val reason: String) : Event(uniqueId)

从“LeakCanary堆转储”HandlerThread发送

3、class HeapDumpFailed( uniqueId: String, val exception: Throwable, val willRetryLater: Boolean) : Event(uniqueId)

从“LeakCanary堆转储”HandlerThread发送。失败

4、class HeapAnalysisProgress( uniqueId: String, val step: Step, val progressPercent: Double) : Event(uniqueId)

从执行分析的线程发送。

5、sealed class HeapAnalysisDone(  uniqueId: String,  val heapAnalysis: T,  showIntent: Intent) : Event(uniqueId)

5.1、class HeapAnalysisSucceeded( uniqueId: String, heapAnalysis: HeapAnalysisSuccess, val unreadLeakSignatures: Set, showIntent: Intent)

分析成功,

5.2、class HeapAnalysisFailed( uniqueId: String, heapAnalysis: HeapAnalysisFailure, showIntent: Intent)

分析失败

这些类都是继承了class Event( val uniqueId: String),且都是内部类。

 分析结果:

        if (event is EventListener.Event.HeapAnalysisDone.HeapAnalysisSucceeded) {
            //分析成功

            val successEvent = event as EventListener.Event.HeapAnalysisDone.HeapAnalysisSucceeded
            val list = successEvent.heapAnalysis.allLeaks
            list?.let {
                for (leak in it) {
                    leak.leakTraces
                }
            }
        }

这样我们就可以得到了分析结果。

六、注册监听:

注册监听涉及到LeakCanary的配置,这边我们先讲解一下LeakCanary.Config配置参数

 data class Config(
    /**
     * Whether LeakCanary should dump the heap when enough retained instances are found. This needs
     * to be true for LeakCanary to work, but sometimes you may want to temporarily disable
     * LeakCanary (e.g. for a product demo).
     *
     * Defaults to true.
     */
    val dumpHeap: Boolean = true,
    /**
     * If [dumpHeapWhenDebugging] is false then LeakCanary will not dump the heap
     * when the debugger is attached. The debugger can create temporary memory leaks (for instance
     * if a thread is blocked on a breakpoint).
     *
     * Defaults to false.
     */
    val dumpHeapWhenDebugging: Boolean = false,
    /**
     * When the app is visible, LeakCanary will wait for at least
     * [retainedVisibleThreshold] retained instances before dumping the heap. Dumping the heap
     * freezes the UI and can be frustrating for developers who are trying to work. This is
     * especially frustrating as the Android Framework has a number of leaks that cannot easily
     * be fixed.
     *
     * When the app becomes invisible, LeakCanary dumps the heap after
     * [AppWatcher.retainedDelayMillis] ms.
     *
     * The app is considered visible if it has at least one activity in started state.
     *
     * A higher threshold means LeakCanary will dump the heap less often, therefore it won't be
     * bothering developers as much but it could miss some leaks.
     *
     * Defaults to 5.
     */
    val retainedVisibleThreshold: Int = 5,

    /**
     * Known patterns of references in the heap, added here either to ignore them
     * ([IgnoredReferenceMatcher]) or to mark them as library leaks ([LibraryLeakReferenceMatcher]).
     *
     * When adding your own custom [LibraryLeakReferenceMatcher] instances, you'll most
     * likely want to set [LibraryLeakReferenceMatcher.patternApplies] with a filter that checks
     * for the Android OS version and manufacturer. The build information can be obtained by calling
     * [shark.AndroidBuildMirror.fromHeapGraph].
     *
     * Defaults to [AndroidReferenceMatchers.appDefaults]
     */
    val referenceMatchers: List = AndroidReferenceMatchers.appDefaults,

    /**
     * List of [ObjectInspector] that provide LeakCanary with insights about objects found in the
     * heap. You can create your own [ObjectInspector] implementations, and also add
     * a [shark.AppSingletonInspector] instance created with the list of internal singletons.
     *
     * Defaults to [AndroidObjectInspectors.appDefaults]
     */
    val objectInspectors: List = AndroidObjectInspectors.appDefaults,

    /**
     * Deprecated, add to LeakCanary.config.eventListeners instead.
     * Called on a background thread when the heap analysis is complete.
     * If you want leaks to be added to the activity that lists leaks, make sure to delegate
     * calls to a [DefaultOnHeapAnalyzedListener].
     *
     * Defaults to [DefaultOnHeapAnalyzedListener]
     */
    @Deprecated(message = "Add to LeakCanary.config.eventListeners instead")
    val onHeapAnalyzedListener: OnHeapAnalyzedListener = DefaultOnHeapAnalyzedListener.create(),

    /**
     * Extracts metadata from a hprof to be reported in [HeapAnalysisSuccess.metadata].
     * Called on a background thread during heap analysis.
     *
     * Defaults to [AndroidMetadataExtractor]
     */
    val metadataExtractor: MetadataExtractor = AndroidMetadataExtractor,

    /**
     * Whether to compute the retained heap size, which is the total number of bytes in memory that
     * would be reclaimed if the detected leaks didn't happen. This includes native memory
     * associated to Java objects (e.g. Android bitmaps).
     *
     * Computing the retained heap size can slow down the analysis because it requires navigating
     * from GC roots through the entire object graph, whereas [shark.HeapAnalyzer] would otherwise
     * stop as soon as all leaking instances are found.
     *
     * Defaults to true.
     */
    val computeRetainedHeapSize: Boolean = true,

    /**
     * How many heap dumps are kept on the Android device for this app package. When this threshold
     * is reached LeakCanary deletes the older heap dumps. As several heap dumps may be enqueued
     * you should avoid going down to 1 or 2.
     *
     * Defaults to 7.
     */
    val maxStoredHeapDumps: Int = 7,

    /**
     * LeakCanary always attempts to store heap dumps on the external storage if the
     * WRITE_EXTERNAL_STORAGE is already granted, and otherwise uses the app storage.
     * If the WRITE_EXTERNAL_STORAGE permission is not granted and
     * [requestWriteExternalStoragePermission] is true, then LeakCanary will display a notification
     * to ask for that permission.
     *
     * Defaults to false because that permission notification can be annoying.
     */
    val requestWriteExternalStoragePermission: Boolean = false,

    /**
     * Finds the objects that are leaking, for which LeakCanary will compute leak traces.
     *
     * Defaults to [KeyedWeakReferenceFinder] which finds all objects tracked by a
     * [KeyedWeakReference], ie all objects that were passed to
     * [ObjectWatcher.expectWeaklyReachable].
     *
     * You could instead replace it with a [FilteringLeakingObjectFinder], which scans all objects
     * in the heap dump and delegates the decision to a list of
     * [FilteringLeakingObjectFinder.LeakingObjectFilter]. This can lead to finding more leaks
     * than the default and shorter leak traces. This also means that every analysis during a
     * given process life will bring up the same leaking objects over and over again, unlike
     * when using [KeyedWeakReferenceFinder] (because [KeyedWeakReference] instances are cleared
     * after each heap dump).
     *
     * The list of filters can be built from [AndroidObjectInspectors]:
     *
     * ```kotlin
     * LeakCanary.config = LeakCanary.config.copy(
     *     leakingObjectFinder = FilteringLeakingObjectFinder(
     *         AndroidObjectInspectors.appLeakingObjectFilters
     *     )
     * )
     * ```
     */
    val leakingObjectFinder: LeakingObjectFinder = KeyedWeakReferenceFinder,

    /**
     * Dumps the Java heap. You may replace this with your own implementation if you wish to
     * change the core heap dumping implementation.
     */
    val heapDumper: HeapDumper = AndroidDebugHeapDumper,

    /**
     * Listeners for LeakCanary events. See [EventListener.Event] for the list of events and
     * which thread they're sent from. You most likely want to keep this list and add to it, or
     * remove a few entries but not all entries. Each listener is independent and provides
     * additional behavior which you can disable by not excluding it:
     *
     * ```kotlin
     * // No cute canary toast (very sad!)
     * LeakCanary.config = LeakCanary.config.run {
     *   copy(
     *     eventListeners = eventListeners.filter {
     *       it !is ToastEventListener
     *     }
     *   )
     * }
     * ```
     */
    val eventListeners: List = listOf(
      LogcatEventListener,
      ToastEventListener,
      LazyForwardingEventListener {
        if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
      },
      when {
          RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->
            RemoteWorkManagerHeapAnalyzer
          WorkManagerHeapAnalyzer.validWorkManagerInClasspath -> WorkManagerHeapAnalyzer
          else -> BackgroundThreadHeapAnalyzer
      }
    ),

    /**
     * Deprecated: This is a no-op, set a custom [leakingObjectFinder] instead.
     */
    @Deprecated("This is a no-op, set a custom leakingObjectFinder instead")
    val useExperimentalLeakFinders: Boolean = false
  )

默认都是有值,对应参数如下:

      private var dumpHeap = config.dumpHeap
      private var dumpHeapWhenDebugging = config.dumpHeapWhenDebugging
      private var retainedVisibleThreshold = config.retainedVisibleThreshold
      private var referenceMatchers = config.referenceMatchers
      private var objectInspectors = config.objectInspectors
      private var onHeapAnalyzedListener = config.onHeapAnalyzedListener
      private var metadataExtractor = config.metadataExtractor
      private var computeRetainedHeapSize = config.computeRetainedHeapSize
      private var maxStoredHeapDumps = config.maxStoredHeapDumps
      private var requestWriteExternalStoragePermission =
        config.requestWriteExternalStoragePermission
      private var leakingObjectFinder = config.leakingObjectFinder
      private var heapDumper = config.heapDumper
      private var eventListeners = config.eventListeners
      private var useExperimentalLeakFinders = config.useExperimentalLeakFinders

新增我们的监听:

       val eventListeners = LeakCanary.config.eventListeners.toMutableList().apply {
            add(MyLeakCanaryEventListener())
        }

        LeakCanary.config=LeakCanary.config.copy(
            eventListeners=eventListeners
        )

这样整个线上监听基本完成。

我们只要对LeakTrace进行分析,把结果存储后,在一个合适的时机上报给服务器即可

七、AppWatcher的看守配置:

        知道如何去分析这些问题,LeakCanary还提供了看门机制,监听哪些也是可以通过配置的。

在AppWatcher中,也提供了Config

  data class Config(
    @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
    val watchActivities: Boolean = true,

    @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
    val watchFragments: Boolean = true,

    @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
    val watchFragmentViews: Boolean = true,

    @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list")
    val watchViewModels: Boolean = true,

    @Deprecated("Call AppWatcher.manualInstall() with a custom retainedDelayMillis value")
    val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5),

    @Deprecated("Call AppWatcher.appDefaultWatchers() with a custom ReachabilityWatcher")
    val enabled: Boolean = true
  ) 

可以看守的有以下,默认都是开启,如果你想关闭哪个,默认设为false即可

AppWatcher.config= AppWatcher.config.copy(watchActivities = false, enabled = false)

这样,我们就可以很好的看守我们想要的结果。

八、ANR日志收集

        ANR或者crash的创建,也意味着ActivityThread的线程已经挂了,这时候我们任何的做法都不能依赖主线程来完成,这样会导致抓取不到日志。

        我们知道,虚拟机android的跨线程通讯都是binder,虽然ActivityThread挂了,但是AMS没有挂,AMS还存活在SystemManager中,又因为每个进程在fork出来的时候都有自己的binder。所以这个时候我们可以通过AMS来获取。

        日志收集核心:获取、存储、上报。获取到存储是核心流程,如何将获取的日志进行存储,IO机制显然无法满足这么短时间的操作。这个时候我们可以参考内存映射mmp机制。目前包括一些开源的都是采用mmap,常见的框架如微信的mmkv,mmkv是采用了mmap,也都是c在处理,利用内存映射来获取日志文档,这样就很容易获取到日志信息。

九、总结

        通过接入、卡顿优化、拦截、配置等,能够很好的满足我们从线上到线下的各种把控。可以很好的完善应用的监控与优化机制。

        Debug模式,直接在LeaksApp中可以很好的看到问题列表。其他更多的可以参考github的开源信息,或者自己接入调试。

你可能感兴趣的:(解决方案,kotlin,android,android,性能优化)