recyclerview崩溃问题研究

关键词:IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder,
Added View has RecyclerView as parent but view is not a real child

研究起因

关于recyclerview就不用多做介绍了,做android开发的相信大家都知道,大多数人会使用它,利用一些三方adapter库几行代码实现复杂item的加载效果,但是真正说到rv中涉及到的一些源码可能很多人也都是一知半解,这段时间真的是花了时间去研究rv的源码,研究rv源码最主要的起因是由于公司内部使用adapter所引起的一些疑难杂症,项目集成上线后会时不时的报一些rv崩溃的异常,问题存在有一些时间,但是苦于rv巨多的代码,崩溃一直没人处理,虽说发生的频率不是很高,但是看着那些无从下手,莫名其妙的崩溃,内心其实是比较煎熬的。这里要说下公司内部使用的adapter库并没有直接使用现有的一些开源库,而是自己进行的代码封装,自己封装adapter的好处显而易见代码可控性高,出现问题可以快速定位,随便还能学一波和rv相关的东西。谁知道不小心翻了车,还是对rv的源码没有理解到位造成的。

Inconsistency detected. Invalid view holder adapter positionViewHolder

bug如风,常伴吾身,上报的崩溃有两个,都是磨人的小妖精,曾抱着侥幸的心理,想是不是rv自身的bug导致的崩溃,直到我亲手把这两个bug给复现出来才知道原来是封装adapter有问题导致的崩溃。先来说一下第一个bug

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder

不知道看到这个崩溃的各位有没有一种似曾相似,其实这个崩溃算是比较常见的崩溃,网上也有相关的解决方法,解释的比较清楚了,就是data数据更新后没有及时调用notify方法引起的,可以在rv的源码中找到这个崩溃的根源

 if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
                throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
                        + "adapter position" + holder + exceptionLabel());
            }

引起bug的根源就是满足了holder.mPosition >= mAdapter.getItemCount()这个条件,现在的问题是如何复现这个bug,其实方法很简单,当你清除数据后没有及时调用notify方法直接去滑动rv的时候,这个bug就会出现,现在解释下引起bug的原因

当清除完数据后此时mAdapter.getItemCount()的值将为0,而holder.mPosition的值实际上就是各个item的对应位置,很显然任意·一个item都会满足holder.mPosition >= mAdapter.getItemCount()这个条件。而如果在清除完数据后调用notifiyDatachange方法,代码最终会通知rv进行重新onlayout,该方法中会调用到layoutmanager一个非常重要的函数fill,fill函数的主要作用就是将各个item排布到rv上,内部通过while循环依次排布各个item,关键判断如下

while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
  ......
}

此时layoutState.hasMore(state)为false,意思就是rv中没有更多的item可以排布,所以抛异常的代码根本不会执行到。

在清除完数据源除了调用notifiyDatachange同步下数据源之外,还可以调用adapter提供的若干局部刷新方法如notifyItemRemove等,这些方法的原理和notifiyDatachange有些不同,是通过更改holder.mposition值来避免
holder.mPosition >= mAdapter.getItemCount()成立,从而避开进入抛异常的代码。

经过上述分析可以知道一个事实就是当数据源改变后一定要及时调用一系列的notify方法来避免这个崩溃的产生。实际上除了数据源清空没有调用notify方法会引发崩溃外,当对数据源进行删除若干项的操作而没有调用notify也会引起崩溃,只不过这种情况下崩溃没有这么容易触发,需要滑动rv到一定程度才会引发崩溃。将数据直接清空不调用notify的操作是让崩溃重现的最佳方法。

Inconsistency detected bug产生实际场景

既然找到了崩溃引发的根源,那么接下来要去解决的问题在项目代码中去查找究竟是哪里的逻辑引发的崩溃。最终根据bugly崩溃日志信息定位到了大致位置,剩下的就是需要自己根据代码逻辑去仔细排查,只要抓住数据源改变后没及时调用notify这个原因,那么发现问题代码就是时间问题。

