关于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.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里。
上一篇: iOS防止内存泄漏