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

内存泄漏时兜底方案

在看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,等一两秒会生成一份内存快照文件。
    dump.png
  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命令来查找,能看到如下的结果:

where
  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可以看到下图:

MAT_0.png

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的内存泄漏释放思路的发散及探索
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容