Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

解决方案:ViewHolder views must not be attached when created. Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate(..., boolean attachToRoot) #2796

Closed
limuyang2 opened this issue Jun 28, 2019 · 3 comments

Comments

@limuyang2
Copy link
Collaborator

Describe the bug

  1. 当前使用的版本号: 2.9.45-androidx 、2.9.46

BUG说明

viewPager2(对于视图回收的viewPager理应也会出现此bug)上,Fragement里放垂直滑动的recyclerView,
第一页Fragement里的RecyclerView adapte设置了空布局后;滑动到第四页,此时,第一页的Fragement已经被回收,执行了onDestroy()方法,但是,此Fragement的实例还在。

这时候再从第四页,滑动回到第一页,Fragement会重走onCreateViewonViewCreated,这时候,由于视图被重建,所以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
但是在AndroidXviewPager2中,虽然也有setOffscreenPageLimit方法,但是其实际使用效果会有点区别。因为viewPager2本身也是用RecyclerView实现的,所以会遵循RecyclerView的复用机制。具体可以去阅读源码,不再解释。
第三、我们也想到过,每次重建视图,就重新new一个Adapter,但是此问题在于,Adapter除了data数据、空布局状态等等需要保存拷贝,还有很多内部状态需要保存拷贝。除非克隆一个Adapter,这是不现实的,唯有复用,并且RecyclerView Adapter是一个松耦合设计,本身就应该具有复用特性,并且实际测试情况下来,重建的RecyclerView视图在复用Adapter后会自动恢复滚动状态等等,这一特性,已经说明了系统本身的设计用意。
再者,每次对Adapter的重建,都是不必要的性能开销。

我相信从Google的出发点,并不是希望我们什么东西都扔给内存,导致内存的滥用。

测试使用的Fragment代码

其实就是非常常规的写法:

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()
    }
}

致谢

这是一个很优秀的开源库,感谢各位作者。希望大家都参与到项目的维护中,你为人人,人人为我。

@limuyang2
Copy link
Collaborator Author

如有错误,或者其他见解的,欢迎提出,共同讨论

@zhanzz
Copy link

zhanzz commented Jul 3, 2019

我觉得adapter不应该持有view,而是保存一个布局资源的id即可,不然在共享缓存池上面也会有问题

@ZHANGKUN0917
Copy link

没懂,你确定是最简单方式

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants