• 记一次ViewPager + RecyclerView的内存泄漏


    基本情况

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

    pic_layer.png

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    找出问题

    从引用链可以看出关键点在于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!");
            }
        }
      ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    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);
    	...... 
    	}
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在代码的第三点中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
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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, 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));
            }
            ......
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    梳理一下流程: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对于其指向的ThreadLocal实例是强引用,这就导致了ThreadLocalMap中对应的Entry的Key不会被gc回收,那么ThreadLocal中的getset对key为null的Entry移除的辅助机制也无法生效,因此除了主动移除Entry之外,只能等到主线程退出之后GapWorker才会被回收,但是主线程退出了这个回收已经没有意义了。

    既然这样为什么Entry的Key还要使用弱引用?

    假设key使用的是强引用,设想有这样一个场景,我们使用线程池创建了多个线程,且这些线程在执行任务过程中都调用了sGapWorkerset方法进行赋值,这些线程在执行完之后会被缓存,那么这些线程的ThreadLocalMap对应的Entry中的Key会对sGapWorker指向的ThreadLocal实例持有强引用,导致实例无法被回收出现内存泄漏,那么key使用弱引用就能避免这种问题。

    既然这样为什么Entry的Value为什么不使用弱引用?

    class Test{
    	static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
    	void A(){
    		sGapWorker.set(new GapWorker());
    	}
    	
    	void B(){
    		GapWorker gp = sGapWorker.get()
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    假设value使用的是弱引用,设想有这样一个场景,首先调用Test的方法A,接着发生了gc,由于value指向的GapWorker对象只有value对它的弱引用了,那么它将被回收,在这之后的某个时间调用了方法B,则这时候获取到的值会是null。可见这种情况下value保存的值相当不稳定,随时都可能被回收。

    但由于value使用的是强引用,value引用的对象还是存在着内存泄漏的可能,ThreadLocal的set和get方法中也会对这些key为null的Entry进行清除,不过这样回收的时机就存在不确定性,为避免value的内存泄漏,就需要我们主动在适当的时候调用ThreadLocalremove方法清除value的引用

  • 相关阅读:
    速卖通卖家如何抓住产品搜索权重
    大模型 Decoder 的生成策略
    【四:Unittest框架】
    【K8S 七】Metrics Server部署中的问题
    python--基础知识点--memray
    [附源码]Python计算机毕业设计大学生项目众筹系统
    【frp实现内网穿透踩坑到成功篇】
    Spock单元测试框架介绍及在美团优选的实践_第二章(static静态方法mock方式)
    Web自动化测试进阶:网页中难点之等待机制 —— 强制等待,隐式等待
    KubeVela 1.4.x 官方文档
  • 原文地址:https://blog.csdn.net/m0_62167422/article/details/125492825