您的位置:首页 > 运维架构

记一个疑难bug的解决过程

2018-01-23 17:47 726 查看

前言

测试给提了一个bug,但是具体解决的时候突然发现找不到问题发生的原因,一下子没有了头绪,后来一步步的按猜测的方向把问题解决了,并找到了问题原因(注意是先解决的问题,后来才找到问题的原因)。在此记录一下,供自己和同样遇到疑难问题的同学参考。此文章主要用于开拓思路,其他不同的问题需要具体分析。

需求背景

先效果看图

效果图A



效果图B



需求描述:

[效果图A]为聚合详情页,红色矩形标记的地方为[板块标题导航]View,默认情况正常展示即可;如果[板块标题导航]View的第一个tab是”最新更新”,则打开聚合详情页时,需要直接将此[板块标题导航]View滚动到标题下方的位置,方便用户直接查看最新更新的板块内容。

Bug描述

当打开某一篇聚合详情页时,[板块标题导航]View的第一个Tab是[最新更新],但是[板块标题导航]View没有自动滚动到标题下方。但是稍微触摸滑动一下页面,[板块标题导航]View马上跳到标题下方,和预期展示的位置一致。

打开其他聚合详情页没有这种情况,一切展示和触摸交互都和预期的一致。

问题分析

Bug出现的场景比较固定,就是打开特定一个聚合页的时候,主要切入点这篇聚合页内容与其他页面有何不同

客户端代码为什么会因为特定数据出现此Bug

问题解决过程

尝试修改服务器数据定位问题 (失败)

通过查看出现bug的[聚合页]与[其他页面]数据不同之处在于[板块数量]比其他页面多一倍以上,所以尝试修改服务器返回的数据(通过Charles修改),将返回的[板块数量]修改为和[其他页面]一样甚至更少,但是bug依旧。

排查项目代码调用错误 (失败)

将[板块标题导航]View定位到标题下方的代码为

listView.setSelectionFromTop(itemIndex,offset);


通过Debug,和其他页面比对itemIndex和offset数值都没有错误

怀疑是这行代码调用的时候异常了没有捕获到,然后给该语句包裹了一层try-catch,实际验证没有验证,bug依旧。

Google描述问题搜索答案 (失败)

此时怀疑是调用setSelectionFromTop方法没有生效,所以就Google了listview setselectionfromtop not working,结果大都是说需要通过 listView.post(new Runnable()) 将setSelectionFromTop方法在 Runnable中执行。但是我项目代码本来就是这么调用的。

附图



然后我又搜到了这篇文章,方法和上面不同,很期待的尝试了一下,依然没有解决我的问题。

然后我就迷茫了,测试问我这个bug啥时候能改完,我也给不出来时间,因为我连兼容的方案都没想到。

和同事讨论问题开拓思路 (有效)

遇到疑难问题,一筹莫展的时候,我喜欢找同事(一定要找靠谱儿有经验的)描述问题,讨论问题可能出现的原因来开拓思路,有时候自己描述问题的时候就有了新思路,有时候俩人讨论着就有了新方向。(此法为独门秘籍,拿去不谢。)

跟我们组最靠谱的玉刚讨论后,他说了一个新的方向,应该是某个地方异常了,我详细对比查看了控制台的errorLog,没有找到可疑的信息。(最后找到的问题原因确实是在ListView中异常了,只是log没有打印出来,且没有crash)

查看系统源代码 (成功)

上面所有常规方法都没解决问题,现在只能尝试查看一下为什么调用setSelectionFromTop不生效了,查看了一下setSelectionFromTop的方法实现



最后一行requestLayout()吸引了我的注意,应该setSelectionFromTop方法通过调用requestLayout()方法通知ListView进行了重新布局绘制。下面我们再看一下requestLayout()方法里面做了什么事情



本来requestLayout()是View的方法,但是ListView重写了此方法,增加了两个变量来控制super.requestLayout(),通过Debug对比正常的[聚合详情页]和有bug的[聚合详情页]的方法调用,发现正常的调用时mBlockLayoutRequests和mInLayout都是false,这样super.requestLayout()可以被顺利调用;但是有bug页面调用此方法时mBlockLayoutRequests为false没有问题,mInLayout却为true!!!这就导致了super.requestLayout()语句没有被调用,造成ListView没有重新排版重回。(上面提到的当手指稍微触摸滑一下页面,[板块标题导航]View马上跳动到标题下方显示正常,这是因为ListView被触摸滑动时又重新排版绘制,按照正确的状态展示出来。)

现在的问题转化为mInLayout变量为什么是true,如果值为false此bug就不存在。经过查找ListView的继承结构,mInLayout是在其父类AdapterView声明的成员变量。



作用如注释所述是指示当前View是否正在被layout

通过看mInLayout变量的声明,发现访问修饰符是default,没有办法通过直接修改mInLayout来验证是否将其修改为false,问题就能暂时解决。下面来查找mInLayout是在哪里赋值的。



