欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-06 11:36:25
...

今儿遇到个场景:在Android P(API 28)中,在退出了含有RecyclerView的RelativeLayout中,LeakCanary报了这么一个内存泄漏:
关于ViewGroup$ViewLocationHolder$mRoot的内存泄漏

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类:
关于ViewGroup$ViewLocationHolder$mRoot的内存泄漏
下面是截取自Android8.0的代码:
关于ViewGroup$ViewLocationHolder$mRoot的内存泄漏
下面是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里。