内存走漏时兜底计划

在看Android内存优化杂谈的时分看到一个 经过兜底收回内存的解决计划:

Activity走漏会导致该Activity引证到的Bitmap、DrawingCache等无法开释,对内存造成大的压力,兜底收回是指对于已走漏Activity,测验收回其持有的资源,走漏的仅仅是一个Activity空壳,然后下降对内存的压力。 做法也非常简略,在Activity onDestory时分从view的rootview开始,递归开释一切子view涉及的图片,背景,DrawingCache,监听器等等资源,让Activity成为一个不占资源的空壳,走漏了也不会导致图片资源被持有。

   …
   …
   Drawable d = iv.getDrawable();
   if (d != null) {
       d.setCallback(null);
   }        
   iv.setImageDrawable(null);
   ...
   ...

在不确保项目不呈现内存走漏的问题的情况下,保底收回Activity或许Fragment中的一切drawable,内存这个东西嘛,能救回来一点是一点。

这儿的题外话,不是说内存走漏的检测不重要,在聊起来的时分总有人跟我说起来不是应该去解决内存走漏吗?为什么要搞这个。假如你能确保你的项目在经过多年迭代、重构、不同水平层次的开发人员维护、后半夜上线不清醒的情况下修改点什么东西等等等等,这样的情况下确保一点内存走漏的问题都没有的话,那当然当我没说。

这儿做的处理是检测到Activity或许Fragment leaking之后,遍历一切持有的子view,开释占内存大户也便是view持有的图片背景资源,当然不限于drawable,假如还持有一些其他的该开释可是未开释的比方播放器资源,文件句柄文件流网络流也是能够根据情况来开释掉的。

说道这儿其实咱们能够提出一些疑问:

  1. 上述计划是在确定走漏的情况下做的,怎么检测内存走漏?
  2. 为什么不直接开释view而是开释掉drawable?

怎么检测内存走漏

说起检测内存走漏就不能不提leakcanary腾讯Matrix的内存走漏也是学习的它的原理。这儿只简略分析原理并实现一个最简略的leaking检测,不涉及到hprof的获取与分析。

leakcanary的原理

  1. onDestroy的时分经过ReferenceQueue创立WeakReference,并为它设置一个Key,存到Set中。
  2. 等待5s后测验从ReferenceQueue中查找此WeakReference,找到就从Set中移除,不成功则GC后再试一次。
  3. 检查此Key是否还在Set中存在,假如存在则认为是走漏。

上述的步骤便是它检测内存走漏的工作原理,运用了带ReferenceQueueWeakReference的构造办法来创立弱引证,当目标目标只要WeakReference持有的情况下就能够被GC收回,收回之后会放到ReferenceQueue中。所以只要检测会不会呈现在ReferenceQueue中就知道有没有被收回。

最简略的leakcanary工程

根据上面的leakcanary的原理,咱们能够实现一个最简略的leakcanary,用来检测某个目标的内存走漏,这个目标能够不止是activity,也能够是其他的目标比方包含的view或许drawable是否有走漏,这样一个东西能够对咱们本篇文章中的内容做实践上的支撑。
主要的中心代码如下:

public class RefWatcher {
    private final GcTrigger gcTrigger;
    private final Set<String> retainedKeys;
    private final ReferenceQueue<Object> queue;
    public RefWatcher() {
        retainedKeys = new CopyOnWriteArraySet<>();
        queue = new ReferenceQueue<>();
        this.gcTrigger = GcTrigger.DEFAULT;
    }
// Activity destory的时分调用此办法,用来观察目标目标是否收回
    public void watch(Object watchedReference, final String referenceName) {
        final long watchStartNanoTime = System.nanoTime();
        String key = UUID.randomUUID().toString();
        retainedKeys.add(key);
        final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                ensureGone(reference, referenceName, watchStartNanoTime);
            }
        }, 5000);
    }
    void ensureGone(final KeyedWeakReference reference, String referenceName, final long watchStartNanoTime) {
        long gcStartNanoTime = System.nanoTime();
        long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
        removeWeaklyReachableReferences();
        if (gone(reference)) {
            return;
        }
        gcTrigger.runGc();
        removeWeaklyReachableReferences();
        if (!gone(reference)) {
            //do HeapDump, HeapAnalyzer
        }
        return;
    }
    private boolean gone(KeyedWeakReference reference) {
        return !retainedKeys.contains(reference.key);
    }
    private void removeWeaklyReachableReferences() {
// 由于一旦一个目标只要当前类中的弱引证持有的情况下,gc的时分会收回掉这个目标而且目标会被放到queue中,假如queue中有目标说明此目标已经被收回了,从retainedKeys中移除掉。
        KeyedWeakReference ref;
        while ((ref = (KeyedWeakReference) queue.poll()) != null) {
            retainedKeys.remove(ref.key);
        }
    }
}

