微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

ViewPager + RecyclerView的内存泄漏

作者:今天想吃什么
转载地址:https://juejin.cn/post/7113395533460275236

基本情况

之前在项目上做内存泄漏优化的时候有一个关于RecyclerView内存泄漏,页面结构如图:

LeakCanary捕获的引用链如下

┬───
│ GC Root: Thread object
│
├─ java.lang.Thread instance
│    Thread name: 'main'
│    ↓ Thread.threadLocals
│             ~~~~~~~~~~~~
├─ java.lang.ThreadLocal$ThreadLocalmap instance
│    ↓ ThreadLocal$ThreadLocalmap.table
│                                 ~~~~~
├─ java.lang.ThreadLocal$ThreadLocalmap$Entry[] array
│    ↓ ThreadLocal$ThreadLocalmap$Entry[4]
│                                      ~~~
├─ java.lang.ThreadLocal$ThreadLocalmap$Entry instance
│    ↓ ThreadLocal$ThreadLocalmap$Entry.value
│                                       ~~~~~
├─ androidx.recyclerview.widget.GapWorker instance
│    ↓ GapWorker.mRecyclerViews
│                ~~~~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList[0]
│               ~~~
╰→ androidx.recyclerview.widget.RecyclerView instance

找出问题

从引用链可以看出关键点在于GapWorker,首先看看这个GapWorker

RecyclerView在android 21及以上版本会使用GapWorker实现预加载机制,在Recyclerview的onAttachedToWindow方法中尝试将其实例化,并通过GapWorker的add方法将Recyclerview自身添加GapWoker的成员变量mRecyclerViews链表中去,在onDetachedFromWindow调用GapWorkerremove方法移除其对自身的引用,GapWoker实例保存在其类静态成员变量sGapWorker(ThreadLocal)中,确保主线程只有一个实例

 RecyclerView

 @Override
    protected void onAttachedToWindow() {
        ......
        if (ALLOW_THREAD_GAP_WORK) {
          //从ThreadLocal中获取GapWorker实例,为null则直接创建一个
            mGapWorker = GapWorker.sGapWorker.get();
            if (mGapWorker == null) {
                mGapWorker = new GapWorker();
                display display = ViewCompat.getdisplay(this);
                float refreshRate = 60.0f;
                if (!isInEditMode() && display != null) {
                    float displayRefreshRate = display.getRefreshRate();
                    if (displayRefreshRate >= 30.0f) {
                        refreshRate = displayRefreshRate;
                    }
                }
                mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
              //将创建的GapWorker实例设置到ThreadLocal中去
                GapWorker.sGapWorker.set(mGapWorker);
            }
          //添加自身的引用
            mGapWorker.add(this);
        }
    }

 @Override
    protected void onDetachedFromWindow() {
        ......
        if (ALLOW_THREAD_GAP_WORK && mGapWorker != null) {
          //异常自身的引用
            mGapWorker.remove(this);
            mGapWorker = null;
        }
    }


final class GapWorker implements Runnable {
  ......
    static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();

    ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();

  	public void add(RecyclerView recyclerView) {
        if (RecyclerView.DEBUG && mRecyclerViews.contains(recyclerView)) {
            throw new IllegalStateException("RecyclerView already present in worker list!");
        }
        mRecyclerViews.add(recyclerView);
    }

    public void remove(RecyclerView recyclerView) {
        boolean removeSuccess = mRecyclerViews.remove(recyclerView);
        if (RecyclerView.DEBUG && !removeSuccess) {
            throw new IllegalStateException("RecyclerView removal Failed!");
        }
    }
  ......

GapWoker实例创建后在主线程的ThreadLocalmap中将以一个key为sGapWorker,value为此实例的Entry保存,GapWoker不会主动调用sGapWorker(ThreadLocal)的remove方法将这个Entry从ThreadLocalmap中移除,也就是说主线程对应的ThreadLocalmap会一直持有这个Entry,那么这就为Recyclerview的内存泄漏创造了条件:只要GapWorker.addGapWorker.remove没有成对的调用,就会导致Recyclerview一直被GapWorker的成员mRecyclerViews持有强引用,形成引用链:

Thread->ThreadLocalmap->Entry(sGapWorker,GapWoker实例)->mRecyclerViews->Recyclerview->Context

接下来就是找到问题发生的地方了,通过断点发现RecyclerviewonAttachedToWindow方法执行了两次,onDetachedFromWindow方法只执行了一次,这就导致了GapWorkermRecyclerViews还保留着一个Recyclerview的引用,所以找到为什么onAttachedToWindow多执行一次就是问题的答案了,那么通常情况下布局里的View的onAttachedToWindow什么时候会被调用

