当前位置:网站首页>记一次ViewPager + RecyclerView的内存泄漏
记一次ViewPager + RecyclerView的内存泄漏
2022-06-30 10:40:00 【Android每日一讲】
基本情况
之前在项目上做内存泄漏优化的时候有一个关于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的引用
边栏推荐
- datax - 艰难debug路
- LVGL 8.2 Image styling and offset
- Iptables target tproxy
- Sarsa笔记
- [understanding of opportunity -34]: fate is within the light cone
- 20万奖金池!【阿里安全 × ICDM 2022】大规模电商图上的风险商品检测赛火热报名中!...
- Review of mathematical knowledge: curve integral of the second type
- Collectors. Tomap application
- 软件测试工程师面试基础题(应届生和测试小菜必备)最基础的面试题
- IDEA 又出新神器,一套代码适应多端!
猜你喜欢
20万奖金池!【阿里安全 × ICDM 2022】大规模电商图上的风险商品检测赛火热报名中!...
The reasoning delay on iphone12 is only 1.6 MS! Snap et al. Analyzed the transformer structure latency in detail, and used NAS to find out the efficient network structure of mobile devices
CP2112使用USB转IIC通信教学示例
Rejuvenated Dell and apple hit each other, and the two old PC enterprises declined rapidly
【STL源码剖析】容器(待补充)
Jetpack Compose DropdownMenu跟随手指点击位置显示
同事的接口文档我每次看着就头大,毛病多多。。。
语音识别-基础(一):简介【语音转文本】
Sarsa笔记
无心剑中译狄金森《灵魂择其伴侣》
随机推荐
Ionic4 drag the ion reorder group component to change the item order
Foresniffer tutorial: extracting data
Didi open source agile test case management platform!
LVGL 8.2 Drop down in four directions
DataX JSON description
pytorch 笔记:validation ,model.eval V.S torch.no_grad
Circuit breaker hystrixcircuitbreaker
基于HAL库的按键(KEY)库函数
数据库什么时候需要使用索引【杭州多测师】【杭州多测师_王sir】
Pytorch notes torch nn. BatchNorm1d
IDEA 又出新神器,一套代码适应多端!
LeetCode Algorithm 86. Separate linked list
Cp2112 teaching example of using USB to IIC communication
JS FAQs
Machine learning interview preparation (I) KNN
Mysql database foundation: constraint and identification columns
深潜Kotlin协程(十六):Channel
LVGL8.2 Simple Checkboxes
国产自研系统的用户突破4亿,打破美国企业的垄断,谷歌后悔不迭
文件共享服务器