Android内存泄露检测工具---LeakCanary的前世今生

曾经检测内存泄露的方式

让我们来看看在没有LeakCanary之前,检测内存泄露的方式

1.Bug收集

通过Bugly、友盟这样的统计平台,统计Bug,了解OutOfMemaryError的情况。

1. 重现问题

对Bug进行筛选,归类,排除干扰项。然后为了重现问题,有时候你必须找到出现问题的机型,因为有些问题只会在特定的设备上才会出现。为了找到特定的机型,可能会想尽一切办法,去买、去借、去求人,去租,商家公司还真干过这事(14年,公司专门派了一个商务去广州找了一家租赁手机的公司,借了50台手机回来,600块钱一天)。然后,为了重现问题,一遍一遍的尝试,去还原当时OutOfMemaryError的原因,用最原始、最粗暴的方式。

2. Dump导出hprof文件

使用Eclipse ADT的DDMS,观察Heap,然后点击手动GC按钮(Cause GC)。

主要观测的两项数据:

3. Heap Size的大小,当资源增加到堆空余空间不够的时候,系统会增加堆空间的大小,但是超过可分配的最大值(比如128M)就会发生OutOfMemaryError,这个时候进程就会被杀死。这个最大值,不同手机会有不同的值,跟手机内存大小和厂商定制过得系统存在关系。

4. Allocated堆中已分配的大小,这是应用程序实际占用的大小,资源回收后,这项数据会变小。

查看操作前后的堆数据,看是否存在内存泄露,比如反复打开、关闭一个页面,看看堆空间是否会一直增大。

5. 然后使用MAT内存分析工具打开,反复查看找到那些原本应该被回收掉的对象。

6. 计算这个对象到GC roots的最短强引用路径。

7. 确定那个路径中那个应用不该有,然后修复问题。

8. 很麻烦,不是吗。现在有一个类库可以直接解决这个问题

LeakCanary

使用方式

Module.appbuild.gradle中引入

 dependencies {
   debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.4-beta2‘
   releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2‘
   testCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2‘
 }

然后在Application中重写onCreate()方法

public class ExampleApplication extends Application {
  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

在Activity中写一些导致内存泄露的代码,当发生内存泄露了,会在通知栏弹出消息,点击跳转到泄露页面

LeakCanary 可以做到非常简单方便、低侵入性地捕获内存泄漏代码,甚至很多时候你可以捕捉到 Android 官方组件的内存泄漏代码,最关键是不用再进行(捕获错误+Bug归档+场景重现+Dump+Mat分析) 这一系列复杂操作。

原理分析

自己实现的步骤

首先,设想如果让我们自己来实现一个LeakCanary,我们怎么来实现。

按照前面曾经检测内存的方式,我想,大概需要以下几个步骤:

1. 检测一个对象,查看他是否被回收了。

2. 如果没有被回收,使用DDMS的dump导出.hprof文件,确定是否内存泄露,如果泄露了导出最短引用路径

3. 把最短应用路径封装起来,作为一个Intent发送给Notification,然后通过页面展示

检测对象,是否被回收

为了方便,查看源码,使用官方给的示例代码,我们发现可以这样使用:

MainActivity.class

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RefWatcher refWatcher = LeakCanary.androidWatcher(getApplicationContext(),
                new ServiceHeapDumpListener(getApplicationContext(), DisplayLeakService.class),
                AndroidExcludedRefs.createAppDefaults().build());
        refWatcher.watch(this);
  }

就是把MainActivity作为一个对象检测起来,查看refWatcher.watch(this);源码

  public void watch(Object watchedReference) {
    watch(watchedReference, "");
  }

  /**
   * Watches the provided references and checks if it can be GCed. This method is non blocking,
   * the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with.
   *
   * @param referenceName An logical identifier for the watched object.
   */
  public void watch(Object watchedReference, String referenceName) {
    Preconditions.checkNotNull(watchedReference, "watchedReference");
    Preconditions.checkNotNull(referenceName, "referenceName");
    if (debuggerControl.isDebuggerAttached()) {
      return;
    }
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    watchExecutor.execute(new Runnable() {
      @Override public void run() {
        ensureGone(reference, watchStartNanoTime);
      }
    });
  }
  1. 先检查监测对象是否为空,为空抛出异常
  2. 如果是在调试Debugger过程中允许内存泄露出现,因为这个时候检测对象是不准确的
  3. 给监测对象生成UUID唯一表示符,存入Set集合,方便查找。
  4. 然后定义了一个KeyedWeakReference,查看下KeyedWeakReference是个什么玩意
