作者:今天想吃什么
转载地址: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的引用
网友评论