Android内存泄漏分析及检测工具LeakCanary简介

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android内存泄漏分析及检测工具LeakCanary简介相关的知识,希望对你有一定的参考价值。

好文推荐:
作者:三百云技术中心

背景

android内存优化是APP稳定运行的重要一环,开发过程中如果代码写的过于随意,很容易造成内存泄漏,多次累积之后,便会产生OOM,进而造成app崩溃。本文介绍了内存泄漏的相关知识和检测工具LeakCanary的实现原理,同时总结概述降低应用运行内存的技巧。

什么是内存泄漏

内存泄漏是指进程中不再使用的对象持续占用内存或不再使用的对象占有的内存没有得到及时释放,从而使得实际可用的内存变小,造成了内存空间的浪费。

如何检测内存泄漏

在Android中主要使用以下两种工具来进行内存泄漏。

Profiler

Profiler是android studio自带的内存检测工具,可以很直观的看到app在运行过程中内存等的变化。但要进一步分析,还需要执行一些其它操作,不够直观。

LeakCanary

LeakCanary是目前使用最多的内存检测工具,它集成在APP内部,只需要在build.gradle文件中添加一个依赖项即可。相比Profiler,它不需要时刻去关注android studio页面中展示出来的内存变化,只会在发生内存泄漏的时候,弹出弹框来提示开发者产生了内存泄漏,并给出产生泄漏的引用链。比Profiler更简单,使用更灵活。

我们的APP目前使用了LeakCanary来进行内存泄漏的检测,下面就介绍一下LeakCanary的主要实现原理。

LeakCanary

LeakCanary初始化

新版本2.x相比1.x的区别在于,不需要手动进行初始化了,只需要在主模块中的build.gradle中添加如下依赖,即可完成初始化。

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'

那么新版本又是如何进行初始化的的呢?在源码中很容易发现找到AppWatcherInstaller这个类,因为ContentProvider在app启动时是在Application之前执行的,因而无需手动再在application类中执行初始化代码。

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

那么AppWatcherInstalleronCreate的方法是在何时调用的?其实就是ContentProvider是在何时初始化的。它其实是在ActivityThreadhandleBindApplication的方法中。

private void handleBindApplication(AppBindData data) {
    ...
    // don't bring up providers in restricted mode; they may depend on the
            // app's custom Application class
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    installContentProviders(app, data.providers);
                }
            }
     // application 初始化
     mInstrumentation.callApplicationOnCreate(app);
    ...
}

通过上述代码可知,ContentProvideronCreate方法要早于Application的初始化,而LeakCanary的初始化正是在ContentProvider初始化时进行的,也就是说它帮助我们自动执行了初始化的过程。

需要注意的是,使用这种方式进行初始化的时候,不可以执行耗时代码,因为ContentProvider的初始化也是在主线程进行了,否则就会导致app启动速度变慢。

LeakCanary检测原理

  • LeakCanary进行内存泄漏检测主要是以下几个步骤:
    1. 获取可能泄漏的对象
    2. 生成.hprof文件
    3. 分析.hprof文件,并进行提示
  • LeakCanary检测的对象
    1. Activity
    2. Fragment和ViewModel
    3. View
    4. Service
fun appDefaultWatchers(
    application: Application,
    reachabilityWatcher: ReachabilityWatcher = objectWatcher
  ): List<InstallableWatcher> {
    return listOf(
      ActivityWatcher(application, reachabilityWatcher),
      FragmentAndViewModelWatcher(application, reachabilityWatcher),
      RootViewWatcher(reachabilityWatcher),
      ServiceWatcher(reachabilityWatcher)
    )
  }

首先我们来看一下ObjectWatcher ,它的关键代码如下:


@Synchronized fun watch(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {
      return
    }
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID()
        .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    SharkLog.d {
      "Watching " +
          (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
          (if (description.isNotEmpty()) " ($description)" else "") +
          " with key $key"
    }

    watchedObjects[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }

主要是对watchedObject使用了弱引用,同时注意到里面使用了ReferenceQueue,这两者结合使用可以实现如果弱引用关联的对象被回收,就会把这个弱引用加入到queue中,以此来判断该对象是否被回收。

LeakCanary主要的检测对象是以上4种,以Activity为例进行分析,其他检测类型也是类似原理,不再赘述。

class ActivityWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        reachabilityWatcher.expectWeaklyReachable(
          activity, "${activity::class.java.name} received Activity#onDestroy() callback"
        )
      }
    }
}

