记一个疑难bug的解决过程

前言

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

需求背景

  • 先效果看图

    效果图A
    记一个疑难bug的解决过程


效果图B
记一个疑难bug的解决过程

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

Bug描述

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

问题分析

  • Bug出现的场景比较固定,就是打开特定一个聚合页的时候,主要切入点这篇聚合页内容与其他页面有何不同
  • 客户端代码为什么会因为特定数据出现此Bug

问题解决过程

  1. 尝试修改服务器数据定位问题 (失败)
    • 通过查看出现bug的[聚合页]与[其他页面]数据不同之处在于[板块数量]比其他页面多一倍以上,所以尝试修改服务器返回的数据(通过Charles修改),将返回的[板块数量]修改为和[其他页面]一样甚至更少,但是bug依旧。
  2. 排查项目代码调用错误 (失败)
    • 将[板块标题导航]View定位到标题下方的代码为
      listView.setSelectionFromTop(itemIndex,offset);
    • 通过Debug,和其他页面比对itemIndex和offset数值都没有错误
    • 怀疑是这行代码调用的时候异常了没有捕获到,然后给该语句包裹了一层try-catch,实际验证没有验证,bug依旧。
  3. Google描述问题搜索答案 (失败)
    • 此时怀疑是调用setSelectionFromTop方法没有生效,所以就Google了listview setselectionfromtop not working,结果大都是说需要通过 listView.post(new Runnable())setSelectionFromTop方法在 Runnable中执行。但是我项目代码本来就是这么调用的。
    • 附图
      记一个疑难bug的解决过程
    • 然后我又搜到了这篇文章,方法和上面不同,很期待的尝试了一下,依然没有解决我的问题。
    • 然后我就迷茫了,测试问我这个bug啥时候能改完,我也给不出来时间,因为我连兼容的方案都没想到。
  4. 和同事讨论问题开拓思路 (有效)
    • 遇到疑难问题,一筹莫展的时候,我喜欢找同事(一定要找靠谱儿有经验的)描述问题,讨论问题可能出现的原因来开拓思路,有时候自己描述问题的时候就有了新思路,有时候俩人讨论着就有了新方向。(此法为独门秘籍,拿去不谢。)
    • 跟我们组最靠谱的玉刚讨论后,他说了一个新的方向,应该是某个地方异常了,我详细对比查看了控制台的errorLog,没有找到可疑的信息。(最后找到的问题原因确实是在ListView中异常了,只是log没有打印出来,且没有crash)
  5. 查看系统源代码 (成功)
    • 上面所有常规方法都没解决问题,现在只能尝试查看一下为什么调用setSelectionFromTop不生效了,查看了一下setSelectionFromTop的方法实现
      记一个疑难bug的解决过程
    • 最后一行requestLayout()吸引了我的注意,应该setSelectionFromTop方法通过调用requestLayout()方法通知ListView进行了重新布局绘制。下面我们再看一下requestLayout()方法里面做了什么事情
      记一个疑难bug的解决过程
    • 本来requestLayout()View的方法,但是ListView重写了此方法,增加了两个变量来控制super.requestLayout(),通过Debug对比正常的[聚合详情页]和有bug的[聚合详情页]的方法调用,发现正常的调用时mBlockLayoutRequestsmInLayout都是false,这样super.requestLayout()可以被顺利调用;但是有bug页面调用此方法时mBlockLayoutRequests为false没有问题,mInLayout却为true!!!这就导致了super.requestLayout()语句没有被调用,造成ListView没有重新排版重回。(上面提到的当手指稍微触摸滑一下页面,[板块标题导航]View马上跳动到标题下方显示正常,这是因为ListView被触摸滑动时又重新排版绘制,按照正确的状态展示出来。)
    • 现在的问题转化为mInLayout变量为什么是true,如果值为false此bug就不存在。经过查找ListView的继承结构,mInLayout是在其父类AdapterView声明的成员变量。
      记一个疑难bug的解决过程
      作用如注释所述是指示当前View是否正在被layout
    • 通过看mInLayout变量的声明,发现访问修饰符是default,没有办法通过直接修改mInLayout来验证是否将其修改为false,问题就能暂时解决。下面来查找mInLayout是在哪里赋值的。
      记一个疑难bug的解决过程
      经过查找,mInLayout的赋值只有一处,就是上图AbsListViewonLayout中,看了此方法你可能会很疑惑,mInLayout没有赋值时默认值是false,虽然在onLayout方法中被赋值为true后,下面紧接着就赋值false,这两行语句一定是成对执行的,因为没有if条件的分支。
      那么真相只有一个:在执行mInLayout = true语句后,在执行mInLayout = false前,发生异常了!mInLayout = false语句没有被执行。(这就找到了上面玉刚猜测的发生异常地方)
    • 这一步就是验证对发生异常的猜测,通过对mInLayout赋值的两行语句之间的语句进行粗略分析,layoutChildren()这行代码可疑度比较高,因为ListViewlayoutChildren方法中执行了大量排版逻辑。我就对我工程中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
    记一个疑难bug的解决过程
    很熟悉的ConcurrentModificationException,产生此异常的原因一般是在使用Iterator(增强for循环也是用它实现的)遍历集合时,集合长度发生了改变。
  • 为了分析异常产生原因,这里再补充一个小知识,如果ListView被设置了onScrollListener,那么ListView在执行onLayout方法时,其onScrollListeneronScroll()方法会被执行(见下图方法栈)
    记一个疑难bug的解决过程
    所以当ListView初始化化的时候就设置了onScrollListener,那么其显示在屏幕上时,onScroll()方法就被调用执行了。
  • 下面就用一个图展示一下与此Bug相关的类
    记一个疑难bug的解决过程
    整个容器是一个自定义的ListViewPageSlidingTabLayout本来是和ViewPager绑定使用的,为了这个需求,解耦了PageSlidingTabLayout和ViewPager的绑定,使其可以与其他容器结合),其中一个Item是PageSlidingTabLayout(就是效果图上红框标记的板块标题导航View),这个Item还要在上滑到标题下方时悬停展示,不再随ListView的上滑而滑出屏幕(悬停View具体实现是创建了一个一模一样的ItemView盖在了ListView上)。OnScrollerListenerDelegateListContainerOnScrollListenerPinnedViewManager三个关键类的作用和与自定义ListView的关系看图上说明应该就清楚了。
  • 这里再贴一张异常发生现场所在类的代码,方便大家理解问题。
    记一个疑难bug的解决过程
  • 重点来了,下面说一下此异常产生的场景,仔细听:ListView执行onLayout()方法时,会触发onScrollListeneronScroll()方法,在onScroll()方法中会触发PinnedViewManager创建吸顶悬浮的PageSlidingTabLayout,PageSlidingTabLayout在初始化时会给ListView设置一个onScrollListener,实际上内部会调用OnScrollerListenerDelegateaddOnScrollerListener方法。此时onScrollListenerList集合的size就增加了,然后下一次onScroll()被调用时,执行for (OnScrollListener listener : onScrollListenerList)就会发生ConcurrentModificationException异常。
  • 至此,Bug的产生原因及解决方法都描述完毕了。从早上来公司开始着手写文章,一遍描述问题、一遍截图插图到现在,再过一小会儿就该去食堂吃晚饭了。

后记

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

最后

  • 细心的小伙伴可能发现我遗漏了一个问题,为什么只有这一篇文章会有Bug,按说不是应该只要有[最新更新]板块的[聚合详情页]都有这个问题吗?是的!有[最新更新]板块的[聚合详情页]都发生异常了!但是只有这一篇[聚合详情页]出现了Bug。因为其他[聚合详情页]发生异常后,接着ListViewonLayout方法被其他逻辑触发调用了,把问题给掩盖了!!!而这一篇[聚合详情页]却没这么幸运!不要再问我为什么了,我还在查,并且不能准确给出找到原因的时间……(心塞╥﹏╥)