LeakCanary 内存泄漏 监测 性能优化 简介 原理 MD

Markdown版本笔记 个人GitHub首页 个人博客 个人微信 个人邮箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

LeakCanary 内存泄漏 监测 性能优化 简介 原理 MD
GitHub:https://github.com/square/leakcanary
Demo地址:https://github.com/baiqiantao/LeakCanaryTest.git java


目录

介绍

A memory leak detection 内存泄露检测 library for Android and Java. android

A small leak will sink a great ship. -- Benjamin Franklin
千里之堤, 毁于蚁穴。 -- 《韩非子·喻老》git

简单使用

添加依赖: github

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1' //当使用support库时添加
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1' //发布时用的是无任何操做的版本
}

初始化: 算法

LeakCanary.install(application);

配置完之后,在 debug 构建的版本中,若是检测到某个 activity 或 fragment 有内存泄露,LeakCanary 就会自动地显示一个通知。 编程

更多介绍

  • 为何要使用LeakCanary性能优化

    • 内存泄漏是一种编程错误[programming error],会致使应用程序保留对再也不须要的对象的引用。所以就会致使没法回收为该对象分配[allocated]的内存,最终致使 OutOfMemoryError crash。
    • 例如,Android Activity 实例在调用 onDestroy 方法后就再也不须要了,可是若是在静态字段中存储了对该Activity的强引用将会阻止其被GC[garbage collected]
    • LeakCanary对一个 longer needed 的对象作了惟一标识,并找到阻止它被垃圾回收的引用链。
    • 看成者首次在Square公司的某款App中启用 LeakCanary 后,他找到并修复了多个内存泄漏,并将 OutOfMemoryError 的崩溃率下降了94%。
  • LeakCanary是怎么工做的服务器

    • 经过 RefWatcher.watch() 建立了一个KeyedWeakReference to the watched object。
    • 而后在后台线程检查引用是否被清除了,若是没有,则triggers a GC
    • 若是引用仍是未被清除,则 dumps the heap 到文件系统中的 .hprof 文件中。
    • 在另一个独立的进程中启动 HeapAnalyzerServiceHeapAnalyzer 使用 HAHA 解析 heap dump 。
    • 得益于惟一的 reference key, HeapAnalyzer 在 heap dump 中找到 KeyedWeakReference,而且定位 leaking reference。
    • HeapAnalyzer 计算到 GC roots 的最短强引用路径,并肯定是否有泄露。若是有的话,建立致使泄露的引用链。
    • 计算结果传递到 APP 进程中的 DisplayLeakService 中, 并以通知的形式展现出来。
  • 如何修复内存泄漏微信

    要修复某个内存泄漏,您须要查看该链并查找致使泄漏的那个引用,即在泄漏时哪一个引用本应该被清除的。LeakCanary以红色下划线突出显示可能致使泄漏的引用。app

  • 如何复制 leak trace 信息

    能够经过 logcat 或经过 Leaks App 的菜单复制·

  • Android SDK可能致使泄漏吗

    是。 在AOSP以及制造商实现中,已经存在许多已知的内存泄漏。 当发生这样的泄漏时,做为应用程序开发人员,您几乎没法解决此问题。
    出于这个缘由,LeakCanary 有一个内置的已知Android漏洞列表可供忽略:AndroidExcludedRefs.java。

  • 如何经过 leak trace 挖掘泄漏信息

    有时 leak trace 是不够的,您须要使用 MATYourKit 挖掘 heap dump。 如下是在堆转储中找到泄漏实例的方法:

    • 查找 com.squareup.leakcanary.KeyedWeakReference 的全部实例
    • 对于其中的每个,请查看 key 字段。
    • 找到 key 字段等于 LeakCanary 报告的 the reference key 的 KeyedWeakReference
    • 找到的那个 KeyedWeakReference 的 referent 字段就是您内存泄漏的对象。
    • 此后,问题就掌握在你手中。A good start 是查看 GC Roots的最短路径(除了弱引用)。
  • 如何修复构建错误

    • 若是leakcan-android不在Android Studio的 external libraries 列表中,可是 leakcanary-analyzer 和 leakcanary-watcher 却存在在那里:尝试作一个 Clean Build。 若是仍然存在问题,请尝试经过命令行构建。
    • error: package com.squareup.leakcanary does not exist: 若是您有其余 build types 而不是 debug 和 release,则还须要为这些构建类型添加特定的依赖项(xxxCompile)。
  • LeakCanary添加了多少个方法

    • 若是您使用ProGuard,答案为9或0。
    • LeakCanary 只应在调试版本中使用,并必定要在发布版本中禁用。咱们为您的发布版本提供了一个特殊的空依赖项:leakcanary-android-no-op
    • LeakCanary 的完整版本更大,毫不应在您的 release 版本中发布。
  • 谁在推进 LeakCanary

    LeakCanary由 @pyricau建立并开源,目前由@jrodbx@JakeWharton@pyricau维护。

  • 为何叫 LeakCanary

    LeakCanary这个名称是参考 煤矿中的金丝雀[canary in a coal mine],由于LeakCanary是一个用于经过提早预警危险[advance warning of a danger]来检测风险[detect risks]的哨兵[sentinel]

  • Instant Run可能触发无效的 leaks

    启用Android Studio的即时运行功能可能会致使LeakCanary报告无效的内存泄漏。 请参阅 Android Issue Tracker 上的问题#37967114(https://issuetracker.google.com/issues/37967114)。

  • 我知道我有泄漏,为何通知不显示

    你是否 attached to a debugger? LeakCanary在调试时忽略泄漏检测以免误报。