public final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;
  public final String name;

  KeyedWeakReference(Object referent, String key, String name,
      ReferenceQueue<Object> referenceQueue) {
    super(Preconditions.checkNotNull(referent, "referent"), Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
    this.key = Preconditions.checkNotNull(key, "key");
    this.name = Preconditions.checkNotNull(name, "name");
  }
}

原来KeyedWeakReference就是对WeakReference进行了一些加工,是一种装饰设计模式,其实就是弱引用的衍生类。配合前面的Set retainedKeys使用,retainedKeys代表的是没有被GC回收的对象,queue中的弱引用代表的是被GC了的对象,通过这两个结构就可以明确知道一个对象是不是被回收了。

5. 接着看上面的执行过程,然后通过线程池开启了一个异步任务方法ensureGonewatchExecutor其实就是AndroidWatchExecutor,查看源码

public final class AndroidWatchExecutor implements Executor {

  static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
  private static final int DELAY_MILLIS = 5000;

  private final Handler mainHandler;
  private final Handler backgroundHandler;

  public AndroidWatchExecutor() {
    mainHandler = new Handler(Looper.getMainLooper());
    HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
    handlerThread.start();
    backgroundHandler = new Handler(handlerThread.getLooper());
  }

  @Override public void execute(final Runnable command) {
    if (isOnMainThread()) {
      executeDelayedAfterIdleUnsafe(command);
    } else {
      mainHandler.post(new Runnable() {
        @Override public void run() {
          executeDelayedAfterIdleUnsafe(command);
        }
      });
    }
  }

  private boolean isOnMainThread() {
    return Looper.getMainLooper().getThread() == Thread.currentThread();
  }

  private void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
    // This needs to be called from the main thread.
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle() {
        backgroundHandler.postDelayed(runnable, DELAY_MILLIS);
        return false;
      }
    });
  }
}

通过主线程Handler转发到后台Handler执行任务,后台线程延迟DELAY_MILLIS的时间执行

6. 具体执行的任务在ensureGone 里面

  void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    //记录观测对象的时间
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //清除在queue中的弱引用 保留retainedKeys中剩下的对象
    removeWeaklyReachableReferences();
    //如果剩下的对象中不包含引用对象,说明已被回收,返回||调试中,返回
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
      return;
    }
    //请求执行GC
    gcTrigger.runGc();
    //再次清理一次对象
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      //记录下GC执行时间
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      //Dump导出hprof文件
      File heapDumpFile = heapDumper.dumpHeap();

      if (heapDumpFile == null) {
        // Could not dump the heap, abort.
        return;
      }
      //记录下Dump和文件导出用的时间
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      //分析hprof文件
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
  }

  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }
  private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }
  1. retainedKeys和queue怎么关联起来的?这里的removeWeaklyReachableReferences方法就实现了我们说的 retainedKeys代表的是没有被GC回收的对象,queue中的弱引用代表的是被GC了的对象。gone返回true说明对象已被回收,不需要观测了。
  2. 为什么执行removeWeaklyReachableReferences()两次?为了保证效率,如果对象被回收,没必要再通知GC执行,Dump操作等等一系列繁琐步骤,况且GC是一个线程优先级极低的线程,就算你通知了,她也不一定会执行,那么我们分析的观测对象的时机就显得尤为重要了,在对象被回收的时候召唤观测。

何时执行观测对象

我们观测的是一个Activity,Activity这样的组件都存在生命周期,在他生命周期结束的事后,观测他如果还存活就肯定就存在内存泄露了,进一步推论,Activity的生命周期结束就关联到它的onDestory()方法,也就是只要重写这个方法就可以了。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        refWatcher.watch(this);
    }

