原创 @shhp 转载请注明作者和出处
背景说明
为了使问题更加清晰,我将出现问题的场景进行简化抽象。现在有一个Activity
,其主体是一个ListView
。ListView
包含了多个模块,每个模块都对应着自己的视图。每个模块都实现了一个接口Section
:
public interface Section {
public View getView(int position, View convertView, ViewGroup parent);
}
ListView
的adapter的getView
会调用各个Section
的getView
来获取不同模块的视图。
现在有一个模块TestSection
对应的视图是一个横向的RecyclerView
,核心代码如下:
public class TestSection implements Section {
RecyclerView mRecyclerView;
public View getView(int position, View convertView, ViewGroup parent) {
if (mRecyclerView == null) {
mRecyclerView = new RecyclerView(parent.getContext());
mRecyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL, false));
}
return mRecyclerView;
}
}
ListView
支持下拉刷新。刷新之后,ListView
会清除原有的所有Section
,然后根据新的数据创建新的Section
集合。而内存泄漏就在下拉刷新之后出现了!
LeakCanary给出的信息如下:
- org.chromium.base.SystemMessageHandler.mLooper
- references android.os.Looper.mThread
- references thread java.lang.Thread.localValues (named ‘main’)
- references java.lang.ThreadLocal$Values.table
- references array java.lang.Object[].[31]
- references android.support.v7.widget.GapWorker.mRecyclerViews
- references java.util.ArrayList.array
- references array java.lang.Object[].[23]
- references android.support.v7.widget.RecyclerView.mContext
- references com.test.TestActivity
问题探究
LeakCanary给出的信息中有一个比较好的入手点,就是android.support.v7.widget.GapWorker.mRecyclerViews
. 那就来看看这个GapWorker
是何方神圣。
final class GapWorker implements Runnable {
static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();
...
}
GapWorker
里有两个关键的成员sGapWorker
和mRecyclerViews
。根据LeakCanary的信息正是这个mRecyclerViews
引用了TestSection
中的mRecyclerView
导致了内存泄露。注意到sGapWorker
是static
的,初步可以推断是这个静态的sGapWorker
引用了一个GapWorker
实例,而那个GapWorker
实例中的mRecyclerViews
又引用了TestSection
中的mRecyclerView
导致了内存泄露。接下来就要寻找GapWorker
和RecyclerView
的联系。关键代码如下:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
...
GapWorker mGapWorker;
...
private static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;
...
@Override
protected void onAttachedToWindow() {
...
if (ALLOW_THREAD_GAP_WORK) {
// Register with gap worker
mGapWorker = GapWorker.sGapWorker.get();
if (mGapWorker == null) {
mGapWorker = new GapWorker();
...
GapWorker.sGapWorker.set(mGapWorker);
}
mGapWorker.add(this);
}
}
@Override
protected void onDetachedFromWindow() {
...
if (ALLOW_THREAD_GAP_WORK) {
// Unregister with gap worker
mGapWorker.remove(this);
mGapWorker = null;
}
}
}
可以看到RecyclerView
中有一个GapWorker
类型的成员变量mGapWorker
,这个mGapWorker
实际上引用的是一个全局的GapWorker
实例。在onAttachedToWindow
中RecyclerView
将自己加入到那个全局的GapWorker
实例的mRecyclerViews
列表里,而在onDetachedFromWindow
中把自己从那个全局列表中移除。按理有onAttachedToWindow
就会有onDetachedFromWindow
,现在看来问题出现在onDetachedFromWindow
没有被调用。
为了找到问题的真相,让我们回到现在的应用场景。ListView
在下拉刷新之后会清除原有的所有Section
,然后创建新的Section
集合。这也就意味着一个新的TestSection
实例被创建。再来看一下TestSection
的getView
的实现:
public class TestSection implements Section {
RecyclerView mRecyclerView;
public View getView(int position, View convertView, ViewGroup parent) {
if (mRecyclerView == null) {
mRecyclerView = new RecyclerView(parent.getContext());
mRecyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL, false));
}
return mRecyclerView;
}
}
当这个新的TestSection
实例的getView
第一次被调用时,mRecyclerView
为null
。由于ListView
的复用机制,此时参数convertView
并不为null
,而实际上它引用了之前那个TestSection
实例的mRecyclerView
!于是现在出现了两个RecyclerView
,我们将新的mRecyclerView
称为NewRV,原先的mRecyclerView
称为OldRV。在getView
返回后,NewRV成为了ListView
的子view,它的onDetachedFromWindow
会被正常调用。然而OldRV就成为了一个无人管的“野孩子”,没有谁会调用它的onDetachedFromWindow
。于是它就静静地待在那个全局的GapWorker
实例的mRecyclerViews
列表里,很无辜地泄露了整个Activity
!
解决方法
既然已经找到问题的真相,那解决方法也就明了了——正确地复用convertView
即可。
public class TestSection implements Section {
RecyclerView mRecyclerView;
public View getView(int position, View convertView, ViewGroup parent) {
if (mRecyclerView == null) {
if (convertView instanceof RecyclerView) {
mRecyclerView = (RecyclerView) convertView;
} else {
mRecyclerView = new RecyclerView(parent.getContext());
mRecyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL, false));
}
}
return mRecyclerView;
}
}
以后在ListView
中嵌套RecyclerView
时真的要小心内存泄漏了!