原理分析

参考

JVM如何断定一个对象是垃圾对象?

JVM采用图论的可达遍历算法来断定一个对象是不是垃圾对象,若是对象A是可达的,则认为该对象是被引用的,GC不会回收;若是对象A或者块B(多个对象引用组成的对象块)是不可达的,那么该对象或者块则断定是不可达的垃圾对象,GC会回收。

内存泄漏的检测机制:

LeakCanary经过ApplicationContext统一注册监听的方式,来监察全部的Activity生命周期,并在Activity的onDestroy时,执行RefWatcher的watch方法,该方法的做用就是检测本页面内是否存在内存泄漏问题。

Activity检测机制是什么?

经过application.registerActivityLifecycleCallbacks来绑定Activity生命周期的监听,从而监控全部Activity; 在Activity执行onDestroy时,开始检测当前页面是否存在内存泄漏,并分析结果。所以,若是想要在不一样的地方都须要检测是否存在内存泄漏,须要手动添加。

检测的流程:

  • 移除不可达引用,若是当前引用不存在了,则不继续执行
  • 手动触发GC操做,gcTrigger中封装了gc操做的代码
  • 再次移除不可达引用,若是引用不存在了,则不继续执行
  • 若是两次断定都没有被回收,则开始分析这个引用,最终生成HeapDump信息

原理:

  • 弱引用与ReferenceQueue联合使用,若是弱引用关联的对象被回收,则会把这个弱引用加入到ReferenceQueue中;经过这个原理,能够看出removeWeaklyReachableReferences()执行后,会对应删除KeyedWeakReference的数据。若是这个引用继续存在,那么就说明没有被回收。
  • 为了确保最大保险的断定是否被回收,一共执行了两次回收断定,包括一次手动GC后的回收断定。两次都没有被回收,很大程度上说明了这个对象的内存被泄漏了,但并不能100%保证;所以LeakCanary是存在极小程度的偏差的。

内存泄漏检测机制是什么?

KeyedWeakReference与ReferenceQueue联合使用,在弱引用关联的对象被回收后,会将引用添加到ReferenceQueue;清空后,能够根据是否继续含有该引用来断定是否被回收;断定回收, 手动GC, 再次断定回收,采用双重断定来确保当前引用是否被回收的状态正确性;若是两次都未回收,则肯定为泄漏对象。

