全民K歌4.6版本发布后,出现了一个与RecyclerView相关的IllegalArgumentException,作此记录。

一、问题

从下面堆栈中可以看出,RecyclerView此时正在执行布局,尝试获取ViewHolder缓存时发生了crash。所以在分析这个问题前,我们先来简单了解一下RecyclerView的布局流程及缓存策略

二、准备

1、布局流程

通过RecyclerView的dispatchLayout方法,可以知道其布局过程大概分为三个步骤:

dispatchLayoutStep1 : preLayout预布局阶段,主要处理Adapter的更新、决定使用怎样的动画及保存当前子View的边界等信息,这里布局的结果是数据变化前的状态

dispatchLayoutStep2 : 修改mInPreLayout状态为false,然后交由LayoutManager的onLayoutChildren方法处理,它会根据当前子View的ViewHolder状态将其回收至各个缓存队列中,然后寻找锚点并往上下两个方法进行填充,当需要子View时,则请求RecyclerView提供,布局结果为数据变化后的状态。而上述crash正是发生在这一阶段!代码如下所示:


private void dispatchLayoutStep2() {//  some code here// Step 2: Run layoutmState.mInPreLayout = false;mLayout.onLayoutChildren(mRecycler, mState);//  some code here
}

dispatchLayoutStep3 : postLayout,保存当前子View的信息并结合prelayout阶段的结果,触发动画执行,最后清理一些状态。

2、缓存策略

RecyclerView共有以下几种缓存:

mAttachedScrap 未与RecyclerView分离的ViewHolder缓存,用于layout过程中临时存放,可以简单理解为当前屏幕正在显示且数据没有发生变化的内容,可直接复用。添加前会执行ChildHelper的detachViewForParent方法,设置View的parent对象为null,但不会从RecyclerView中remove;另外,还会对mScrapContainer对象进行设置,使得ViewHolder.isScrap为true

mChangedScrap 也未与RecyclerView分离,但数据已发生变化,用于动画执行前的preLayout阶段。同样会执行detachViewForParent及设置mScrapContainer

mCachedViews 当itemView滑出屏幕并从RecyclerView中被remove时,会先添加到这里,其最大容量默认为2

mVewCacheExtension 业务自定义的的缓存逻辑,K歌没有实现

RecycledViewPool 最后一级缓存,添加前需要先从RecyclerView中remove掉,对不同的viewType默认缓存5个ViewHolder,复用时需要重新绑定数据

除了执行动画的需要,在preLayout阶段会优先从 mChangedScrap 缓存中获取ViewHolder外,其它情况都是先按   mAttachedScrap > mCachedViews > mViewCachedExtension > RecycledViewPool  的顺序进行复用,如果没有可用的,就调用Adapter的onCreateViewHolder方法进行创建

三、分析

