Describe the bug
在viewPager2(对于视图回收的viewPager理应也会出现此bug)上,Fragement里放垂直滑动的recyclerView,
第一页Fragement里的RecyclerView adapte设置了空布局后;滑动到第四页,此时,第一页的Fragement已经被回收,执行了onDestroy()方法,但是,此Fragement的实例还在。
这时候再从第四页,滑动回到第一页,Fragement会重走onCreateView和onViewCreated,这时候,由于视图被重建,所以RecyclerView并不相同,同时对RecyclerView进行重新设置adapter。
(理想的、正确的状态是,从第四页回到第一页仍然显示空布局)
前面已经提到,Fragement只是回收视图,但是实例任然在,所以作为全局变量的adapter并没变,adapter仍是同一个,
此时的问题就变成了:同一个adapter设置给了两个或者多个不同实例的RecyclerView,这时候BUG出现,重复设置adapter会报如下错:
ViewHolder views must not be attached when created. Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate(..., boolean attachToRoot)
此错误非常不明确,没有错误具体行数,排查较为麻烦。
经过反复对源码的断点调试,以及对源码翻看,发现问题所在。
首先源码中的mEmptyLayout是一个class全局变量,当显示空布局时,在:
public K onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
………………
case EMPTY_VIEW:
baseViewHolder = createBaseViewHolder(mEmptyLayout);
break;
………………
}
此方法中,mEmptyLayout被直接进行了引用,在第一次给RecyclerView设置adapter的时候,没问题。当第二次把同一adapter设置给不同实例的RecyclerView后,就会出现上述错误。因为mEmptyLayout被第一个RecyclerView视图所持有,不能被添加到第二个RecyclerView上。
也就是说adapter不能被复用,所以在之前的Issuse中,给出的解决方案都是让ViewPager不回收视图,全部保存在内存中。其实说实话,这种方式对内存很不友好。
很简单:将上述方法修为以下:
public K onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
………………
case EMPTY_VIEW:
//在重用mEmptyLayout视图前,先将mEmptyLayout从之前对父布局中释放出来
ViewGroup elViewGroup = (ViewGroup)mEmptyLayout.getParent();
if (elViewGroup != null) {
elViewGroup.removeView(mEmptyLayout);
}
baseViewHolder = createBaseViewHolder(mEmptyLayout);
break;
………………
}
同时,为了保证复用性,以下方法最好也进行修改:
public void bindToRecyclerView(RecyclerView recyclerView) {
// if (getRecyclerView() != null) {
// throw new IllegalStateException("Don't bind twice");
// }
// 改为此判断方式,应该判断是否是同一实例
if (getRecyclerView() == recyclerView) {
throw new IllegalStateException("Don't bind twice");
}
setRecyclerView(recyclerView);
getRecyclerView().setAdapter(this);
}
解决方法是我们此小团队考虑到的最简方式,
第一、不论是对于viewPager还是viewPager2,我们一致认为,不应该强行保存所有视图在内存中,当页数很多时候,同时每个页面也复杂的话,非常占用内存。
第二、viewPager可以设置setOffscreenPageLimit,用于达到缓存的目的,或者重写viewPager适配器中的destroyItem。
但是在AndroidX的viewPager2中,虽然也有setOffscreenPageLimit方法,但是其实际使用效果会有点区别。因为viewPager2本身也是用RecyclerView实现的,所以会遵循RecyclerView的复用机制。具体可以去阅读源码,不再解释。
第三、我们也想到过,每次重建视图,就重新new一个Adapter,但是此问题在于,Adapter除了data数据、空布局状态等等需要保存拷贝,还有很多内部状态需要保存拷贝。除非克隆一个Adapter,这是不现实的,唯有复用,并且RecyclerView Adapter是一个松耦合设计,本身就应该具有复用特性,并且实际测试情况下来,重建的RecyclerView视图在复用Adapter后会自动恢复滚动状态等等,这一特性,已经说明了系统本身的设计用意。
再者,每次对Adapter的重建,都是不必要的性能开销。
我相信从Google的出发点,并不是希望我们什么东西都扔给内存,导致内存的滥用。
其实就是非常常规的写法:
class Test1Fragment : Fragment() {
private val mAdapter by lazy { TestAdapter() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_test, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mRecyclerView.adapter = mAdapter
if (savedInstanceState != null) {
//恢复状态
} else {
//第一次加载数据
//…………
//假如数据加载失败、或者数据为null,显示空布局
mAdapter.setEmptyView(R.layout.layout_empty, mRecyclerView)
}
}
override fun onDestroy() {
super.onDestroy()
}
}
这是一个很优秀的开源库,感谢各位作者。希望大家都参与到项目的维护中,你为人人,人人为我。
如有错误,或者其他见解的,欢迎提出,共同讨论
我觉得adapter不应该持有view,而是保存一个布局资源的id即可,不然在共享缓存池上面也会有问题
没懂,你确定是最简单方式
Most helpful comment
如有错误,或者其他见解的,欢迎提出,共同讨论