记一次ViewPager + RecyclerView的内存泄漏

基本情况

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

记一次ViewPager + 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办法将R链表逆置ecyclerview本身增加到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,val实例化对象ue为此实例的Entry链表结构保存,源码编辑器GapWoker不会自动调用sGapWorker(ThreadLocal)的remove办法将这个Entry从ThreadLocalMap中移除,也便是说主线程对应的ThreadLocalMap会一向链表的特点持有这个Entry,那么这就为Recyclerview的内存走漏创造了条件:只要GapWor成员变量和成员方法ker.add线程池拒绝策略GapWorker.remove没有成对的调用,就会导致Recyclerview一向被GapWorker的成员mRecyclerViews持有强引证,构成引证链:源码交易平台

Thread源码交易平台->ThreadLocalMap->Entry(sGapW实例化对象是什么意思orker,GapWoker实例)->mRecyclerViews->Recyclerview->Context

接下来便是找到问题产生的当地了,经过断点发现RecyclerviewonAtt线程池原理achedToWindow办法履行了两次,onDetachedF源码编辑器下载romWindow办法只履行了一次,这就导致了GapWorkermRecyclerViews还保留着一个对Recyclerview的引证,所以找到为什么o实例化servlet类异常nAttachedToWin成员变量和成员方法dow多履行一次便是问题的答案了,那么线程池原理通常情况下布局里的View的onAttachedToWindow什么时候会被调用?

  1. ViewRootImpl首帧绘制的时候,会层层链表向下调用子view的dispatchAttachedToWindow办法,在这个办法中会调用onAttachedToWindow办法
  2. 实例化和初始化的区别子View增加到父Vi线程池的工作原理ewGroup中,并且父ViewGro链表up的成员变量mAttachInfo(界说在View中)不为空时(在dispatchAttachedToWindow办法中赋值,dispatchDetachedFromWindow办法中线程池核心参数置空),view的dispatchA线程池面试题tt成员变量和成员方法achedToWindow会被调用,从而调用到onAtta实例化和初始化的区别chedToWindow办法

从页面的结构剖析实例化对象是什么意思,Recyclerview归于Fr线程池核心参数agment的View,而Fr线程池拒绝策略agment依附在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现已在绘制流程中,mAtta链表chInfo不为空)

再看项目中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,导源码编程器致了La线程池的使用youtInflater会调用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, int index, 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解决问源码编程器题,有时候不起眼的小错误总能浪费你很多时刻

考虑

ThreadLocalMap的Entry关于Key不是弱引证吗?为什么还会导致内存走漏?

从弱引证的界线程池参数配置说上来看,一个目标若只被弱引证所引证,那么目标会被gc收回。但从GapWorker源码可以看到,sGapWorkerstatic final源码网站饰的类静态成员,sGapWorker关于其指向的ThreadLoc链表和数组的区别al实例是强引证,这就导致了ThreadL成员变量和静态变量的区别ocalMap中对应的Entry的Key不会被gc收回,那么Thr成员变量和成员方法eadLocal中的getset对key为nul源码编辑器下载l的Entry移除的辅佐机制也无法收效,因此链表和数组的区别除了自动移除Entry之外,只能比及主线程退出之后GapWorker才会被收回,可是主线程退出源码精灵永久兑换码了这个收回现已没有意义了。

既然这样为什么Entry的K源码网站ey还要运用弱引证?

假定key运用的是强引证,设想有这样一个场景,我们运用线程池创立了多个线程,且这些线程在履行任务进程中都调用了sGapWorkerset办法进行赋值,这些线程在履行完之后会被缓存,那么这些线程的ThreadLocalMap对应的Entry中的Key会对sGapWorker指向的ThreadL实例化对象ocal实例持有强引证,导致实例无法被收回呈现内存走漏,成员变量是什么意思那么k线程池拒绝策略ey运用弱引证就能防止这种问题。

既然这样为什么Entry实例化对象的Value为什么不运用弱引证?

class Test{
	static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
	void A(){
		sGapWorker.set(new GapWorker());
	}
	void B(){
		GapWorker gp = sGapWorker.get()
	}
}

假定value运用的是弱引证,设想有这样一个场景,首要调用Test的办法A,接着产生了gc,由于value指向的GapWorker目标只要value对它的弱引证了链表逆置,那么它将被收回,在这之后的某个时刻调用了办法B,则这时候获取到的值源码交易平台会是nu源码编辑器ll。可见这种情况下value保存的值相当不稳定,随时都或许被收回。

但由于value运用的是强引证,value引证的线程池目标仍是存在着内存走漏的或许,ThreadLocal的set和get办法中也会对这些ke源码时代y为null的Entry进源码之家行铲除,不过这线程池面试题样收回的时机就存在不确定性,为防止va源码编辑器下载lu源码之家e的内存走漏,就需求我们自动在适当的时候调用ThreadL实例化servlet类异常ocalremove办法铲除value的引证

发表回复

提供最优质的资源集合

立即查看 了解详情