热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

RecyclerView引发的内存泄露

原创@shhp转载请注明作者和出处背景说明为了使问题更加清晰,我将出现问题的场景进行简化抽象。现在有一个Activity,其主体是一个ListView。ListView包含了多个模

原创 @shhp 转载请注明作者和出处

背景说明

为了使问题更加清晰,我将出现问题的场景进行简化抽象。现在有一个Activity,其主体是一个ListViewListView包含了多个模块,每个模块都对应着自己的视图。每个模块都实现了一个接口Section:

public interface Section {
public View getView(int position, View convertView, ViewGroup parent);
}

ListView的adapter的getView会调用各个SectiongetView来获取不同模块的视图。

现在有一个模块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 sGapWorker = new ThreadLocal<>();
ArrayList mRecyclerViews = new ArrayList<>();
...
}

GapWorker里有两个关键的成员sGapWorkermRecyclerViews。根据LeakCanary的信息正是这个mRecyclerViews引用了TestSection中的mRecyclerView导致了内存泄露。注意到sGapWorkerstatic的,初步可以推断是这个静态的sGapWorker引用了一个GapWorker实例,而那个GapWorker实例中的mRecyclerViews又引用了TestSection中的mRecyclerView导致了内存泄露。接下来就要寻找GapWorkerRecyclerView的联系。关键代码如下:

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实例。在onAttachedToWindowRecyclerView将自己加入到那个全局的GapWorker实例的mRecyclerViews列表里,而在onDetachedFromWindow中把自己从那个全局列表中移除。按理有onAttachedToWindow就会有onDetachedFromWindow,现在看来问题出现在onDetachedFromWindow没有被调用。

为了找到问题的真相,让我们回到现在的应用场景。ListView在下拉刷新之后会清除原有的所有Section,然后创建新的Section集合。这也就意味着一个新的TestSection实例被创建。再来看一下TestSectiongetView的实现:

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第一次被调用时,mRecyclerViewnull。由于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时真的要小心内存泄漏了!


推荐阅读
author-avatar
沫沫
微交易http://www.ikkwt.com/ 微交易平台http://www.ikkwt.com/pingtai/ 网络借贷平台大全http://www.kljie.com/ 小微金融http://www.lcbcn.com/ 微投资平台http://www.lcbcn.com/pt/ 网络借贷平台排行榜http://www.kljie.com/pingtai/
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有