原理上面说的很清晰了,能够结合代码中的注释加深理解。
愈加具体的代码能够检查SimpleLeakCanary,能够直接clone下来run一下。

相同能够用来检测收回drawable代码是否有用,具体能够检查DrawableRefWatcher。 针对activity中的某一个drawable,在加了上面收回drawable的代码和不加的情况下看针对drawable的内存走漏是否有用。

为什么不直接开释view

上面的解决计划是在内存走漏发生的时分收回开释activity中一切的drawable来实现的。其实咱们很简单就想到,为什么不直接开释view而要去开释drawable呢?

咱们盲猜,那肯定是由于开释drawable简单,由于drawable的引证链比较单一,无非是有个view引证了这个drawable导致它无法开释。
那假如是view呢?它的引证链是什么样的呢?咱们怎么能看到一个view的GC引证链呢?

MAT的运用

MAT是个很好的东西,下载链接在Memory Analyzer (MAT),其实用过eclipse开发Android的年代,很多人应该对他都很熟悉,这儿不具体介绍它的用法,更具体的用法能够来看高爷的 Android 内存优化 (1) – MAT 运用入门,能够看到更多高阶的MAT的用法。

我这儿仅仅抛砖引玉,针对运用MAT怎么检查一个view的一切引证途径。

  1. 找一个目标view,我这儿运用了一个自定义view叫TestLeakingView,这样能够更方便的在MAT中定位它。
  2. dump一份内存快照,能够用AndroidStudio的profiler,在左边点Capture heap dump,等一两秒会生成一份内存快照文件。
    Activity内存泄漏时包含的view还有没有的救?
  3. 将AndroidStudio生成的hprof文件转化成MAT运用的规范格局,能够凭借Android SDK下的hprof-conv东西来做转化,我mac下目录为/Users/yocn/Library/Android/sdk/platform-tools/hprof-conv,用法为 hprof-conv in.hprof out.hprof,得到的out.hprof便是规范的hprof文件

假如找不到自己的SDK目录,能够运用where指令来查找,能看到如下的成果:

Activity内存泄漏时包含的view还有没有的救?

  1. 运用MAT翻开转化之后的规范hprof文件。

这也不展开MAT的用法或许兼容性问题,提示mac最新版MAT需求java-11,在 显现包内容 -> Contents -> Info.plist 文件中装备成自己的java11的途径。

<string>-vm</string><string>/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home/bin/java</string>

这儿翻开dominator_tree,找到咱们自定义的TestLeakingView,右键找到Merge Shortest Path to GC root,挑选with all references能够看到下图:

Activity内存泄漏时包含的view还有没有的救?

RenderNode是硬件加速相关的类,能够参阅运用Android RenderNode加速绘制

能够看到单个view起码被以上的几条途径引证,包括window中的view树,上下文context的引证。这些都是framework控制和显现view的基础,所以还是很难做剥离的。

开释view的测验

其实我看到上面的view的引证的时分也是测验过一些view的开释。依照上面的引证途径做一些测验:

  • 从view树中移除
  • 移除mContext引证

效果其实不是很理想,这儿不赘述测验的办法,尤其是高版本的Android做了一些hide的API和反射办法的限制之后,其实这些测验没有什么意义,仅仅作为思路上的发散,做一些可能性上的探究,如此而已。

总结:

  1. 内存走漏下兜底计划开释drawable是可行的,能够挑选性的运用此计划
  2. leakcanary凭借了ReferenceQueue来做内存走漏的检测
  3. 能够自己实现leakcanary用来做更小粒度的目标的内存走漏的检测,比方view或许drawable
  4. 运用MAT来检测view的引证链,解说为什么开释drawable而不是view
  5. 是否能够做view的内存走漏开释思路的发散及探究