总结下流程就是

  • 断定是否回收(KeyedWeakReference是否存在该引用), Y -> 退出, N -> 向下执行
  • 手动触发GC
  • 断定是否回收, Y -> 退出, N-> 向下执行
  • 两次未被回收,则分析引用状况:
    • humpHeap : 这个方法是生成一个文件,来保存内存分析信息
    • analyze: 执行分析

内存泄漏的轨迹生成机制:

LeakCanary采用了MAT对内存信息进行分析,并生成结果。其中在分析时,分为findLeakingReference与findLeakTrace来查找泄漏的引用与轨迹,根据GCRoot开始按树形结构依次生成当前引用的轨迹信息。

自定义 LeakCanary

  • 如何观察具备生命周期的对象

    在您的应用程序中,您可能有其余具备生命周期的对象,例如Fragment,Service,Dagger组件等。可使用RefWatcher来监视应该进行垃圾回收的实例:refWatcher.watch(schrodingerCat);

  • 使用 no-op 依赖

    release 版本的leakcanary-android-no-op依赖项仅包含LeakCanary和RefWatcher类。若是您要自定义LeakCanary,您须要确保自定义仅出如今 debug 版本中,由于它可能会引用leakcanary-android-no-op依赖项中不存在的类。

  • 自定义图标和标签

    DisplayLeakActivity附带了一个默认图标和标签,您能够经过在应用中提供R.mipmap.leak_canary_iconR.string.leak_canary_display_activity_label来更改它:

  • install 方法的默认逻辑

public static RefWatcher install(Application application) {
   return refWatcher(application)
      .listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      .buildAndInstall();
}
  • 不监测特定的Activity
    默认状况下,LeakCanary会监视全部的Activity。 您能够自定义 installation steps 以执行不一样的操做,例如忽略某种类型Activity的泄漏:
RefWatcher refWatcher = LeakCanary.refWatcher(this)
      .watchActivities(false)
      .buildAndInstall();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
   @Override
   public void onActivityDestroyed(Activity activity) {
      if (activity instanceof IgnoreActivity) {
         return;
      }
      refWatcher.watch(activity);
   }
   //...
});
  • 在运行时打开和关闭 LeakCanary
refWatcher = LeakCanary.refWatcher(this)
      .heapDumper(getHeapDumper()) //在运行时开启和关闭LeakCanary
      .buildAndInstall();
public TogglableHeapDumper getHeapDumper() {
    if (heapDumper == null) {
        LeakDirectoryProvider leakDirectoryProvider = LeakCanaryInternals.getLeakDirectoryProvider(this);
        AndroidHeapDumper defaultDumper = new AndroidHeapDumper(this, leakDirectoryProvider);
        heapDumper = new TogglableHeapDumper(defaultDumper);
    }
    return heapDumper;
}
public class TogglableHeapDumper implements HeapDumper {
    private final HeapDumper defaultDumper;
    private boolean enabled = true;

    public TogglableHeapDumper(HeapDumper defaultDumper) {
        this.defaultDumper = defaultDumper;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public File dumpHeap() {
        return enabled ? defaultDumper.dumpHeap() : HeapDumper.RETRY_LATER;
    }
}
MyApplication.app().getHeapDumper().setEnabled(false);

测试案例

Application

public class MyApplication extends Application {
    private RefWatcher refWatcher;
    private static MyApplication app;
    private TogglableHeapDumper heapDumper;

    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            Log.i("bqt", "此进程是专用于LeakCanary进行堆分析用的。您不该该在此进程中初始化您的应用。");
            return;
        }

