RecyclerView的BUG探讨

RecyclerView 这是一个全新的列表控件,这里就暂时不讨论它的用法了,百度随便一搜就会出现一大堆用法,这里讨论的是我在使用过程中遇到的一个bug。异常信息如下:

10-29 00:44:52.845: E/AndroidRuntime(17874): java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{42cbf960 position=12 id=-1, oldPos=1, pLpos:1 scrap no parent}
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:3147)
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:3277)
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:3258)
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1803)
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1302)
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1265)
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:522)
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:1872)
10-29 00:44:52.845: E/AndroidRuntime(17874):    at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:2155)

当然网上还有一个类似的BUG Inconsistency detected. Invalid item position 157(offset:157).state:588(这个BUG似乎是谷歌程序猿的锅,反正网上大家都这样说,暂且就相信这种说法吧)。然而我遇到的则是第一种bug,说实话这种bug很难排查,因为异常的堆栈根本没有你自己的代码(对于这种情况的BUG,我暂时还没总结出一套好的排查方式,希望有人看到的话,能留言指导我一下,不胜感激)。于是我把异常信息放到百度 谷歌搜索了一下,搜到下面这篇文章,虽然我还是很疑惑,但是至少给我们指明了一个方向。链接在这里哦
文章中说到:在进行数据移除和数据增加时,务必要保证RVAdapter中的数据和移除的数据保持一致!什么意思呢?我自己琢磨认为是这样的,如果你更新你的集合后,调用RVAdapter的新出现的notifyxxxx方法时,adapter 的更新预期结果和实际集合更新结果不同,那么就会出现异常了。
举个例子吧: 比如你集合remove了两条数据,但是你RVAdapter只notifyItemRemoved(2)(这里表示移除第三条数据,只移除了一条),这种情况旧属于数据不一致了。还有一种是你增加数据,你集合增加10条数据,但是你RVAdapter的notify只增加了5条数据,这也是数据不一致。
好了,通过以上两种情况,大概就知道了,这个数据一致其实说的是要保证数量一致。就是说RVAdapter有个size,你的集合有个size。这两个size,在调用RvAdapter的notifyxxxx时候必须保持相同。
下面我们来看实际案例吧(本人项目出现的BUG,╮(╯▽╰)╭调BUG伤不起啊,这么晚还一边写博客,一边猜想BUG)。
需求:
监听用户输入,每输入一个字就向服务器请求一次数据,返回最新的搜索结果。
实现:

public class TestActivity extends Activity {

        RecyclerView mRecyclerView;
        RecyclerAdapter mAdapter;
        LinearLayoutManager manager;
        Button search;
        List<String> searchDatas = new ArrayList<String>();
        boolean isloading;
        Handler mHandler = new Handler() {

                @Override
                public void handleMessage(android.os.Message msg) {
                        mAdapter.notifyItemRangeInserted(0, 10);
                        isloading = false;

                };
        };
        Runnable searchTask = new Runnable() {

                public void run() {
                        int size = searchDatas.size();
                        for (int i = 0; i < 10; i++) {
                                searchDatas.add("这是第" + (size + i) + "个搜索");
                        }                       
                        mHandler.sendEmptyMessage(0);
                }
        };

        @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                mRecyclerView = (RecyclerView) findViewById(R.id.list);
                manager = new LinearLayoutManager(this);
                mRecyclerView.setLayoutManager(manager);
                mAdapter = new RecyclerAdapter(searchDatas);
                mRecyclerView.setAdapter(mAdapter);
                search = (Button) findViewById(R.id.search);
                search.setOnClickListener(new OnClickListener() {
                        //为了少写点Demo (*^__^*)  这里用按钮点击 来代替用户输入监听
                        @Override
                        public void onClick(View v) {
                                if (!isloading) {
                                        isloading = true;
                                        searchDatas.clear();                                      
                                        mAdapter.notifyDataSetChanged();                                        
                                        new Thread(searchTask).start();
                                }

                        }
                });

        }
}

