当前位置:网站首页>ViewPager + RecyclerView的内存泄漏
ViewPager + RecyclerView的内存泄漏
2022-07-05 18:10:00 【码中之牛】
作者:今天想吃什么
转载地址: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
会调用GapWorker
的remove
方法移除其对自身的引用,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.add
和GapWorker.remove
没有成对的调用,就会导致Recyclerview
一直被GapWorker
的成员mRecyclerViews
持有强引用,形成引用链:
Thread->ThreadLocalMap->Entry(sGapWorker,GapWoker实例)->mRecyclerViews->Recyclerview->Context
接下来就是找到问题发生的地方了,通过断点发现Recyclerview
的onAttachedToWindow
方法执行了两次,onDetachedFromWindow
方法只执行了一次,这就导致了GapWorker
的mRecyclerViews
还保留着一个对Recyclerview
的引用,所以找到为什么onAttachedToWindow
多执行一次就是问题的答案了,那么通常情况下布局里的View的onAttachedToWindow
什么时候会被调用?
ViewRootImpl
首帧绘制的时候,会层层向下调用子view的dispatchAttachedToWindow
方法,在这个方法中会调用onAttachedToWindow
方法- 将子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, 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)
将自身添加到GapWoker
的mRecyclerViews
中去,在Activity退出时,onDetachedFromWindow
调用了一次,则mRecyclerViews
还残留了一个对Recyclerview
的强引用,这就导致了内存泄漏的发生。
解决方案:将true改为false解决问题,有时候不起眼的小错误总能浪费你很多时间
思考
ThreadLocalMap的Entry对于Key不是弱引用吗?为什么还会导致内存泄漏?
从弱引用的定义上来看,一个对象若只被弱引用所引用,那么对象会被gc回收。但从GapWorker
的源码可以看到,sGapWorker
是static final
修饰的类静态成员,sGapWorker
对于其指向的ThreadLocal
实例是强引用,这就导致了ThreadLocalMap
中对应的Entry的Key不会被gc回收,那么ThreadLocal
中的get
和set
对key为null的Entry移除的辅助机制也无法生效,因此除了主动移除Entry之外,只能等到主线程退出之后GapWorker
才会被回收,但是主线程退出了这个回收已经没有意义了。
既然这样为什么Entry的Key还要使用弱引用?
假设key使用的是强引用,设想有这样一个场景,我们使用线程池创建了多个线程,且这些线程在执行任务过程中都调用了sGapWorker
的set
方法进行赋值,这些线程在执行完之后会被缓存,那么这些线程的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()
}
}
假设value使用的是弱引用,设想有这样一个场景,首先调用Test
的方法A,接着发生了gc,由于value指向的GapWorker
对象只有value对它的弱引用了,那么它将被回收,在这之后的某个时间调用了方法B,则这时候获取到的值会是null。可见这种情况下value保存的值相当不稳定,随时都可能被回收。
但由于value使用的是强引用,value引用的对象还是存在着内存泄漏的可能,ThreadLocal的set和get方法中也会对这些key为null的Entry进行清除,不过这样回收的时机就存在不确定性,为避免value的内存泄漏,就需要我们主动在适当的时候调用ThreadLocal
的remove
方法清除value的引用
边栏推荐
- 破解湖+仓混合架构顽疾,星环科技推出自主可控云原生湖仓一体平台
- [performance test] full link voltage test
- Sophon CE Community Edition is online, and free get is a lightweight, easy-to-use, efficient and intelligent data analysis tool
- 彻底理解为什么网络 I/O 会被阻塞?
- nano的CAN通信
- [TestLink] testlink1.9.18 solutions to common problems
- Writing writing writing
- Career advancement Guide: recommended books for people in big factories
- MATLAB中print函数使用
- 从类生成XML架构
猜你喜欢
Introduction to the development function of Hanlin Youshang system of Hansheng Youpin app
彻底理解为什么网络 I/O 会被阻塞?
Leetcode daily question: the first unique character in the string
Sophon autocv: help AI industrial production and realize visual intelligent perception
Binder开辟线程数过多导致主线程ANR异常
Can communication of nano
Let more young people from Hong Kong and Macao know about Nansha's characteristic cultural and creative products! "Nansha kylin" officially appeared
Simulate the hundred prisoner problem
Whether to take a duplicate subset with duplicate elements [how to take a subset? How to remove duplicates?]
Isprs2022 / Cloud Detection: Cloud Detection with Boundary nets Boundary Networks Based Cloud Detection
随机推荐
开户注册股票炒股安全吗?有没有风险的?靠谱吗?
南京大学:新时代数字化人才培养方案探讨
图像分类,看我就够啦!
Login and connect CDB and PDB
buuctf-pwn write-ups (9)
Le cours d'apprentissage de la machine 2022 de l'équipe Wunda arrive.
Sophon Base 3.1 推出MLOps功能,为企业AI能力运营插上翅膀
Generate classes from XML schema
EasyCVR平台通过接口编辑通道出现报错“ID不能为空”,是什么原因?
Privacy computing helps secure data circulation and sharing
JVM third talk -- JVM performance tuning practice and high-frequency interview question record
Unicode processing in response of flash interface
Tkinter window preload
Tupu software digital twin | visual management system based on BIM Technology
Leetcode daily question: the first unique character in the string
Isprs2022 / Cloud Detection: Cloud Detection with Boundary nets Boundary Networks Based Cloud Detection
Nacos distributed transactions Seata * * install JDK on Linux, mysql5.7 start Nacos configure ideal call interface coordination (nanny level detail tutorial)
How can cluster deployment solve the needs of massive video access and large concurrency?
文章中的逻辑词
Can communication of nano