        refWatcher = LeakCanary.refWatcher(this)
                .watchActivities(true)  //默认为true,会监视全部Activity,你能够设置为false而后再指定要监测的Activity
                .watchFragments(true) //默认为true,会监视 native Fragment,若是添加了support依赖,则也会监视support中的Fragment
                .watchDelay(1, TimeUnit.SECONDS) //设置应该等待多长时间,直到它检查跟踪对象是否已被垃圾回收
                .maxStoredHeapDumps(7) //设置LeakCanary最多能够保存的 heap dumps 个数,默认为7
                .excludedRefs(getExcludedRefs()) //忽略特定的引用,这个垃圾东西设置后老是不生效
                .heapDumper(getHeapDumper()) //在运行时开启和关闭LeakCanary
                //.listenerServiceClass() //能够更改默认行为以将 leak trace 和 heap dump 上载到您选择的服务器。
                .buildAndInstall();
        app = this;
    }

    private ExcludedRefs getExcludedRefs() {
        return AndroidExcludedRefs.createAppDefaults()//通过大量测试,我感受TMD彻底忽略不了Activity和Fragment中内存泄漏
                .instanceField("com.bqt.test.Single", "imageView") //类名,字段名
                .staticField("com.bqt.test.StaticLeakActivity", "bitmap") //类名,静态字段名
                .clazz("com.bqt.test.StaticLeakActivity") //忽略提供的类名的全部子类的全部字段和静态字段
                .thread("Thread-10086") //忽略指定的线程,通常主线程名为【main】,子线程名为【Thread-整数】
                .build(); //忽略的引用若是又经过watch手动监测了,则仍会监测其内存泄漏状况
    }

    public static MyApplication app() {
        return app;
    }

    public RefWatcher getRefWatcher() {
        return refWatcher;
    }

    public TogglableHeapDumper getHeapDumper() {
        if (heapDumper == null) {
            LeakDirectoryProvider leakDirectoryProvider = LeakCanaryInternals.getLeakDirectoryProvider(this);
            AndroidHeapDumper defaultDumper = new AndroidHeapDumper(this, leakDirectoryProvider);
            heapDumper = new TogglableHeapDumper(defaultDumper);
        }
        return heapDumper;
    }
}

MainActivity

public class MainActivity extends FragmentActivity implements AdapterView.OnItemClickListener {
    private FrameLayout frameLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ListView listView = new ListView(this);
        String[] array = {"静态成员致使的内存泄漏",
                "单例致使的内存泄漏:Fragment",
                "禁用 LeakCanary",
                "",};
        listView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, Arrays.asList(array)));
        listView.setOnItemClickListener(this);
        frameLayout = new FrameLayout(this);
        frameLayout.setId(R.id.fragment_id);
        listView.addFooterView(frameLayout);
        setContentView(listView);
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        switch (position) {
            case 0:
                startActivity(new Intent(this, StaticLeakActivity.class));
                break;
            case 1:
                getSupportFragmentManager().beginTransaction()
                        .add(frameLayout.getId(), new SingleLeakFragment(), "SingleLeakFragment")
                        .commit();
                break;
            case 2:
                MyApplication.app().getHeapDumper().setEnabled(false);
                break;
            default:
                break;
        }
    }
}

静态成员致使的内存泄漏

public class StaticLeakActivity extends Activity {
    private static Bitmap bitmap;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView imageView = new ImageView(this);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon);
        imageView.setImageBitmap(bitmap);
        setContentView(imageView);
    }
}


相关信息:

* com.bqt.test.StaticLeakActivity has leaked:
* InputMethodManager$ControlledInputConnectionWrapper.!(mParentInputMethodManager)!
* ↳ InputMethodManager.!(mLastSrvView)!
* ↳ PhoneWindow$DecorView.mContext
* ↳ StaticLeakActivity
* Reference Key: 7f96d2f1-bf17-47e2-84ad-cd5976d72766
* Device: HUAWEI HONOR PLK-UL00 PLK-UL00
* Android Version: 6.0 API: 23 LeakCanary: 1.6.1 26145bf
* Durations: watch=1007ms, gc=149ms, heap dump=1840ms, analysis=6567ms

单例致使的内存泄漏

public class SingleLeakFragment extends Fragment {
    private ImageView imageView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        imageView = new ImageView(getContext());
        return imageView;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        imageView.setImageResource(R.drawable.icon);
        Single.SINGLETON.setImageView(imageView);//单例中引用View一样会致使Activity内存泄漏
    }
}
public enum Single {
    @SuppressLint("StaticFieldLeak")
    SINGLETON; //定义一个枚举的元素,它就表明了Single的一个实例
    private ImageView imageView;

