Android内存泄漏原因&检测工具

Post on Jan 15, 2023 by Wei Lin

概述

什么是内存泄漏-Android

Android内存泄漏定位、分析、解决全方案——segmentfault

Java虽然有自动回收机制,GC(Garbage Collection)自动回收大部分无用的内存空间,但是对于还保持着引用,而逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。

例如:

  • 忘记释放分配的内存的(Cursor忘记关闭等);
  • 应用不再需要这个对象,但未释放该对象的所有引用;
  • 强引用持有的对象,垃圾回收器是无法在内存中回收这个对象;
  • 持有对象生命周期过长,导致无法回收。


Android(Java)回收策略示意图:

内存泄漏1.png

图中的每个圆节点代表对象的内存资源,箭头代表可达路径。当圆节点与 GC Roots存在可达路径时,表示当前资源正被引用,虚拟机是无法对其进行回收的(如图中的黄色节点)。

反过来,如果圆节点与GC Roots不存在可达路径,则意味着这块对象的内存资源不再被程序引用,系统虚拟机可以在GC过程中将其回收掉。


从定义上讲,Android(Java)平台的内存泄漏是指没有用的对象资源仍然与GC-Root保持可达路径,但逻辑上却不再使用,导致系统无法进行回收。

什么是GC Roots?

GC Roots(垃圾回收根节点)是指在垃圾回收过程中被视为存活对象的根节点集合。在垃圾回收算法中,GC Roots 是起始点,从这些根节点开始遍历内存中的对象图(Object Graph)。通过标记-清除、标记-整理或复制等垃圾回收算法,可以识别并清除那些不可达的对象(即无法从 GC Roots 连接到的对象),从而回收这些对象所占用的内存空间,让其成为可用于分配新对象的内存。


GC Roots 通常包括以下几类对象:

  • 虚拟机栈中引用的对象:包括方法的参数、局部变量和返回值。
  • 本地方法栈中引用的对象:由本地方法(Native Method)引用的对象。
  • 静态变量引用的对象:静态变量属于类,生命周期与应用程序的生命周期相同。
  • 常量引用的对象:在常量池中的常量引用的对象,例如字符串常量。
  • JNI(Java Native Interface)引用的对象:JNI 是 Java 与本地代码交互的接口,JNI 引用的对象在本地代码中可能继续被使用。


这些 GC Roots 对象形成了一张图,表示了内存中对象之间的引用关系。垃圾回收器通过从 GC Roots 开始遍历对象图,标记所有可达的对象,并清除不可达的对象,以回收内存并保证内存的有效利用。

内存泄漏的危害

(1) 应用或系统卡顿

一次的内存泄漏可能看不出来什么影响,但是较多的内存泄漏会造成应用卡顿。因为系统分配给每个应用的内存是有限的,内存泄漏会导致其他组件的可用内存变少,一方面系统GC频率变高,GC时所有进程都会等待,会造成系统变卡顿。


(2) 程序崩溃

可能会导致程序运行崩溃(Out Of Memory,OOM)。一旦内存不足以为某些对象分配所需要的空间,则会导致程序崩溃,造成不良好的用户体验。


OOM:内容溢出,是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。

Android常见内存泄漏

Android中的内存泄漏主要有以下几种场景。

(1)static变量持有对象

  • 单例模式
  • static变量持有Activity
  • static View持有Activity


(2)非静态内部类持有外部类的引用

  • Handler使用异常
  • HandlerThread使用异常
  • Thread(Runnable)
  • AsyncTask使用异常
  • 匿名内部类持有外部类的引用


实际上Handler、Thread(Runnable)泄漏、HandlerThread泄漏、AsyncTask泄漏、基本都是由于非静态内部类持有外部类的引用和匿名内存类持有外部类的引用这两种方式泄漏,而且这几种泄漏出现的频率都比较高。


(3)资源未关闭

(4)注册的监听器未注销


(5)其它

其它泄漏场景,包含了最近出现的各种case集合,出现概率不高,不做统一分析。

LeakCanary介绍

LeakCanary是什么

LeakCanary是Square公司为Android开发者提供的一个自动检测内存泄漏的工具。直译为“泄漏的金丝雀”,名称来源于早期的煤矿业,因为金丝雀对瓦斯及其敏感,稍微有瓦斯泄漏,它就会停止歌唱,浓度再高一些,金丝雀就会直接中毒身亡。而LeakCanary组件就像检测瓦斯泄漏的金丝雀一样,在危险发生之前,提前检测内存泄漏。


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


LeakCanary实现了对Activity,Fragment,View和ViewModel实例的内存泄漏的监控,被监控的对象,在垃圾回收的5秒后如果没有回收掉的话,这个对象就会被认为有内存泄漏的可能。


发展史:

  • 初始版本,仅Activity的监控;

  • 1.6.1版本,加入了对Fragment的监控;
  • 1.6.2版本,加入了对Fragment view的监控;
  • 2.2版本,加入了对ViewModel的监控。

LeakCanary原理

在Android应用启动时,会在启动APP下其它组件前启动ContentProvider,LeakCanary利用ContentProvider自动启动这一点,继承ContentProvier,并监控其它类。

MainProcessAppWatcherInstaller类继承ContentProvider:

internal class MainProcessAppWatcherInstaller : ContentProvider() {

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

  override fun query(
    uri: Uri,
    projectionArg: Array<String>?,
    selection: String?,
    selectionArgs: Array<String>?,
    sortOrder: String?
  ): Cursor? = null

  override fun getType(uri: Uri): String? = null

  override fun insert(uri: Uri, contentValues: ContentValues?): Uri? = null

  override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0

  override fun update(
    uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?
  ): Int = 0
}


在AppWatcher.manualInstall()中初始化并启动对象监控:

fun manualInstall(
  application: Application,
  retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
  watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {...}

默认实现了对Activity、Fragment和ViewModel、RootView、Service实例的内存泄漏的监控,这些对象都是有明确的生命周期,而且占用内存较高,它们的内存泄漏是需要我们重点关注的。


LeakCanary判断内存泄漏的时机

LeakCanary判断各个组件是否发生内存泄漏的方法,本质上都是通过注册回调的方法监控生命周期的变化。


(1) Activity

通过调用Application.registerActivityLifecycleCallbacks()注册onActivityDestroyed()回调,监控Activity执行onDestroy()。

//需要实现Application.ActivityLifecycleCallbacks接口类并注册
getApplication().registerActivityLifecycleCallbacks(this);

//LeakCanary的实现及注册如下,使用kotlin语言
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"
        )
      }
    }

  override fun install() {
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
  }

  override fun uninstall() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
  }
}

//Application.java
//将回调添加到Application对象的mActivityLifecycleCallbacks中
public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {
    synchronized (mActivityLifecycleCallbacks) {
        mActivityLifecycleCallbacks.add(callback);
    }
}

//Activity.java
//当Activity执行onDestroy()时,会执行所有注册ActivityLifecycleCallbacks并重写nActivityDestroyed()的回调
protected void onDestroy() {
    if (DEBUG_LIFECYCLE) Slog.v(TAG, "onDestroy " + this);
    mCalled = true;

    getAutofillClientController().onActivityDestroyed();

    //......

    dispatchActivityDestroyed();

    notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_STOP);

    //......
}

private void dispatchActivityDestroyed() {
    Object[] callbacks = collectActivityLifecycleCallbacks();
    if (callbacks != null) {
        for (int i = callbacks.length - 1; i >= 0; i--) {
            ((Application.ActivityLifecycleCallbacks) callbacks[i]).onActivityDestroyed(this);
        }
    }
    getApplication().dispatchActivityDestroyed(this);
}


(2) Fragment

通过调用Application.registerActivityLifecycleCallbacks()注册onActivityCreated()回调,监控Activity的onCreate();通过FragmentManager.registerFragmentLifecycleCallbacks()注册onFragmentDestroyed()回调,监控Fragment执行onDestroy()。


(3) ViewModel

针对AndroidX库中的Activity,注册onActivityCreated()回调监听Activity的onCreate();对于Fragment,注册onFragmentCreated()回调监听Fragment的onCreate();注册ViewModel的onCleared()回调。


(4 RootView

通过注册View.OnAttachStateChangeListener监听器,监听View对象从Window中绑定或分离的情况。


(5) Service

类似于 Activity,也是监控其onDestroy()的执行情况,但是由于 Service 没有开放声明周期的回调,所以是通过hook的方式获取 Service 的生命周期。


LeakCanary判断内存泄漏的原理:利用了WeakRefrence + RefrenceQueue的机制(仅被弱引用持有的对象,当对象被回收时,会存入到引用队列中),从引用队列中不断的获取对象,将已确认被GC的对象剔除,剩余未被回收的对象则定义为可能泄漏的对象,当达到一定的判断条件时,通知用户内存泄漏。

LeakCanary使用

LeakCanary——Getting started

leakCanary检测内存泄漏的原理——CSDN

LeakCanary的1.x和2.x版本在使用上有一定区别。

1.x版本除了在build.gradle中添加依赖,还需要手动初始化:

public class LaunchApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        if(BuildConfig.DEBUG){
            LeakCanary.install(this);
        }
    }
}


2.x版本无需手动初始化,只添加依赖即可,在appName/app/build.gradle的dependencies项下添加:

dependencies {
    //其它依赖包......
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8'
}

debugImplementation引用方式,表示只在debug模式的编译和最终的debug apk打包时有效,LeakCanary在监控对象时一定程度上会影响性能及app的运行速度,因此只在debug模式下使用。

内存泄漏2.png


然后通过Android Studio安装软件即可,手机桌面会自动生成一个“Leaks”图标。在使用APP的过程中,若LeakCanary检测到应该被回收的对象在一定时间后(默认5s)没有被回收,则认定发生了内存泄漏,于是LeakCanary会dump相关信息,如下图。

也可以通过点击内存泄漏具体界面的“Print leak trace to Logcat”(如下图),将内存泄漏的日志信息输出到main log(Tag为LeakCanary)。手机连接电脑,在终端输入adb logcat |grep -E “LeakCanary”查看。


LeakCanary还会在手机的Download/packageName下生成<date>.hprof文件,将该文件拖入Android Studio中可以查看更详细的信息,如下:

内存泄漏3.png


内存泄漏4.png