在MainActivity中加上这行代码就好了,但是我们显然不想每个Activity都这样干,都是同样的代码为啥要重复着写,当然解决办法呼之欲出:

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }

        @Override public void onActivityStarted(Activity activity) {
        }

        @Override public void onActivityResumed(Activity activity) {
        }

        @Override public void onActivityPaused(Activity activity) {
        }

        @Override public void onActivityStopped(Activity activity) {
        }

        @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }

        @Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };
    void onActivityDestroyed(Activity activity) {
        refWatcher.watch(activity);
    }

LeakCanary源码是这样做的,通过ActivityLifecycleCallbacks转发,这样我们就不需要在每个Activity方法的结束再多写一行代码了,但是这个方法有个缺点,看注释

  // If you need to support Android < ICS, override onDestroy() in your base activity.
        October 2011: Android 4.0.
        public static final int ICE_CREAM_SANDWICH = 14;

如果是SDK 14 Android 4.0以下的系统,不具备这个接口,也就是还是的通过刚才那种方式重写onDestory方法。

分析hprof文件

接着分析,查看文件解析类发现他是个转发工具类

public final class ServiceHeapDumpListener implements HeapDump.Listener {
  ...
  @Override public void analyze(HeapDump heapDump) {
      Preconditions.checkNotNull(heapDump, "heapDump");
     //转发给HeapAnalyzerService
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }
}

通过IntentService运行在另一个进程中执行分析任务

public final class HeapAnalyzerService extends IntentService {

  private static final String LISTENER_CLASS_EXTRA = "listener_class_extra";
  private static final String HEAPDUMP_EXTRA = "heapdump_extra";

  public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    context.startService(intent);
  }

  @Override protected void onHandleIntent(Intent intent) {
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    ExcludedRefs androidExcludedDefault = createAndroidDefaults().build();
    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(androidExcludedDefault, heapDump.excludedRefs);
    //获取分析结果
    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }
}

查看heapAnalyzer.checkForLeak代码

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return AnalysisResult.failure(exception, since(analysisStartNanoTime));
    }

    ISnapshot snapshot = null;
    try {
      // 加载hprof文件
      snapshot = openSnapshot(heapDumpFile);
      //找到泄露对象
      IObject leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return AnalysisResult.noLeak(since(analysisStartNanoTime));
      }

      String className = leakingRef.getClazz().getName();
      // 最短引用路径
      AnalysisResult result =
          findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
      //如果没找到  尝试排除系统进程干扰的情况下找出最短引用路径
      if (!result.leakFound) {
        result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
      }

      return result;
    } catch (SnapshotException e) {
      return AnalysisResult.failure(e, since(analysisStartNanoTime));
    } finally {
      cleanup(heapDumpFile, snapshot);
    }
  }

到这里,我们就找到了泄露对象的应用路径,剩下的工作就是发送消息给通知,然后点击通知栏跳转到我们另一个App打开绘制出路径即可。

补充—排除干扰项

但是我们在找出最短引用路径的时候,有这样一段代码,他是干什么的呢

 // 最短引用路径
      AnalysisResult result =
          findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
      //如果没找到  尝试排除系统进程干扰的情况下找出最短引用路径
      if (!result.leakFound) {
        result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);

查看findLeakTrace

  private AnalysisResult findLeakTrace(long analysisStartNanoTime, ISnapshot snapshot,
      IObject leakingRef, String className, boolean excludingKnownLeaks) throws SnapshotException {

    ExcludedRefs excludedRefs = excludingKnownLeaks ? this.excludedRefs : baseExcludedRefs;

    PathsFromGCRootsTree gcRootsTree = shortestPathToGcRoots(snapshot, leakingRef, excludedRefs);

    // False alarm, no strong reference path to GC Roots.
    if (gcRootsTree == null) {
      return AnalysisResult.noLeak(since(analysisStartNanoTime));
    }

    LeakTrace leakTrace = buildLeakTrace(snapshot, gcRootsTree, excludedRefs);

    return AnalysisResult.leakDetected(!excludingKnownLeaks, className, leakTrace, since(analysisStartNanoTime));
  }