这里说下项目问题代码的逻辑,项目中有这么个场景,当用户没有登录时rv会展示数据,当用户登录后rv展示的数据会根据网络请求进行改变,很常见的一个逻辑,问题就出在写代码的同事不知道数据源改变同步调用notify,当进行用户登录操作时会进行一个数据源清空的操作,然后在登陆完毕后请求到数据执行add操作和notifydatachange来展示新的数据。就是这么个操作会有概率引起崩溃问题,当清空数据源之后notifydatachange不是紧接着调用而是在数据返回后才调用,如果这时网络有延迟notifydatachange就会一直得不到调用,如果此时有用户在屏幕上执行滑动操作那么恭喜你app crash!!。这个问题没有被测试发现的一个原因就在于内网网络速度很快,可以说notifydatachange几乎都是跟在数据清空后调用的,还有一个比较坑的地方在于明明是清空数据不同步调用notify引起的崩溃,但bugly会把崩溃发生的代码定位到登陆逻辑里面,导致问题查了半天找不到原因,这个崩溃在项目中存在有一段时间都没人解决,主要原因还是在于没有对rv有一定了解。

Added View has RecyclerView as parent but view is not a real child

很难搞定的一个bug,但最终还是在我的不懈努力下找到了解决方法。此时有掌声,啪啪啪...在bugly是一个比较稀有的bug,常出没于一些比较低端点的手机上,时有时无若隐若现,这个崩溃不是rv自身代码bug,而是我们自身代码存在一定缺陷引起的,好在网上可以搜到一些关于这个bug的传闻,当快速滑动rv到加载更多的时候有时候会触发这个bug,但是网上没有一篇文章有对这个bug的原因有过分析,只是知道触发条件是快递滑动rv到加载更多,不停重复这个操作会复现这个bug。
但是我按照这个操作来来回回操作了几十次,换了n台测试机都没触发过这个崩溃,程序没崩溃这就让我很崩溃了,让我们看下这个崩溃的源码所在,addviewint内部有如下代码

if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
                // ensure in correct position
                int currentIndex = mChildHelper.indexOfChild(child);
                if (index == -1) {
                    index = mChildHelper.getChildCount();
                }
                if (currentIndex == -1) {
                    throw new IllegalStateException("Added View has RecyclerView as parent but"
                            + " view is not a real child. Unfiltered index:"
                            + mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel());
                }
                if (currentIndex != index) {
                    mRecyclerView.mLayout.moveView(currentIndex, index);
                }
            }

在这里我们发现了引起崩溃的具体条件child.getParent() == mRecyclerView并且currentIndex==-1,第一个条件getParent为rv,当addviewint的时候,被添加的child的parent居然不为null,一种似曾相似的感觉,相信很多人之前都遇到过viewpager中左右滑动view时可能会报出一个类似 child already has parent的崩溃,没想到rv中居然也报了这么个崩溃。

问题分析

这个child.getParent()==mRecyclerView的条件着实让人费解,结合网上流传滑动到底部加载更多引起崩溃的原因大致可以猜测引起崩溃的view是一个"加载更多"view,那么问题来了为什么其他view都没有问题唯独"加载更多"这个view会有问题。在我们封装adapter的时候一般都会提供加载更多这么个功能,加载更多严格来说并不是真正的数据源,而是adapter在getitemcount的时候我们通过对数据源.size+1来忽悠adapter,让adapter误以为我们有这么多数据要加载,当加载到最后一个item时,通过inflate加载更多的layoutid来展示“加载更多”布局,项目原来封装的adapter里面有这样一段逻辑足以引起怀疑,这段代码在oncreateviewholder时会被调用到,生成的view作为一个rootview传递给holder

public final View onCreateView(ViewGroup parent) {
        if (itemView == null) {
            itemView = inflate(layoutRes, parent, false);
            onViewCreate(itemView);
        }
        return itemView;
    }