    public void setImageView(ImageView imageView) {
        this.imageView = imageView;
    }
}


相关信息:

* com.bqt.test.SingleLeakFragment has leaked:
* InputMethodManager$ControlledInputConnectionWrapper.!(mParentInputMethodManager)!
* ↳ InputMethodManager.!(mLastSrvView)!
* ↳ ListView.mOnItemClickListener
* ↳ MainActivity.mFragments
* ↳ FragmentController.mHost
* ↳ FragmentActivity$HostCallbacks.mFragmentManager
* ↳ FragmentManagerImpl.mAdded
* ↳ ArrayList.array
* ↳ array Object[].[0]
* ↳ SingleLeakFragment
* Reference Key: 4877bf10-596c-440f-b69c-5d239f670944
* Device: HUAWEI HONOR PLK-UL00 PLK-UL00
* Android Version: 6.0 API: 23 LeakCanary: 1.6.1 26145bf
* Durations: watch=17245ms, gc=138ms, heap dump=1675ms, analysis=8159ms

使用LeakCanary检测内存泄露中文翻译

原文

Nov 18 2015

咱们的 App 曾经遇到不少的内存泄漏致使 OutOfMemoryError 的崩溃,一些甚至是在生产环境。Square 的 Pierre-Yvews Ricau 开发了 LeakCanary 最终解决了这些问题。LeakCanary 是一个帮助你检测和修复内存泄漏的工具。在这个分享中,Pierre 教授你们如何修复内存泄漏的错误,让你的 App 更稳定和可靠。

介绍 (0:00)

你们好,我是 Pierre-Yvews Ricau (叫我 PY 就行),如今在 Square 工做。

Square 出了一款名为:Square Register 的 App, 帮助你用移动设备完成支付。在用这个 App 的时候,用户先要登录他的我的帐号。

不幸的是,在签名页面有的时候会由于内存溢出而出现崩溃。老实说,这个崩溃来的太不是时候了 — 用户和商家都没法确认交易是否完成了,更况且是在和钱打交道的时候。咱们也强烈的意识到,咱们须要处理下内存溢出或者内存泄露这种事情了。

内存泄漏:非技术讲解 (1:40)

我想要聊的内存泄露解决方案是: LeakCanary。

LeakCanary 是一个能够帮助你发现和解决内存泄露的开源工具。

可是到底什么是内存泄露呢?咱们从一个非技术角度来开始,先来举个例子。
...
有外部的引用指向了本不该该再指向的对象。相似这样的小规模的内存泄露堆积之后就会形成大麻烦。

LeakCanary 救援 (3:47)

这就是咱们为何要开发 LeakCanary。

我如今可能已经清楚了 可被回收的 Android 对象应该及时被销毁。

可是我仍是无法清楚的看到这些对象是否已经被回收掉。有了 LeakCanary 之后,咱们:

给可被回收的 Android 对象上打了智能标记,智能标记能知道他们所指向的对象是否被成功释放掉。

若是过一小段时间对象依然没有被释放,他就会给内存作个快照。LeakCanary 随后会把结果发布出来:

帮助咱们查看到内存到底怎么泄露了,并清晰的向咱们展现那些没法被释放的对象的引用链。

举个具体的例子:在咱们的 Square App 里的签名页面。用户准备签名的时候,App 由于内存溢出出错崩溃了。咱们不能确认内存错误到底出在哪儿了。

签名页面持有了一个很大的有用户签名的 Bitmap 图片对象。图片的大小和用户手机屏幕大小一致 — 咱们猜想这个有可能会形成内存泄露。首先,咱们能够配置 Bitmap 为 alpha 8-bit 来节省内存。这是很常见的一种修复方案,并且效果也不错。可是并无完全解决问题,只是减小了泄露的内存总量。可是内存泄露依然在哪儿。