ActivityWatcher中注册了ActivityLifecycleCallbacks,同时在onActivityDestroyed的时候,执行了一些操作,查看源码:

  @Synchronized override fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {
      return
    }
    removeWeaklyReachableObjects()
    val key = UUID.randomUUID()
      .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    SharkLog.d {
      "Watching " +
        (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
        (if (description.isNotEmpty()) " ($description)" else "") +
        " with key $key"
    }

    watchedObjects[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }

上述代码的主要逻辑是:

  1. 移除弱可达的对象
  2. 将当前的watchedObject添加到KeyedWeakReference当中
  3. 将这个weakReference保存到数组中
  4. checkRetainedExecutor中执行moveToRetained方法

根据removeWeaklyReachableObjects方法中原理,如果这个对象除了由ObjectWatcher所添加的WeakReference以外,没有其他对象在引用它了,那么这个对象也就可以回收了,watchedObjects也就可以移除他了。

  private fun removeWeaklyReachableObjects() {
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }
}

checkRetainedExecutor其实是个单例对象,里面会通过handler来延迟5s来执行方法。如果超过5s则会触发LeakCanary的泄漏检测机制。5s只是个经验值应该,因为GC并不是实时发生,因而预留5s交给GC操作。

触发了LeakCanary的泄漏检测之后,则会执行HeapDumpTriggerdumpHeap方法,在获取到了.hprof文件之后,调用HeapAnalyzerService.runAnalysis()给出分析结果。 关于.hprof文件的分析,不是本文重点,具体可以参考hprof文件协议。其分析基本也就是根据GC Root去寻找泄漏的对象,大体流程图如下。

在Android中常见的内存泄漏

单例

单例所导致的内存泄漏几乎是在android开发中最为常见的内存泄漏问题了。

public class Singleton {

   private static Singleton singleton;
   private Context context;

   private Singleton(Context context) {
       this.context = context;
   }

   public static Singleton getInstance(Context context) {
       if (singleton == null) {
           singleton = new Singleton(context);
       }
       return singleton;
   }

}

在上面的代码中,如果在执行getInstance方法的时候,传入的是activity的对象,那么该activity对象就没法被及时回收,导致内存泄漏,可以考虑传入ApplicationContext,或者把context放入到方法变量中。

非静态内部类(包括匿名内部类)

非静态内部类会默认持有外部类的引用,如果它的生命周期长于外部类时,就会导致内存泄漏。 在android开发,这种情况常常见于Handler的使用。

尽可能避免使用静态变量

    class MainActivity : AppCompatActivity() {

    companion object {
        @JvmStatic
        private var info: StaticInfo? = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        info = StaticInfo(this)
    }

    class StaticInfo(activity: MainActivity) {

}

在上述代码中,info是一个静态变量,但是它持有了activity的引用,由于静态变量的生命周期要比activity的生命周期长,导致activity无法及时回收,造成内存泄漏。

资源未关闭造成的内存泄漏

诸如cursor、inputStream等对象一定要注意及时关闭

    try {

    }catch (e:Exception) {

    }finally {
        // 可以在finally方法里把cursor等对象进行关闭
    } 

集合中的对象未及时清理造成的内存泄漏

 val list = ArrayList<Activity>()

例如,如果一个list中存放的是activity对象,就会可能导致activity无法及时回收。如果该list是静态对象的话,不及时移除activity的话,就更会产生内存泄漏了。

webview造成的内存泄漏

因为webview在加载完网页后,它的callback会持有activity的引用,造成webview的内存无法释放。可以在activity的onDestroy()方法中移除该webview,并调用webview.destroy()。

未取消注册或回调造成的内存泄漏

在android中回调是使用非常多的,但如果在注册回调的时候,传入了context对象,则需要注 意及时取消回调,否则就可能会出现内存泄漏。例如eventbus和广播。

内存优化

  • 使用intentService代替Service,或者service执行完记得及时停止
  • 在系统资源紧张的时候,尽可能多释放一些非重要的资源(如图片的内存缓存)
class MyApp : Application() {

    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        // 可以在这里做些内存释放的工作
    }

}
  • 避免bitmap的滥用

如果不是必须操作bitmap,对于图片加载,我们可以使用一些优秀的第三方库来进行加载。使用bitmap记得复用和及时回收。

  • 使用针对内存优化过的数据容器

在大多数场景下,可以使用SparseArray代替HashMap等可以一定程度上减少内存使用。

  • 使用多进程

推送单独开启进程,webview开启单独进程

总结

本文首先介绍了android的内存泄漏检测工具LeakCanary的实现原理,然后介绍一些造成内存泄漏的原因,最后给出一些android内存优化的建议。当然内存问题千千万,本文无法做到面面俱到。希望通过本文的介绍,大家都能在自己的app中跟踪和解决内存泄漏的问题,毕竟优化内存虽然很麻烦,但也很重要。

最后

小编在网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 CodeChina地址:https://codechina.csdn.net/u012165769/Android-T3 访问查阅。

以上是关于Android内存泄漏分析及检测工具LeakCanary简介的主要内容,如果未能解决你的问题,请参考以下文章

Android内存泄漏检测工具:LeakCanary

Android内存泄漏检测工具:LeakCanary

Android内存泄漏检测工具:LeakCanary

Android内存优化三:内存泄漏检测与监控

Android常见内存泄露

使用UMDH检测内存泄漏