注意这个条件当itemView==null才inflate,如果不为null则直接复用itemView,理论上每一个rv只会存在一个"加载更多"布局,以下简称loadmore,我们每次滑到底部看到的loadmore实际上都是通过rv的缓存机制得到的,但是在个别极端情况下是会存在两个loadmore布局的!!

双生loadmore情况

这是个很坑爹的情况,但是却是真实有可能发生的,只不过概率没这么高而已,我们都知道rv默认情况下都是有动画效果的,而loadmore的动画效果就是loadmore布局下移,然后新获取的数据展示出来,相信这种动画效果大家都见过,经过对源码分析发现一个情况就是当loadmore还在执行下移动画此时快速滑动rv到底部的时候就会出现这种双生loadmore情况,何为双生laodmore,说白了就是同时会存在两个loadmore,而这两个loadmore用的却是同一个itemview,这种情况最大的一个特点就是此时第一个loadmore还在执行动画中,还并没有从rv上移除所以此时getParent==mRecyclerView,至此第一个崩溃条件就成立了,这二个条件 int currentIndex = mChildHelper.indexOfChild(child);值为-1,具体就不想分析了,原因就是执行动画的时候得到的值就是-1就对了,到此两条件满足崩溃重现江湖!!

解决崩溃问题

bug知道根源都好解决,难得是发现崩溃的整个过程,想解决很简单只要破坏第一个条件成立的情况就可以了,修改onCreateView中的代码如下

public final View onCreateView(ViewGroup parent) {
        itemView = inflate(layoutRes, parent, false);
        onViewCreate(itemView);
        return itemView;
    }

到此世界就清净了。。。让每一个loadmore创造出来的时候独立拥有一个itemview即可。写到这里就不免想到那个笑话,考核程序员绩效通过代码量来体现,修一个bug只删了两行代码,但是整个过程却让我把rv的源码来回翻了N次。这个bug之所有这么难复现主要原因还是这个bug太细节了,低端手机性能比较差在loadmore执行动画还没结束的时候的快速下拉有更大概率触发崩溃。还有一个大坑就是这个bug在compat 27的包中会被提前报出,导致我在demo使用compat 27的代码复现这个bug的时候死活不能复现。

复现路上的大坑

通过代码对比就能发现问题所在,compat26中rv的oncreateviewholder代码

public final VH createViewHolder(ViewGroup parent, int viewType) {
            TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
            final VH holder = onCreateViewHolder(parent, viewType);
            holder.mItemViewType = viewType;
            TraceCompat.endSection();
            return holder;
        }

接下来是compat 27代码中的oncreateviewholder

public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
            try {
                TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
                final VH holder = onCreateViewHolder(parent, viewType);
                if (holder.itemView.getParent() != null) {
                    throw new IllegalStateException("ViewHolder views must not be attached when"
                            + " created. Ensure that you are not passing 'true' to the attachToRoot"
                            + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
                }
                holder.mItemViewType = viewType;
                return holder;
            } finally {
                TraceCompat.endSection();
            }
        }

这么一对比是不是就发现问题所在了,27版本中直接将这个问题在createViewHolder的时候就暴露出来了,也就是这个原因导致我在27版本的代码中脑袋想冒烟也没复现出那个bug,所以在调试一些bug的时候尽量让demo的sdk版本和线上sdk版本一致!!这是一个血一样的教训。

总结

到此自己解决的两个bug问题就已经全部阐述完毕了,第一个bug比较好解决因为复现的条件比较简单,只要bug能复现剩下的一切都好商量。第二个bug就有点恶心了,各种无法复现,最后还是通过源码去定位问题,饶了一大圈首先发现是sdk版本问题,然后在线上相同的sdk版本上继续深究问题原因,也正是在解决这两个bug的过程,结合网上一些rv优化技术,让自己对之前封装的adapter又进行了一次重构,可以说在理解rv源码基础上封装起adapter才能更有把握,否则出现一个问题就两眼一抹黑,扔在那边没人处理,在使用一些三方库的时候虽然很爽,但是很多时候还是自己亲手做些东西才能更有收获。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容