最主要的问题是咱们 App 的堆满了,应该要留有足够的空间给咱们的签名图片,可是因为不少处的内存泄露叠加在一块儿占用了不少内存。

技术讲解内存泄漏 (8:06)

假设,我有一个 App,这个 App 点一下就能买一个法棍面包。

private static Button buyNowButton;

因为某种缘由,我把这个 button 设置成了 static 的。问题随之而来:

这个按钮除非你设置成了null,否则就内存泄露了!

你也许会说:“只是一个按钮而已,没啥大不了”。问题是这个按钮还有一个成员变量:叫 mContext,这个东西指向了一个 Acitvity,Acitivty 又指向了一个 Window,Window 又拥有整个 View 继承树。算下来,那但是一大段的内存空间。

静态的变量是 GC root 类型的一种。垃圾回收器会尝试回收全部非 GC root 的对象,或者某些被 GC root 持有的对象。因此若是你建立一个对象,而且移除了这个对象的全部指向,他就会被回收掉。可是一旦你将一个对象设置成 GC root,那他就不会被回收掉。

当你看到相似“法棍按钮”的时候,很显然这个按钮持有了一个 Activity 的引用,因此咱们必须清理掉它。当你沉浸在你的代码的时候,你确定很难发现这个问题。你可能只看到了引出的引用。你能够知道 Activity 引用了一个 Window,可是谁引用了 Activity?

你能够用像 IntelliJ 这样的工具作些分析,可是它并不会告诉你全部的东西。一般,你能够把这些 Object 的引用关系组织成图,可是是个单向图。

分析堆 (10:16)

咱们能作些什么呢?咱们来作个快照。

咱们拿出全部的内存而后导出到文件里,这个文件会被用来分析和解析堆结构。
其中一个工具叫作 Memory Analyzer,也叫 MAT。
它会经过 dump 的内存,而后分析全部存活在内存中的对象和类。

你能够用 SQL 对他作些查询,相似以下:

SELECT * FROM INSTANCEOF android.app.Activity a WHERE a.mDestroyed = true

这条语句会返回全部的状态为 destroyed 的实例。一旦你发现了泄露的 Activity,你能够执行 merge_shortest_paths 的操做来计算出最短的 GC root 路径。从而找出阻止你 Acitivty 释放的那个对象。

之因此说要 “最短路径”,是由于一般从一个 GC root 到 Acitivty,有不少条路径能够到达。好比说:个人按钮的 parent view,一样也持有一个 mContext 对象。

当咱们看到内存泄露的时候,咱们一般不须要去查看全部的这些路径。咱们只须要最短的一条。那样的话,咱们就排除了噪音,很快的找到问题所在。

LeakCanary 救你于水火 (12:04)

有 MAT 这样一个帮咱们发现内存泄露的工具是个很棒的事情。可是在一个正在运行的 App 的上下文中,咱们很难像咱们的用户发现泄露那样发现问题所在。咱们不能要求他们在作一遍相同操做,而后留言描述,再把 70MB+ 的文件发回给咱们。咱们能够在后台作这个,可是并不 Cool。咱们指望的是,咱们可以尽早的发现泄露,好比在咱们开发的时候就发现这些问题。这也是 LeakCanary 诞生的意义。

一个 Activity 有本身生命周期。你了解它是如何被建立的,如何被销毁的,你指望他会在 onDestroy() 函数调用后,回收掉你全部的空闲内存。若是你有一个可以检测一个对象是否被正常的回收掉了的工具,那么你就会很惊讶的喊出:“这个可能形成内存泄露!它本该被回收掉,但却没有被垃圾回收掉!”

Activity 无处不在。不少人都把 Activity 当作神级 Object 通常的存在,由于它能够操做 Services,文件系统等等。常常会发生对象泄漏的状况,若是泄漏对象还持有 context 对象,那 context 也就跟着泄漏了。

Resources resources = context.getResources();
LayoutInflater inflater = LayoutInflater.from(context);
File filesDir = context.getFilesDir();
InputMethodManager inputMethodManager =(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);