经过查找,mInLayout的赋值只有一处,就是上图AbsListView的onLayout中,看了此方法你可能会很疑惑,mInLayout没有赋值时默认值是false,虽然在onLayout方法中被赋值为true后,下面紧接着就赋值false,这两行语句一定是成对执行的,因为没有if条件的分支。

那么真相只有一个:在执行
mInLayout = true
语句后,在执行
mInLayout = false
前,发生异常了!
mInLayout = false
语句没有被执行。(这就找到了上面玉刚猜测的发生异常地方)

这一步就是验证对发生异常的猜测,通过对mInLayout赋值的两行语句之间的语句进行粗略分析,
layoutChildren()
这行代码可疑度比较高,因为ListView在layoutChildren方法中执行了大量排版逻辑。我就对我工程中ListView的子类重写了layoutChildren()方法,方法体内使用try-catch包裹
super.layoutChildren()


奇迹出现了:此方法确实执行中发生了异常,但是被catch住了,页面正常显示,所有触摸交互和预期一致。bug暂时解决了。

小建议:(Android客户端写代码catch异常时,没有特殊需求,都使用
Exception
而不要是要其子类,因为当你写try-catch时就代表此处可能会出现异常,如果此处你只catch了子类ExceptionA,但是发生了ExceptionB异常,那么你没有处理很可能造成客户端Crash,这是不可接受的!如果使用了Exception,即使对各种异常处理的不细致,至少不会造成客户端Crash。如果有些异常不应该这里处理,而是抛给上层,也应该是在catch代码块中判断异常类型,如果匹配则抛给上层,其余类型都要统一处理。都是为了一件事:异常可控!避免客户端crash!而不是crash后再去增加catch的异常类型。)

异常产生原因分析

至此我们已经把bug解决完毕,此时距离开始解决bug已经过去3个小时。现在我们来看看产生这个异常的原因,先上图看异常log



很熟悉的ConcurrentModificationException,产生此异常的原因一般是在使用Iterator(增强for循环也是用它实现的)遍历集合时,集合长度发生了改变。

为了分析异常产生原因,这里再补充一个小知识,如果ListView被设置了onScrollListener,那么ListView在执行onLayout方法时,其onScrollListener的onScroll()方法会被执行(见下图方法栈)



所以当ListView初始化化的时候就设置了onScrollListener,那么其显示在屏幕上时,onScroll()方法就被调用执行了。

下面就用一个图展示一下与此Bug相关的类



整个容器是一个自定义的ListView(PageSlidingTabLayout本来是和ViewPager绑定使用的,为了这个需求,解耦了PageSlidingTabLayout和ViewPager的绑定,使其可以与其他容器结合),其中一个Item是PageSlidingTabLayout(就是效果图上红框标记的板块标题导航View),这个Item还要在上滑到标题下方时悬停展示,不再随ListView的上滑而滑出屏幕(悬停View具体实现是创建了一个一模一样的ItemView盖在了ListView上)。OnScrollerListenerDelegate、ListContainerOnScrollListener和PinnedViewManager三个关键类的作用和与自定义ListView的关系看图上说明应该就清楚了。

这里再贴一张异常发生现场所在类的代码,方便大家理解问题。



重点来了,下面说一下此异常产生的场景,仔细听:当ListView执行
onLayout()
方法时,会触发onScrollListener的
onScroll()
方法,在onScroll()方法中会触发PinnedViewManager创建吸顶悬浮的PageSlidingTabLayout,PageSlidingTabLayout在初始化时会给ListView设置一个onScrollListener,实际上内部会调用OnScrollerListenerDelegate的
addOnScrollerListener
方法。此时onScrollListenerList集合的size就增加了,然后下一次
onScroll()
被调用时,执行
for (OnScrollListener listener : onScrollListenerList)
就会发生
ConcurrentModificationException
异常。

至此,Bug的产生原因及解决方法都描述完毕了。从早上来公司开始着手写文章,一遍描述问题、一遍截图插图到现在,再过一小会儿就该去食堂吃晚饭了。

后记

昨天在问题解决后,和玉刚总结问题原因的时候,讨论了一下是否需要写篇文章记录一下,都不约而同的因为没有技术含量、知识点零碎而选择算了。睡了一觉,我找到了串起这些零碎知识点的那根绳子,就是本文的标题。然后就有了这篇文章。

最后

细心的小伙伴可能发现我遗漏了一个问题,为什么只有这一篇文章会有Bug,按说不是应该只要有[最新更新]板块的[聚合详情页]都有这个问题吗?是的!有[最新更新]板块的[聚合详情页]都发生异常了!但是只有这一篇[聚合详情页]出现了Bug。因为其他[聚合详情页]发生异常后,接着ListView的
onLayout
方法被其他逻辑触发调用了,把问题给掩盖了!!!而这一篇[聚合详情页]却没这么幸运!不要再问我为什么了,我还在查,并且不能准确给出找到原因的时间……(心塞╥﹏╥)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息