有了上面对RecyclerView基础的了解,再来看到下crash发生的地方:


ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {//    some code here...//    拿到ViewHolder缓存holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);if (holder != null) {//    对ViewHolder进行校验,但没有通过if (!validateViewHolderForOffsetPosition(holder)) {if (!dryRun) {// 准备添加到到RecyledViewPoolholder.addFlags(ViewHolder.FLAG_INVALID);//    isScrap 说明是从mAttachedScrap获取到的if (holder.isScrap()) {//    crash发生在这里removeDetachedView(holder.itemView, false);holder.unScrap();} else if (holder.wasReturnedFromScrap()) {holder.clearReturnedFromScrapFlag();}recycleViewHolderInternal(holder);}holder = null;} else {fromScrapOrHiddenOrCache = true;}}//    some code here...

逻辑上可以判断,holder是在getScrapOrHiddenOrCachedHolderForPosition方法中获取到的,其内部实现是对mAttachedScrap、mCachedViews 及ChildHelper中因动画需要未与RecyclerView分离的ItemView 进行查找并返回(ChildHelper主要是接管了RecyclerView对子View的处理,解决动画过程中,子View与Adapter数据不同步的问题,有兴趣可自行了解,此处不展开),值得注意的是,这里的缓存查找是以position为索引的,而RecycledViewPool则是通过viewType进行查找的,这很关键。

holder.isScrap的判断则说明了这是 mAttachedScrap 中的缓存,之所以会走到引发了crash的removeDetachedView,是因为对holder的校验没有通过,已不符合可直接复用的特点,于是准备把它从RecyclerView中remove并改放到 RecycledViewPool 中,然后就crash了。

可为什么会校验不通过呢?再来看下校验的源码:


boolean validateViewHolderForOffsetPosition(ViewHolder holder) {// if it is a removed holder, nothing to verify since we cannot ask adapter anymore// if it is not removed, verify the type and id.if (holder.isRemoved()) {if (DEBUG && !mState.isPreLayout()) {throw new IllegalStateException("should not receive a removed view unless it"+ " is pre layout" + exceptionLabel());}return mState.isPreLayout();}if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "+ "adapter position" + holder + exceptionLabel());}if (!mState.isPreLayout()) {// don't check type if it is pre-layout.final int type = mAdapter.getItemViewType(holder.mPosition);if (type != holder.getItemViewType()) {return false;}}if (mAdapter.hasStableIds()) {return holder.getItemId() == mAdapter.getItemId(holder.mPosition);}return true;
}

K歌业务中没有设置stableId,mAdapter.hasStableIds()一定为false;另外,我们的crash是发生在dispatchLayoutStep2的步骤中,调用onLayoutChildren前会将mState.mInPreLayout设置为false。那就只有两种可能了: 要么holder处于FLAG_REMOVED的状态,要么holder与Adapter取到的类型不一致 。此处先作为 线索一 ,后续需要用到。

回归到crash堆栈中,看下有没有其它的有用信息。最后,发现了ViewHolder与FeedListView的两个细节

ViewHolder{394df98d position=2 id=-1, oldPos=-1, pLpos:-1}


//    这里是ViewHolder.toString方法摘要
//    some code here...
if (isScrap()) {sb.append(" scrap ").append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]");
}
//    some code here...
return sb.toString();

引起crash的ViewHolder位于列表中第3位且没有scrap字样,也就是isScrap为false,这就不对了,调用removeDetachedView前先判断了isScrap为true的,为什么进到方法里面就变成false了呢?原来传参给的是itemView,方法内又通过itemView的LayoutParam取到ViewHolder,正常来说,View与ViewHolder间是双向引用、一一对应的关系,这里定是出现了 ViewHolder1指向View,View又指向了另一个ViewHolder2的情况,说明我们的View被多个ViewHolder共用了。

要解释这个问题,就得看下Adapter创建ViewHolder的代码:


public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {if (viewType == REFRESH_HEADER) {//    下拉刷新return new RefreshHeaderContainerViewHolder(mRefreshHeaderContainer);} else if (viewType == HEADER) {//    Header容器return new HeaderContainerViewHolder(mHeaderContainer);} else if (viewType == FOOTER) {//    Footer容器return new FooterContainerViewHolder(mFooterContainer);} else if(viewType == FOOTER_EMPTY){//    列表内容少,希望用空白填满列表return new
FooterEmptyViewHolder(mFooterEmpty);} else if (viewType == LOAD_MORE_FOOTER) {//    上拉加载return new LoadMoreFooterContainerViewHolder(mLoadMoreFooterContainer);} else {//    具体业务模块自行创建return mAdapter.onCreateViewHolder(parent, viewType);}
}

业务使用的RecyclerView是经过了封装的,添加了对 刷新、Header、Footer、空白、加载的支持。其中,mAdapter.onCreateViewHolder都是通过new ViewHolder(new View())的形式创建的,不可能存在View共用的情况;而另外几个,确实有对同一类型的viewType创建多个ViewHolder的可能,但这不是正常逻辑,因为列表中的这些类型有且只有一个,只需创建一次就行。再看堆栈中的position=2,就可以锁定是Footer的异常了,因为除了列表为空时,Footer的position为2,其它几个类型都不会出现为2的情况。检查了业务逻辑上Footer相关的代码并与Header进行了对比,没找到合理的解释,暂且放下并标记为 线索二:RecyclerView创建了两个ViewHolder并指向了同一个Footer

继续看上面提到的另一个细节

FeedListView{27f84f4a IFE….. ……ID 0,231-1080,1767 #7f0d0416 app:id/se}

View.toString摘要:


public String toString() {StringBuilder out = new StringBuilder(128);out.append(getClass().getName());out.append('{');out.append(Integer.toHexString(System.identityHashCode(this)));out.append(' ');switch (mViewFlags&VISIBILITY_MASK) {case VISIBLE: out.append('V'); break;case INVISIBLE: out.append('I'); break;case GONE: out.append('G'); break;default: out.append('.'); break;}
}

虽然叫FeedListView,实际是继承自RecyclerView。从toString方法可以知道,RecyclerView处于INVISIBLE的状态。而K歌动态只有在请求到后台数据前才会是INVISIBLE的状态,只要拿到了数据或协议失败,都会更改为VISIBLE的状态。

这是很奇怪的一个现象,因为从log来看,数据是加载成功的了,用户也有在列表中进行滑动、送礼、收听之类的互动操作,所以,我们的列表一定是可见的。鉴于Crash堆栈也不可能有错,为了解释这种现象,大胆推测:用户手机上出现了两个FeedListView,一个正常显示,一个不可见

相对于上面的这些分析,验证就显得简单多了,我们通过用户启动时,Fragment.OnCreate相关的log来印证了线索三是对的,且不仅是存在了两个列表,还出现了两个FeedSubFragment,但FeedFragment只有一个,得到 线索三:动态页面出现了两个FeedSubFragment及FeedListView,一个正常显示,一个不可见

onCreate:com.tencent.karaoke.module.feed.ui.FeedFragment onCreate:com.tencent.karaoke.module.feed.ui.FeedSubFragment
onCreate:com.tencent.karaoke.module.feed.ui.FeedSubFragment

FeedSubFragment是在FeedFragment的init方法中创建的,init是在onCreateView进行调用的,只会执行一次:

排除了业务逻辑创建两个Fragment的可能,那就只能是系统创建的了。容易联想到应用退后台被系统杀掉重建的情况,FeedFragment与FeedSubFragment都会被系统恢复,而FeedFragment恢复的过程中也会走到onCreateView的生命周期,于是又创建一个FeedSubFragment。

通过打开开发者选项中的“不保留活动”,复现了这样的场景,恢复后产生了2个FeedSubFragment,一个正常显示,另一个从xml加载布局后没有发起数据的请求,于是页面一直是loading的默认状态,而FeedListView为INVISIBLE。

至于原因,可以先看下我们页面的结构:

FeedFragment包含2个部分,一个是Titlebar,包含关注、好友、热门、附近4个Tab选项,另一个是FeedSubFragment用于承载各个Tab的内容,随Tab切换更新数据显示。用户点开K歌时,默认是定位好友页的,但如果发现用户上次离开时不在好友,那这次打开应自动切换到用户离开时的那个页面,这是通过TitleBar内View的performClick来触发切换的,FeedFragment监听到点击后通知FeedSubFragment发起网络请求。

因为FeedFragment只会有一个FeedSubFragment的引用,所以一个能正常显示,另一个一直是loadind的状态,与前面用户crash时的状态是一致的。而对用户来说,这是无感知的,因为正常显示的那个Fragment不是透明的,盖在了另一个的上面。

四、关联

整理下我们已有的线索:

  1. 引起crash的holder处于FLAG_REMOVED的状态或与Adapter取到的类型不一致

  2. RecyclerView创建了两个ViewHolder并指向了同一个Footer

  3. 动态页面出现了两个FeedSubFragment及FeedListView,一个正常显示,一个不可见

对于线索1,我们先假设是第一种情况,通过追踪FLAG_REMOVED设置的路径,发现只有当业务调用了Adapter的notifyXXXRemoved方法时,才会为ViewHolder添加FLAG_REMOVED标记。而线索二中的Footer实际上是一个容器,业务调用addFooterView添加进来的布局都会填入容器中,不管用户如何操作,对RecyclerView来说,Footer始终是有且只有一个,不存在删除Footer的情况。于是线索一纠正为: mAttachedScrap 中取到的ViewHolder类型与Adapter取到的不一致。

mAttachedScrap 中的ViewHolder是通过对比LayoutPosition查找到的,而Adapter.getItemType的结果则是分析数据集而来,两者的不一致说明了RecyclerView的状态与数据集产生了不同步的情况,往往出现在Adapter中的列表数据发生了变化而又没有调用notityXXX方法通知到RecyclerView的情况下。

crash所在的列表并没有请求后台数据却产生了数据的变化,能产生这一现象的只有用户发布作品后,由客户端自己构造的假数据了。

因作品发布与K歌业务逻辑关联较大,参考意义不大,这里只做简要的文字说明:

用户发布作品后,会生成一条发布数据在动态中显示,这条数据是存在于单例中的,两个FeedSubFragment都能取到,发布完成并刷新列表才会把它从单例中清除。另外,用户在K歌内的一些互动操作会触发广播,比如在作品详情页评论了作品,那动态中这个作品的feed评论计数会实时更新,不需要等待列表的刷新操作,广播也都是有注册的。

作品刚发布时,不可见的那个页面对此无感知,会出现RecyclerView是Refresh、Header、Footer、Empty、Load五个item的状态,而Adapter的数据集中在Header与Footer间多了一条假feed,虽然没有调用notifyXXX,但当有互动操作或跳其它Activity返回等其它原因触发layout时,也不会引起crash,如下:

①② 通过position可以从 mAttachedScrap 正确获取到原来的ViewHolder并直接复用

③ 通过position取到了Footer的ViewHolder,发现类型不同,把它从布局中remove并添加到缓存池 RecycledViewPool ,最后新创建一个假Feed的ViewHolder

④ 取到了Empty的ViewHolder,同样回收至RecycledViewPool,但因为上一步有把Footer的ViewHolder添加到了RecycledViewPool,处理完Empty后,会尝试从RecycledViewPool查找,而这里是通过viewType来查找的,所以可以找到上一步添加进来的ViewHolder,从而复用

⑤⑥ 同④

当假feed已经被layout出来,数据被删除却没有notify的情况下执行layout又会怎样呢?

①② 可直接复用

③ 取到了假feed的ViewHolder,回收至RecycledViewPool,然后重新创建了一个Footer的ViewHolder,这就导致了两个ViewHolder指向同一个View的出现,一个新创建的添加到RecyclerView中显示,并清除FLAG_TMP_DETACHED标记,另一个仍然存在于Scrap缓存中未被使用

④ 取到了Scrap缓存中Footer的ViewHolder,尝试回收至RecycledViewPool,却发现Footer已经不是FLAG_TMP_DETACHED的状态,因为上一步已经把它添加到RecyclerView中,清除了这一标记,于是抛出文章开头的IllegalArgumentException异常

可能有人会感兴趣增删数据并调用了notifyXXXRemoved的正常情况下,RecyclerView是如何在preLayout及postLayout阶段都能通过position获取到正确的ViewHolder的,可以自行了解下ViewHolder的mPreLayoutPosition跟mPosition的作用,这里不细说了

五、总结

至此,原因也就比较清晰了:用户使用K歌停留在动态非好友页,退后台被系统杀掉重启时,没有考虑到Fragment恢复的情况,导致在正常的Fragment下多生成了一个不可见的Fragment,之后发布了作品并对其执行了会引起数据变化的互动操作,使其layout到布局中,刷新列表后不可见的RecyclerView列表状态与Adapter数据不同步,跳转到其它Activity再返回时,触发了RecyclerView的重新布局,检测到了状态不对并抛出了异常。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/31557897/viewspace-2219668/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/31557897/viewspace-2219668/

记一次全民K歌的crash定位过程相关推荐

  1. xm在线转换成mp3_全民K歌月花费25万,95后表示在线K歌可以换一种玩法

    作者:田巧云,新零售商业评论高级编辑 "在线K歌有望进入3.0时代,开放出更多赛道." "来左边儿,跟我一起画个龙,在你右边儿,画一道彩虹:来左边儿,跟我一起画彩虹,在你 ...

  2. 见招拆招,敏捷实战:揭秘全民K歌背后的音乐黑科技

    随着数字音乐的不断发展,线上 K 歌越来越流行.移动 K 歌从用户深层情感需求与消费场景出发,满足了新生代用户表达情绪.抒发个性的需求.而全民 K 歌作为 K 歌平台中的"标杆", ...

  3. 全民K歌内存篇2——虚拟内存浅析

    <全民K歌内存篇1--线上监控与综合治理> <全民K歌内存篇2--虚拟内存浅析> <全民K歌内存篇3--native内存分析与监控> 一.简介 在多任务操作系统中, ...

  4. 全民K歌直播推荐算法实践

    导读:直播是社交娱乐app的综合性变现工具,如何培养用户的心智,高效的建立用户和主播的多种连接 ( 点击.观看.关注.常看.常打赏 ) 是直播生态的重要问题之一.为了解决这个问题,各大平台所使用的方法 ...

  5. 全民K歌内容挖掘与召回

    分享嘉宾:timmyqiu 腾讯音乐 应用研究 编辑整理:郭真继 出品平台:DataFunTalk 导读:推荐系统一般分为两部分,召回阶段和排序阶段.召回阶段是从全量数据中挑选出用户可能感兴趣的一部分 ...

  6. 全民K歌推荐后台架构

    分享嘉宾:davidwwang 腾讯音乐 | 基础开发组副组长 编辑整理:梁尔舒 出品平台:DataFunTalk 导读:首先介绍一下我们业务背景,腾讯音乐集团,于2018年是从腾讯拆分独立上市,目前 ...

  7. 腾讯音乐:全民K歌推荐系统架构及粗排设计

    编辑整理:张振.于洋 导读:腾讯音乐娱乐集团 ( TME ) 目前有四大移动音乐产品:QQ音乐.酷狗音乐.酷我音乐和全民K歌,总月活超8亿.其中,全民K歌与其他三款产品有明显的差异,具体表现如下:以唱 ...

  8. 速领!抗疫大礼包(含QQ音乐、全民K歌、网易云音乐等等)

    腾讯系和网易系推出了抗疫礼包 都是免费领取,数量有限,大家抓紧哈! 腾讯系包括QQ音乐周卡.全民K歌周卡.腾讯视频周卡 网易系包括网易云音乐月卡.网易严选季卡.网易邮箱月卡.有道云笔记月卡 领取路径: ...

  9. 复盘 | 听全民K歌体验设计师聊聊歌房项目完整设计历程

    PMCAFF(www.pmcaff.com):互联网产品社区,是百度,腾讯,阿里等产品经理的学习交流平台.定期出品深度产品观察,互联产品研究首选. 外包大师(www.waibaodashi.com): ...

最新文章

  1. PHP开发之递归算法的三种实现方法
  2. 数据中心基础设施故障处理流程
  3. QList 列表指针的操作 释放
  4. vue 根据组件地址动态加载异步组件
  5. java.util.Random 类的 nextInt(int num )
  6. 自己动手写DB数据库框架(增)
  7. Spring Boot与Spring Cloud是什么关系?
  8. java提示单个cass怎么办,求助解决hibernate报错,java.lang.casscastexception
  9. python flask将读取的图片返回给web前端
  10. Java实现的各种排序算法
  11. CentOS安装SVN
  12. postSQL hash分表
  13. Mysql数据库手册
  14. 开平方的快速算法(C程序)
  15. 淘宝用户行为数据分析
  16. 解决运行zebra时“Multiple command installs to node 4 of command”的问题
  17. 天使投资(AI),风险投资(VC),私募基金(PE);A轮,B轮,C轮,D轮,E轮,F轮融资之间的区别...
  18. 后台推送消息给app_小米加入统一推送联盟!避免多种APP后台运行,国产手机春天来了...
  19. 微信小程序vue+nodejs+uniapp家装 装修装饰公司管理系统
  20. 寒武纪科技 服务器芯片,国内首款人工智能服务器诞生!搭载“寒武纪”芯片,走在世界前列...

热门文章

  1. 使用Python批量修改文件名后缀
  2. 美图秀秀美化图片之【智能优化】模块界面设计
  3. Java服务端支付功能模块--(二)微信支付
  4. html自动识别密码,HTML登录表单:提供用户名,自动填写密码
  5. cytoscape画饼图
  6. 一个软件工程师的反省
  7. Sqoop简单介绍及使用
  8. C#快速入门(vs安装和环境配置)
  9. 分析游戏中的金钱交易:Multi-view Attention Networks
  10. QT QTranslator 中英文翻译linguist语言家 翻译过程