这段代码有没有问题呢?以我个人来看,我觉得是没有问题,然而现实确实,我一点搜索就会崩溃,而且神奇的是,能看到RecyclerView已经刷新,当然一瞬间程序就crash了。 然而实际项目里,我也是这样做的却没有崩溃,为什么Demo就crash了呢?问题就出现在我为searchDatas这个集合赋值的过程里,Demo里我为了省事,随便开个子线程添加了一点数据,这中间的时间是非常短的,而实际项目需要去服务器请求数据 还要解析数据,这个耗时就会长一点。 于是我修改了一下我的searchTask:

Runnable searchTask = new Runnable() {

                public void run() {
                        try {
                                Thread.sleep(3000);
                        } catch (InterruptedException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                        }
                        int size = searchDatas.size();
                        for (int i = 0; i < 10; i++) {
                                searchDatas.add("这是第" + (size + i) + "个搜索");
                        }                       
                        mHandler.sendEmptyMessage(0);
                }
        };

于是,神奇的事情发生了,程序不会Crash了。这尼玛坑爹呢?这中间发生了什么状况啊?还记得我上面说过有个现象吗?Demo在crash之前是有一瞬间显示了数据的,于是我又再次修改了我的searchTask。

 Runnable searchTask = new Runnable() {

                public void run() {

                        int size = searchDatas.size();
                        for (int i = 0; i < 10; i++) {
                                searchDatas.add("这是第" + (size + i) + "个搜索");
                                if (i==5) {
                                        try {
                                                Thread.sleep(3000);
                                        } catch (InterruptedException e) {
                                                // TODO Auto-generated catch block
                                                e.printStackTrace();
                                        }
                                }
                        }                       
                        mHandler.sendEmptyMessage(0);
                }
        };

猜猜这一次会发生什么?WTF!是的,你没有猜错,我点击按钮后,RecyclerView显示了6条数据,然后大概三秒后,程序crash了。这个现象确实很神奇,到这里我有个猜测,是不是RVAdapter的notifyDataSetChanged()方法影响了呢?于是,我把这个调用给注释了。结果似乎真的就是这样,我第一次点击按钮的时候,程序是不会crash的,也能正常显示数据。但是我第二次点击,程序就crah了。
接下来我做了一个简单的验证,第二次点击程序是在我调用searchDatas.clear()方法后crash的。
经过以上实验,我得到了一个不一定正确的结论:
1.RVAdapter的notifyDataSetChanged方法执行后,在一定时间内,如果你更新了你的集合(无论是否在主线程更新集合),那么这个更新会实时反应到控件上,也就是说你的控件显示也会更新。
2.在上面结论的基础上,也就是说你调用notifyDataSetChanged方法后,很快又更新了集合,然后调用了诸如notifyItemRangeInserted这样的方法,这个时候是会触发BUG的,如果是再调用notifyDataSetChanged方法是不会触发BUG的。
3.针对上面的情况,我得到一个初步的解决办法,也就是说我每次清空数据调用notifyDataSetChanged方法之后,执行更新集合任务先休眠几秒(任务要是执行太快就像一开始的Demo那样了),至于休眠几秒这个我暂时还没想清楚,大概就休眠3秒再执行吧。
额,因为这篇文章是我熬夜写的,其实我也是一边写,一边做验证的,所以我又整理了一下我的结论,结论应该如下:
1.RVAdapter的notifyDataSetChanged方法执行后,在一定时间内,如果你更新了你的集合(无论是否在主线程更新集合),那么这个更新会实时反应到控件上,也就是说你的控件显示也会更新。
2.调用诸如notifyItemRangeInserted这样的方法之前,考虑清楚你的集合到底更新成什么样了!要注意参考结论1,结论1会影响你的判断。
好了,到此,BUG应该是解决了,明天到公司再验证吧!至于为何会出现结论1这种奇葩情况,我暂时也不清楚,希望英语好的哥们帮我去google的社区问问吧。
这篇文章因为是我熬夜一边猜想验证,一边写的,应该还有不少问题,希望看到的人能够留言指出,不胜感激!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值