唯一的不同是excludingKnownLeaks 从字面意思也很好理解,是否排除已知内存泄露

其实是这样的,在我们系统中本身就存在一些内存泄露的情况,这是上层App工程师无能为力的。但是如果是厂商或者做Android Framework层的工程师可能需要关心这个,于是做成一个参数配置的方式,让我们灵活选择岂不妙哉。当然,默认是会排除系统自带泄露情况的,不然打开App,弹出一堆莫名其妙的内存泄露让人惶恐,而且我们还可以自己配置。

通过ExcludedRefs这个类

public final class ExcludedRefs implements Serializable {

  public final Map<String, Set<String>> excludeFieldMap;
  public final Map<String, Set<String>> excludeStaticFieldMap;
  public final Set<String> excludedThreads;

  private ExcludedRefs(Map<String, Set<String>> excludeFieldMap,
      Map<String, Set<String>> excludeStaticFieldMap, Set<String> excludedThreads) {
    // Copy + unmodifiable.
    this.excludeFieldMap = unmodifiableMap(new LinkedHashMap<String, Set<String>>(excludeFieldMap));
    this.excludeStaticFieldMap = unmodifiableMap(new LinkedHashMap<String, Set<String>>(excludeStaticFieldMap));
    this.excludedThreads = unmodifiableSet(new LinkedHashSet<String>(excludedThreads));
  }

  public static final class Builder {
    private final Map<String, Set<String>> excludeFieldMap = new LinkedHashMap<String, Set<String>>();
    private final Map<String, Set<String>> excludeStaticFieldMap = new LinkedHashMap<String, Set<String>>();
    private final Set<String> excludedThreads = new LinkedHashSet<String>();

    public Builder instanceField(String className, String fieldName) {
        Preconditions.checkNotNull(className, "className");
      Preconditions.checkNotNull(fieldName, "fieldName");
      Set<String> excludedFields = excludeFieldMap.get(className);
      if (excludedFields == null) {
        excludedFields = new LinkedHashSet<String>();
        excludeFieldMap.put(className, excludedFields);
      }
      excludedFields.add(fieldName);
      return this;
    }

    public Builder staticField(String className, String fieldName) {
        Preconditions.checkNotNull(className, "className");
        Preconditions.checkNotNull(fieldName, "fieldName");
      Set<String> excludedFields = excludeStaticFieldMap.get(className);
      if (excludedFields == null) {
        excludedFields = new LinkedHashSet<String>();
        excludeStaticFieldMap.put(className, excludedFields);
      }
      excludedFields.add(fieldName);
      return this;
    }

    public Builder thread(String threadName) {
        Preconditions.checkNotNull(threadName, "threadName");
      excludedThreads.add(threadName);
      return this;
    }

    public ExcludedRefs build() {
      return new ExcludedRefs(excludeFieldMap, excludeStaticFieldMap, excludedThreads);
    }
  }
}

参考源码的使用方法

时间: 2024-12-10 02:59:56

Android内存泄露检测工具---LeakCanary的前世今生的相关文章

Android 内存泄露检测工具 LeakCanary

LeakCanary 是 Android 和 Java 内存泄露检测框架.LeakCanary 可以用更加直白的方式将内存泄露展现在我们的面前. 开始使用 在 build.gradle 中加入引用,不同的编译使用不同的引用: ? 1 2 3 4 dependencies {    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'    releaseCompile 'com.squareup.leakcanary:leak

Android 内存泄露检测工具 LeakCanary 的监控原理

首先回顾一下  java 的几种 reference: 从jdk 1.2 开始,引用分为 强引用,软引用.若引用 和虚引用, 其中 软引用.若引用 和虚引用 和 ReferenceQueue 关联. 在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象.也就是说,只有对象处于可触及(reachable)状态,程序才能使用它.从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期.这4种级别由高到低依次为:强引用.软引用.弱引

内存泄露检测工具——LeakCanary

很简单:我们不是创建服务不是为了赚钱:我们赚钱是为了提供更好的服务.我们认为这才是做事的态度. 学习使用Java的同学都应该知道,Java的JVM给我们提供的垃圾回收机制是极为好用的.但是我们也很清楚,垃圾回收机制不是万能的,使用不当很容易造成内存泄露.之前我们也介绍过Java中常用的内存泄露检测工具MAT,目前Java程序最常用的内存分析工具应该是MAT(Memory Analyzer Tool),它是一个Eclipse插件,同时也有单独的RCP客户端. 不熟悉MAT的同学,或者对Java垃圾

android 内存泄漏检测工具 LeakCanary 泄漏金丝雀

韩梦飞沙 yue31313 韩亚飞 han_meng_fei_sha [email protected] 内存泄漏检测工具 android 内存泄漏检测工具 ======== 内存泄漏 就是  无用的对象没有被回收,占用着内存,使得可用内存变小了. 如何检测内存泄漏, 可以使用 LeakCanary来检测内存泄漏. leak  是 泄漏的意思.. Canary 是 金丝雀 的意思. 在运行 应用的时候, 泄漏金丝雀 如果检测到内存泄漏 会显示一个通知. ======== LeakCanary捕获

自己实现简易的内存泄露检测工具VLD

有一个很著名的内存泄露检测工具Visual leak detected想必大家都不陌生,但今天我们可以自己写一个简易版的.哈哈,自己动手,丰衣足食有木有!!! 它的原理就是我们重载了操作符new和delete,当用new开辟空间的时候,就讲这块空间串到我们定义的结构体MEMNode形成的链表中,(这是老师的写法,在main程序结束时,用check_vld()函数检测有没有内存泄露)但我觉得,如果我们动态开辟了一个对象,在它的析构函数里用delete释放的话,这种方法就不太可行.因为析构函数只有到

vld,Bounds Checker,memwatch,mtrace,valgrind,debug_new几种内存泄露检测工具的比较

概述 内存泄漏(memory leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,在大型的.复杂的应用程序中,内存泄漏是常见的问题.当以前分配的一片内存不再需要使用或无法访问时,但是却并没有释放它,这时就出现了内存泄漏.尽管优秀的编程实践可以确保最少的泄漏,但是根据经验,当使用大量的函数对相同的内存块进行处理时,很可能会出现内存泄漏. 内存泄露可以分为以下几类:1. 常发性内存泄漏.发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏.2. 偶发性内存泄漏.发生

vld(Visual Leak Detector) 内存泄露检测工具

初识Visual Leak Detector 灵活自由是C/C++语言的一大特色,而这也为C/C++程序员出了一个难题.当程序越来越复 杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题.内存泄漏是最常见的内存问题之一.内存泄漏如果不是很严重,在短时间内对程序不会有太大的 影响,这也使得内存泄漏问题有很强的隐蔽性,不容易被发现.然而不管内存泄漏多么轻微,当程序长时间运行时,其破坏力是惊人的,从性能下降到内存耗尽,甚 至会影响到其他程序的正常运行.另外内存问题的一个共同特点是,内存问题本身

Linux下C++内存泄露检测工具

下载安装:http://blog.csdn.net/wanglin754/article/details/7194145 下载地址:http://www.valgrind.org/downloads/current.html#current 安装valgrind tar jxvf valgrind-3.7.0.tar.bz2             注意这里的参数里加了j,表示有bz2属性 cd valgrind-3.7.0 ./configure make make install make

C程序内存泄露检测工具

今天给大家带来一款检测C程序内存泄露的一款实用工具--memwatch memwatch简介 MEMWATCH 由 Johan Lindh 编写,是一个开放源代码 C 语言内存错误检测工具.只要在代码中添加一个头文件并在 gcc 语句中定义了 MEMWATCH 之后,您就可以跟踪程序中的内存泄漏和错误了.MEMWATCH 支持 ANSI C,它提供结果日志记录,能检测双重释放(double-free).错误释放(erroneous free).没有释放的内存(unfreed memory).溢出