LeakCanary API 演练 (13:32)

咱们回过头来再看看智能标记smart pin,咱们但愿知道的是当生命后期结束后,发生了什么。幸运的时,LearkCanary有一个很简单的 API。

第一步:建立 RefWatcher。这里的Ref 实际上是 Reference 的缩写。给 RefWatcher 传入一个对象的实例,它会检测这个对象是否被成功释放掉。

RefWatcher refWatcher = LeakCanary.install(this);

第二步:监听 Activity 生命周期。而后,当 onDestroy 被调用的时候,咱们传入 Activity。

refWatcher.watch(this);// Make sure you don’t get installed twice.

什么是弱引用 (14:17)

想要了解这个是怎么工做的,我得先跟你们聊聊弱引用Weak References

我刚才提到过静态域的变量会持有Activity 的引用。因此刚才说的“下单”按钮就会持有 mContext 对象,致使 Activity 没法被释放掉。这个被称做强引用Strong Reference

一个对象能够有不少的强引用,在垃圾回收过程当中,当这些强引用的个数总和为零的时候,垃圾回收器就会释放掉它。
弱引用就是一种不增长引用总数的持有引用方式,垃圾回收期是否决定要回收一个对象,只取决于它是否还存在强引用。

因此说,若是咱们:

将咱们的 Activity 持有为弱引用,一旦咱们发现弱引用持有的对象已经被销毁了,那么这个 Activity 就已经被垃圾回收器回收了。
不然,那能够大概肯定这个 Activity 已经被泄露了。

弱引用的主要目的是为了作 Cache,并且很是有用。主要就是告诉 GC,尽管我持有了这个对象,可是若是一旦没有对象在用这个对象的时候,GC 就能够在须要的时候销毁掉。

在下面的例子中,咱们继承了 WeakReference:

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(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
        this.key = checkNotNull(key, "key");
        this.name = checkNotNull(name, "name");
    }
}

你能够看到,咱们给弱引用添加了一个 Key,这个 Key 是一个惟一字符串。想法是这样的:当咱们解析一个heap dump文件的时候,咱们能够遍历全部的 KeyedWeakReference 实例,而后找到对应的 Key。

首先,咱们建立一个 weakReference,而后咱们写入『一下子,我须要检查弱引用』。(尽管一下子可能就是几秒后)。当咱们调用 watch 函数的时候,其实就是发生了这些事情。

public void watch(Object watchedReference, String referenceName) {
   checkNotNull(watchedReference, "watchedReference");
   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(() -> ensureGone(reference, watchStartNanoTime));
}

在这一切的背后,咱们调用了 System.GC (免责声明— 咱们本不该该去作这件事情)。然而,这是一种告诉垃圾回收器:『Hey,垃圾回收器,如今是一个不错的清理垃圾的时机。』,而后咱们再检查一遍,若是发现有些对象依然存活着,那么可能就有问题了。咱们就要触发 heap dump 操做了。

HAHA 内存分析器 (16:55)

亲手作 heap dump 是件超酷的事情。当我亲手作这些的时候,花了不少时间和功夫。我每次都是作相同的操做:

下载 heap dump 文件,在内存分析工具里打开它,找到实例,而后计算最短路径。

可是我很懒,我根本不想一次次的作这个。(咱们都很懒对吧,由于咱们是开发者啊!)

我本能够为内存分析器写一个 Eclipse 插件,可是 Eclipse 插件机制太糟糕了。后来我灵机一动,我其实能够把某个 Eclipse 的插件,移除 UI,利用它的代码。

HAHA 是一个无 UI Android 内存分析器。基本上就是把另外一我的写的代码从新打包。开始的时候,我就是 fork 了一份别的代码而后移除了UI部分。两年前,有人从新 fork 了个人代码,而后添加了 Android 支持。又过了两年,我才发现这我的的仓储,而后我又从新打包上传到了 maven center

我最近根据 Android Studio 修改了代码实现。代码还说的过去,还会继续维护。

