今兒遇到個場景:在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.getChildCount
和ViewGroup.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. 結論
- 該問題是基於Andorid9.0 Framewrok層
ViewGroup
的一個Bug,靜態內部類的mRoot
沒有及時釋放持有的外部引用導致的泄漏。在Android9.0以前沒有mRoot,Android10在釋放資源時將mRoot置空修復該Bug。
在Android9.0的Java代碼層無法進行修復。 - 基於手機廠商可能會修改fwk層的代碼,有些廠商可能發現了這個bug所以進行了修復,但是有些廠商沒有發現,所以這就導致了並非每個手機都會出現這樣的問題。
- 該問題比較容易出現在多獲取焦點子View的ViewGroup中,比如有RecyclerView、ListView的ViewGroup裏。