关于ViewGroup$ViewLocationHolder$mRoot的内存泄漏

今儿遇到个场景:在Android P(API 28)中,在退出了含有RecyclerView的RelativeLayout中,LeakCanary报了这么一个内存泄漏:
在这里插入图片描述

1. 定位问题

1.1 定位源码

在AndroidP中ViewGroup内部有这么一个静态内部类ViewLocationHolder

// ViewGroup.java
    /**
     * Pooled class that holds a View and its location with respect to
     * a specified root. This enables sorting of views based on their
     * coordinates without recomputing the position relative to the root
     * on every comparison.
     */
    static class ViewLocationHolder implements Comparable<ViewLocationHolder> {
        private static final int MAX_POOL_SIZE = 32;
        private static final SynchronizedPool<ViewLocationHolder> sPool =
                new SynchronizedPool<ViewLocationHolder>(MAX_POOL_SIZE);
        public static final int COMPARISON_STRATEGY_STRIPE = 1;
        public static final int COMPARISON_STRATEGY_LOCATION = 2;
        private static int sComparisonStrategy = COMPARISON_STRATEGY_STRIPE;
        private final Rect mLocation = new Rect();
        private ViewGroup mRoot;   // 1
        public View mView;
        private int mLayoutDirection;

        public static ViewLocationHolder obtain(ViewGroup root, View view) {
            ViewLocationHolder holder = sPool.acquire();  // 2
            if (holder == null) {
                holder = new ViewLocationHolder();
            }
            holder.init(root, view);  // 3
            return holder;
        }
        
        private void init(ViewGroup root, View view) {
            Rect viewLocation = mLocation;
            view.getDrawingRect(viewLocation);
            root.offsetDescendantRectToMyCoords(view, viewLocation);
            mView = view;
            mRoot = root;   // 4
            mLayoutDirection = root.getLayoutDirection();
        }

        private void clear() {  //5
            mView = null;
            mLocation.set(0, 0, 0, 0);
        }
       .....
    }

从英文注释可以看出来,这个类的作用是保存一个View和它的位置(Rect),使用它的类能做通过 List<ViewLocationHolder>的compare,能把这些View在原来的ViewGroup上排列好,而不用重新去计算这些view在ViewGroup上的顺序位置了。
我们在来解析一下源码:
注释1:这个静态类有个全局变量 mRoot,表示的是这个View的父View
注释2:因为他是以池子的形式存储,所以它的获取方式是 obtain(),在池子中取出一个空的ViewLoacationHolder,如果取不出,就new一个出来。

注释3:拿到注释2的 ViewLocationHodler,调用 init()对它初始化
注释4:赋值mRoot

注释5:在clear()方法中,并没有把mRoot置空…

但从这里看,我们就已经知道了为什么泄漏了,在ViewGroup销毁的时候,由于其静态内部类ViewLocationHolder的mRoot字段没有释放,所以它持有着这个ViewGroup的引用,导致ViewGroup的内存也不能释放,产生了内存泄漏。

1.2 是否能解决

解决方法是在 clear()中将mRoot字段置空,或者将 ViewLocationHolder.mRoot字段设置为弱引用
但是,我们修改不了ViewGroup的源码,它是属于framework层的= = ,他是来自于framework层的Bug,所以我们只能任由这个泄漏出现…

2. 源码反推

这里不得不产生了更多的问号。
(1)为什么是只有Andorid P有这个玩意?
(2)我用到ViewGroup的地方这么多,那是不是只要在Android P上,我随时随地都可能出现这个Bug?

对于这样的问题,我不得不再往下深入代码了= =
首先,我们得先找到ViewLocationHolder会在什么时候拿出来用,它的入口方法是 ViewLocationHolder.obtain(),我们要看看是谁调用了obtain:

// ViewGroup.java
    /**
     * Pooled class that orderes the children of a ViewGroup from start
     * to end based on how they are laid out and the layout direction.
     */
    static class ChildListForAccessibility {

        private static final int MAX_POOL_SIZE = 32;
        private static final SynchronizedPool<ChildListForAccessibility> sPool =
                new SynchronizedPool<ChildListForAccessibility>(MAX_POOL_SIZE);
        private final ArrayList<View> mChildren = new ArrayList<View>();
        private final ArrayList<ViewLocationHolder> mHolders = new ArrayList<ViewLocationHolder>();  // 1
        
        public static ChildListForAccessibility obtain(ViewGroup parent, boolean sort) {  
            ChildListForAccessibility list = sPool.acquire(); // 2
            if (list == null) {
                list = new ChildListForAccessibility();
            }
            list.init(parent, sort);   // 3
            return list;
        }

        private void init(ViewGroup parent, boolean sort) {
            ArrayList<View> children = mChildren;   // 4
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {  
                View child = parent.getChildAt(i);
                children.add(child);    // 5
            }
            if (sort) { // 6
                ArrayList<ViewLocationHolder> holders = mHolders; // 7
                for (int i = 0; i < childCount; i++) {
                    View child = children.get(i);
                    ViewLocationHolder holder = ViewLocationHolder.obtain(parent, child); // 8
                    holders.add(holder);
                }
                sort(holders);  // 9
                for (int i = 0; i < childCount; i++) {  // 10
                    ViewLocationHolder holder = holders.get(i);
                    children.set(i, holder.mView); 
                    holder.recycle();   // 11
                }
                holders.clear();
            }
        }
        ...
    }    

这里又出现了了一个ViewGroup的静态内部类:ChildListForAccessibility ,从英文注释中可以看出,它的作用就是管理所有的ViewLocationHolder,而且它同样被放在一个池子中。在注释1中,它持有了一个ViewLocationHolder类型的list。来解析下这个源代码:

注释2、3:从池子中取出一个空的ChildListForAccessibility,然后调用其 init()

注释4、5:调用ViewGroup.getChildCountViewGroup.getChildAt,拿到所有的子View存放到 children对象中。
注释6:判断是否需要对这些子View进行排序,如果要,则进入到if语句中去。

注释7:创建 ViewLocationHolder类型的list
注释8:为注释5中的 children对象里面的每一个子View创建一个 ViewLocationHolder,并放入到注释7的list中
注释9:给这个list排序。排序后,里面所有的子View都有了顺序。
注释10:遍历这个排序的list,重新将排好序的list的子View放到 children对象中去。
注释11:注释7的list已经没用了,所以调用每个 ViewLocationHolder.recycler(),这个方法就会调用上节中的 clear()释放资源。

这个类的作用是对ViewGroup的所有子类进行排序,所以我们要找到从哪里进行排序的,因为ChildListForAccessibility.obtain()是入口方法,所以我们要找到使用到这个方法的地方,我发现有两处ViewGroup的方法调用了它,他们分别是

  • ViewGroup.addChildrenForAccessibility
    将可以访问(即可以有焦点)的子View添加到 outChildren这个对象中
  • ViewGroup.dispatchPopulateAccessibilityEventInternal
    用来分发焦点事件,遍历所有排序后的子View,如果某个子View获取焦点,则退出循环。

这个方法是处理一个ViewGroup里面可以获得焦点的子View的一类方法。也就是说,无论是哪个版本,都可以执行这些方法。

下面是截取的Android7.0的ViewGroup的 ViewLocationHolder类:
在这里插入图片描述
下面是截取自Android8.0的代码:
在这里插入图片描述
下面是Android10.0的代码:

    static class ViewLocationHolder implements Comparable<ViewLocationHolder> {
        ....
        private ViewGroup mRoot;
        ....
        private void clear() {
            mView = null;
            mRoot = null; // 这里置空了
            mLocation.set(0, 0, 0, 0);
        }
    }

这里发现,Android10.0中在clear()方法里,对mRoot置空了,就把这个Bug给修了…

3. 结论

  1. 该问题是基于Andorid9.0 Framewrok层 ViewGroup的一个Bug,静态内部类的mRoot没有及时释放持有的外部引用导致的泄漏。在Android9.0以前没有mRoot,Android10在释放资源时将mRoot置空修复该Bug。
    在Android9.0的Java代码层无法进行修复。
  2. 基于手机厂商可能会修改fwk层的代码,有些厂商可能发现了这个bug所以进行了修复,但是有些厂商没有发现,所以这就导致了并非每个手机都会出现这样的问题。
  3. 该问题比较容易出现在多获取焦点子View的ViewGroup中,比如有RecyclerView、ListView的ViewGroup里。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值