  1. ViewRootImpl首帧绘制的时候,会层层向下调用子view的dispatchAttachedToWindow方法在这方法中会调用onAttachedToWindow方法
  2. 将子View添加到父ViewGroup中,并且父ViewGroup的成员变量mAttachInfo(定义在View中)不为空时(在dispatchAttachedToWindow方法中赋值,dispatchDetachedFromWindow方法中置空),view的dispatchAttachedToWindow会被调用,进而调用onAttachedToWindow方法

页面的结构分析,Recyclerview属于Fragment的View,而Fragment依附在ViewPager上,则Fragment的实例化由ViewPager控制,在ViewPager的onMeasure方法中可以看到它会去加载当前页的Fragment

  ViewPager

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	......
    	mInLayout = true;
    	//1 实例化当前页Fragment
        populate();
        mInLayout = false;
    		......
    }

    void populate() {
        populate(mCurItem);
    }

    void populate(int newCurrentItem) {
	  		......
        if (curItem == null && N > 0) {
        //2 在这里面会调用adapter的instantiateItem方法实例化fragment
        //并且将会调用FragmentManager.beginTransaction()启动事务,将fragment的attach,add等行为添加进去
            curItem = addNewItem(mCurItem, curIndex);
        }
	......
	//3 在这里面会执行前面生成的事务,将fragment的view添加到ViewPager中
	mAdapter.finishUpdate(this);
	...... 
	}
    }

代码的第三点中FragmentManager执行事务将Fragment的view添加到ViewPager中,这里也就是上文说到的onAttachedToWindow方法调用的第二种情况。(此时ViewPager已经在绘制流程中,mAttachInfo不为空)

再看项目中Fragment加载view的代码,如下:

 项目中的Fragment
	override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_list, container, true /**问题所在*/)
        //这里需要注意的是LayoutInflator.infalte的attachToRoot为true时,返回的是传入的root参数,也就container
        //此处的container实际是ViewPager,因此需要再通过findViewById找到R.layout.fragment_list的根view返回
        val list = view.findViewById<RecyclerView>(R.id.list)
        return list
    }

inflate方法attachToRoot参数传递了true,导致了LayoutInflater调用root.addView()将view添加到root(也就是ViewPager)中去

LayoutInflater
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
	......
	if (root != null && attachToRoot) {
    	root.addView(temp, params);
	}
	......
}

ViewGroup
 public void addView(View child, LayoutParams params) {
        addView(child, -1, params);
 }

 public void addView(View child, int index, LayoutParams params) {
 	......
 	addViewInner(child, index, params, false);
 }

 private void addViewInner(View child, LayoutParams params,
            boolean preventRequestLayout) {
        ......
        //ViewPager已经在measure过程中,mAttachInfo不为空,此case会进入
        AttachInfo ai = mAttachInfo;
        if (ai != null && (mGroupFlags & FLAG_PREVENT_disPATCH_ATTACHED_TO_WINDOW) == 0) {
            ......
            //child为fragment中加载的view
            child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
        }
        ......
 }

梳理一下流程:ViewPager在onMeasure中加载Framgent,Fragment的onCreateView中加载view时attachToWindow传true触发了view的第一次onAttachedToWindow,在Fragment加载完成之后,ViewPager没有判断view的父View是否为自身,又通过FragmentManager再一次将view添加进来,这就触发了view的第二次onAttachedToWindow,至此Recyclerview两次调用 mGapWorker.add(this)将自身添加GapWokermRecyclerViews中去,在Activity退出时,onDetachedFromWindow调用了一次,则mRecyclerViews还残留了一个Recyclerview的强引用,这就导致了内存泄漏的发生。

解决方案:将true改为false解决问题,有时候不起眼的小错误总能浪费你很多时间

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