LeakCanary 的实现 (19:19)

咱们有本身的库去解析 heap dump 文件,并且实现的很容易。咱们打开 heap dump,加载进来,而后解析。而后咱们根据 key 找到咱们的引用。而后咱们根据已有的 Key 去查看拥有的引用。咱们拿到实例,而后获得对象图,再反向推导发现泄漏的引用。

以上(下)全部的工做都发生在 Android 设备上。
  • 当 LeakCanary 探测到一个 Activity 已经被销毁掉,而没有被垃圾回收器回收掉的时候,它就会强制导出一份 heap dump 文件存在磁盘上。
  • 而后开启另一个进程去分析这个文件获得内存泄漏的结果。若是在同一进程作这件事的话,可能会在尝试分析堆内存结构的时候而发生内存不足的问题。
  • 最后,你会获得一个通知,点击一下就会展现出详细的内存泄漏链。并且还会展现出内存泄漏的大小,你也会很明确本身解决掉这个内存泄漏后到底可以解救多少内存出来。

LeakCanary 也是支持 API 的,这样你就能够添加内存泄漏的回调,比方说能够把内存泄漏问题传到服务器上
用上 API 之后,咱们的程序崩溃率下降了 94%!简直棒呆!

Debug 一个真实的例子 (22:12)

这个是 Android 4年前的一次代码修改留下的问题,当时是为了修复另外一个 bug,然而带来了没法避免的内存泄漏。咱们也不知道什么时候能被修复。

忽略 SDK Crashes (28:10)

一般来讲,老是有些内存泄漏是你没法修复的。咱们某些时候须要忽略掉这些没法修复的内存泄漏提醒。在 LeakCanary 里,有内置的方法去忽略没法修复的问题。

我想要重申一下,LeakCanary 只是一个开发工具。不要将它用到生产环境中。一旦有内存泄漏,就会展现一个通知给用户,这必定不是用户想看到的。

咱们即使用上了 LeakCanary 依然有内存溢出的错误出现。咱们的内存泄露依然有多个。有没有办法改变这些呢?

LeakCanary 的将来 (29:14)

public class OomExceptionHandler implements Thread.UncaughtExceptionHandler {
    private final Thread.UncaughtExceptionHandler defaultHandler;
    private final Context context;

    public OomExceptionHandler(Thread.UncaughtExceptionHandler defaultHandler, Context context) {...}

    @Override
    public void UncaughtException(Thread thread, Throwable ex) {
        if (containsOom(ex)) {
            File heapDumpFile = new File(context.getFilesDir(), "out-of-memory.hprof");
            Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
        }
        defaultHandler.uncaughtException(thread, ex);
    }

    private boolean containsOom(Throwable ex) {...}
}

这是一个 Thread.UncaughtExceptionHandler,你能够将线程崩溃委托给它,它会导出 heap dump 文件,而且在另外一个进程里分析内存泄漏状况。

有了这个之后,咱们就能作一些好玩儿的事情了,好比:列出全部的应该被销毁却依然在内存里存活的 Activity,而后列出全部的 Detached View。咱们能够依此来为泄漏的内存按重要性排序。

我实际上已经有一个很简单的 Demo 了,是我在飞机上写的。尚未发布,由于还有些问题,最严重的问题是没有足够的内存去解析 heap dump 文件。想要修复这个问题,得想一想别的办法。好比采用 stream 的方法去加载文件等等。

Q&A (31:50)

Q: LeakCanary 能用于 Kotlin 开发的 App?
PY: 我不知道,可是应该是能够的,毕竟到最后他们都是字节码,并且 Kotlin 也有引用。

Q:大家是在 Debug 版本一直开启 LeakCanary 么?仍是只在最后的某些版本开启作作测试
PY: 不一样的人有不一样的方法,咱们一般是一直都开着的。

备注:这篇文章是2015年做者视频内容的中文翻译,有一些内容可能已经改变了。

2018-10-2

相关文章
相关标签/搜索