關於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裏。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章