关于解Bug的总结

时间:2021-09-11 14:55:52
1. 与其他应用交互的Bug
   背景:
    一个手机音乐播放器,多媒体通常存放在手机外部存储卡上(SDcard上)。所以只有当SD卡Mount到手机上时,才可以播放媒体。音乐播放器就会监听SD卡状态,当SD卡从手上卸载或弹出时,播放器会保存现场并停止播放;当SD卡重新Mount回手机时,再恢复现场和继续播放。也即当播放器收到“SD卡Eject”消息时,停止,当收到“SD卡Mount"时继续播放。播放器可以在后台播放媒体。
    一个文件管理器是另外一个应用程序,可以用来编辑(如删除)SD卡上的文件。文件发生变化后,这个第三方应用会发出”SD卡Mount“这样的消息,以便告知其他应用和系统,文件发生了变化。
   问题:
    当播放器在后台播放媒体的时候,起动文件管理器,然后随便删除一个文件(也可以是正在播放的媒体文件),这时候,播放器停止播放媒体,并且当打开播放器时,发现播放器是处于播放状态的,但是却没有声音。
   分析: 播放器不会没有理由的去停止正在播放的媒体。查找发现,播放器停止播放的原因有几个:用户请求停止,切换媒体时,SD卡Eject,SD卡Mount时。经过调试和跟踪日志信息,发现前面三种情况都未发生。那么问题的原因就是SD卡Mount时,进行了恢复现场的操作,导致媒体停止了。那为什么会进行恢复现场的操作呢,因为SD卡并未被Eject和Mount。进一步调试和跟踪日志发现,当文件管理器删除文件后会发出“SD卡Mount”的消息,以便告知其他应用文件发生了变化。
   解决方案:
   当分析出了问题出现的原因,解决起来就容易了,只需要让播放器在做恢复现场的动作前多一些检测来保证,先前SD卡有Eject过,现在确实是SD卡Mount回来。否则,应该忽略这个消息。
   教训:
   这应该是软件缺陷引起的Bug,音乐播放器本应该做这样的条件检查。音乐播放器本身的策略是没有错的,当没有其他应用程序发出这样的消息时,它是完全可以正常工作的。这是一个典型的与其他应用或模块交互时产生的Bug。
   对于这类Bug,最重要的线索就是,当与其他应用一起操作时会出现问题。这个Bug比较简单,因为很明确是与文件管理器有关。但在有些关系复杂的系统中,当出现了问题,很难搞清是表现问题的模块出了问题,还是其他模块出了问题。这就需要调试跟踪日志,以缩小范围。在调试的时候要一个一个模块的试验和排除,以缩小范围和进一点的深入调查。
2. 原因与问题离的很远的Bug
   背景:
   一个模块专门负责处理SD卡上的多媒体文件,解析这些文件,获取它们的元信息,并存入到媒体数据库中。它是由Java和C++以JNI方式组合来实现的。Java进行上层流程控制和写入数据库操作;C++负责解析文件获取文件元信息。它们之间通过JNI来通信。
   问题:
   当处理一个特殊的文件时候Java的JVM(虚拟机)会异常退出(JVM abort),并打印出一条错误信息:"JNI Warning: illegal start byte 0xb1".
   分析:
   这是一个很严重的错误,因为它会导致JVM崩溃,进程也会被内核杀掉。唯一的线索就是JVM崩溃时打印出的一条信息"JNI Warning: illegal start byte 0xb1".通过搜索这条消息(幸亏有所有的源代码),发现它是由JVM内部的CheckJNI.c文件打印出来的。这个文件是JVM中比较重要的一个文件,它负责对JNI的所有参数进行合法性检查。特别是字串。因为Java中用的是Modified UTF-8编码格式,所以CheckJNI.c文件就会对所传进来的字串进行编码检查,如果字串不是一个合法的Modified UTF-8格式,就会发出警告并停止JVM。这里找到了问题所在,是因为JVM检查到了不合法的字串才导致JVM崩溃的。那么不合法的字串是从哪里来的呢?又是出现在哪个模块呢?因为系统中有无数地方在用到JNI,所以不能盲目的去查找。又由于这是在媒体扫描器扫描某个媒体时候出现的,因此主要目标锁定在媒体扫描器的JNI部分。媒体扫描器的C++部分解析多媒体文件,然后把所得到的结果(通常为字串)通过JNI传回给Java层。问题就很有可能出在这里,因为多媒体文件的信息有多种编码格式,其中元信息就有可能是非法的Modified UTF-8编码格式。经过调试跟踪,发现,确实是这里出现了问题。
   解决方案:
   这个问题的解决方案仍不是很好,一种方法是对其进行编码转换,但这要知道字串原来的编码方式。另一咱简单的方法就是在传给JNI之前做一次编码合法性检查,以过滤到不合法的字串。最后,采用了后一方式解决了这个问题。
   教训:
   首先,系统崩溃时所给出的信息和出错时给出的信息是第一重要的线索,虽然它们可能不是问题真正的原因,但是从它们出发就可以追踪到原因。其次,错误信息,是由代码打印出来的,所以当你不知道是哪个模块出错时,可以用错误信息对源码进行局搜索,就可以定位出模块和源码位置。最后,如果是一系列的原因导致了一问题,那么可以在最源头解,也可以其中的某一个环节来解,只要不会导致程序崩溃即可。
3. 空指针异常NullPointerException
   背景:
   在C/C++/Java中空指针异常是比较常见的一类导致程序崩溃的原因。在C/C++/Java中,如果使用的指针或对象没有正确的初始化,则很容易发生NullPointerException。
   问题:
   当发生NullPointerException的时候,程序通常会因异常而崩溃的。但通常都会打印出运行时的堆栈信息。
   分析:
   从程序的堆栈信息,会很容易的看到发生问题的代码位置,这样就可以找到直接原因。但是这找到问题的一小部分,具体是什么导致对象为空,这就不是那么容易调查出原因了。
   解决方案:
   对于这类问题,一开始能想到的办法就是加上对空指针的检测,如果指针或对象为空的话就不对其进行操作。但这是行不通的,这也不是正确的解决方法,最直接的问题就是,当指针为空的时候应该去做什么。如果在一个类中,在其他地方引用的时候都做了空指针检测,而这个地方没有做,那么可以仿照其他地方那样,加上空指针检测。但假如不是这样的情况,就要好好的调查一下指针为什么会是空,而非处理空指针。但这通常都是比较困难的,因为要去追踪对象是从哪里来的,又在哪里被修改和引用,是在哪里初始化的。只有找到了真正让对象为空的原因,才能算是比较完整的解决了问题。但如果一个比较复杂的系统,引用的地方很多,且假如又涉及到多线程时,则追踪起来会更加的困难。
   教训:
   对于空指针问题,不能简单的加上一个条件。要进一步的深入去调查是什么导致了指针为空。除非,你有充足的理由去加上条件。
4. 无解的问题
   案例一:
   背景:
   有些问题是极及诡异的,而且出现的机率非常的小,但它们还是会出现,但是找不到合适的解决方案。
   问题:
   在一个GUI系统中,在一个比较基础的类里面报出了一个NullPointerException。由于这个类会被很所有涉及GUI的应用程序所使用。
   分析:
   根据程序退出时打印出来的堆栈信息,找到了发生异常的代码位置。令人感到惊讶的是,这一行是绝不可能发生空指针的,因为它用的都是基本数据类型。上下几十行之内也是绝不可能发生NullPointer的。
   解决方案:
   这个问题,始终没有找到解决方案。
   案例二:
   背景:
   对一个系统做大规模的随机压力测试。一个对象的类Message是一个final的类,且其重载了toString()方法,它会按如下格式打印信息:"{ what=XXX when=XXX XXXXXX }"
   问题:
   在一次测试过程中,系统核心进程因异常退出,导致系统自动重启。
   分析:
   问题的原因是在核心进程中发生了一个RuntimeException,并有一条消息:"[c0x44bc: This message is already in use."。从堆栈找到退出的位置的代码,发现它是程序检测到不合理的操作然后抛出的一个RuntimeException:代码如下:
         // ...
     if (msg.when != 0) {
             throw new RuntimeException(msg + " This message is already in use.");
         }
         // ...
   这里的msg是一个Message的对象。
   通常来讲+会调用对象的toString()方法,而toString()方法的输出又有特定的格式,所以,这里就发生了让人极其迷惑的事情。因为最终打印出来的消息跟对象的toString()有很大的差别。从日志信息来看,当前对象应该是一个char数组,而并非一个Message对象,但是在相关的上下文都无法找到这样一个char数组。
   解决方案:
   这个问题,怀疑是对象的内存已被破坏,其内的数据已不再是对象本身。这也是一个没有解决方案的问题。
   教训:
   这类问题是真正的难题。需要对语言和系统达到精通的人物才可能解的了。
5. 难重现的必现问题
   背景:
   一个Gallery应用程序能够以网格形式显示很多张图片。当图片较多时,就会在左边出现滑块用来滚动屏幕。正常来讲,当打开应用时,这个滑块应处在最上面的位置。
   问题:
   有人报告说初始打开应用时,滑块不是在最上面,而是在中部,或其他地方。而且声称这是必现的问题。
   分析:
   但当调试的时候却怎么也无法重现这个问题。虽然这并不是一个严重的问题,但感觉这样的行为是很诡异的。在调试的过程中,发现,测试人员的应用与我这边应用显示图片的顺序似乎有些不同,是正好相反的顺序。这是有一个可配置的选项在控制的,所选项设置为正序或倒序。发现测试人员当时用的倒序,而通常大多数情况下,用的都是正序(可能是因为没有人去设置这个东西,它默认的是正序)。这可能是问题产生的原因。果然,当把顺序设置为倒序的时候,滑块的位置就不会置顶了。
   解决方案:
   找到了问题的重现规律,对于这种问题就好办了。发现,当设置为倒序的时候,代码的本意是想把滑块放在最后,但是计算时有些错误,误把一个屏幕窗口的高度当成整个文档长度了。所以当,整个文档长度超过一个屏幕时,滑块的位置就不正确了。由于,把滑块放在最上端的更符合一般情况,所以就无论排序顺序,把滑块置顶。
   教训:
   事实上,很少问题是真正的小概率事件(Seldom),只有当涉及多个线程的时候才会有真正Seldom的问题,因为无法确定线程的执行顺序。对于其他的问题,应该是都还没有找到重现的规律。相信了这一个事实以后,就要不断的去试验和假设以重现问题。在试验的时候,也要注意细节,因为很多细节都可能是一条重要线索。最重要的是要相信问题是存在的,是可以重现的,更是可以解决的。
   有一些方法技巧可以用来重现看似比较难重现的问题:
      1. 仔细询问或查看问题出现时的相关操作和日志,以确定是否漏掉了一些必要的前提条件和操作
      2. 如果涉及与其他应用或模块交互,则要了解每个应用和模块的特性,然后做适当的假设,再去做试验。
      3. 猜测可能导致问题的原因,然后去创造这些条件,看在有这些条件的情况下,是否可以重现问题。比如,如果猜测是时间长短或文件大小导致了问题,那么就可以调大时间,用大文件来测试。如果猜测是空指针的问题,那么就可以故意创造出一个空指针等等。
      4. 要相信问题是存在的,也要相信问题是可以重现的,更要相信这个问题是可以修复的。
      5. 要有耐心,不断的思考,假设,然后去验证,一次改变一个条件,一点一点的分析与验证,最终是能够重现并修改问题的。
6. 真正难重现的问题----线程问题
   背景:一个手机音乐播放器支持多个播放列表,当删除播放列表的时候,如果其中有正在播放的歌曲,那么当删除这个播放列表后,是不会再继续播放。在删除过程中,如果删除到正在播放的歌曲时,会打开并播放播放列表中的下一个歌曲,直到播放列表中的所有歌曲都删除完。删除歌曲的动作是放在与播放不同的线程。
   问题:有些时候,删除含有正在播放歌曲的播放列表后,当前歌曲跳到了其他播放列表继续播放
   分析:删除过程中会涉及到很多个过程:停止,找到下一首,播放,和删除,如果每一次都这么走的话,是不可能出现这个问题的。经过大量的调试与跟踪,最终发现,是由于停止和播放没能一次执行完,中途被打断。因为删除是在另一个线程中,因此有可能当正在执行停止的时候,TaskScheduler切换了线程,执行了播放,从而切换到了另外的播放列表。
   解决方案:找到了问题的原因,解决起来就很容易了,加上同步锁,让删除,停止和播放的每一个过程都保证不被中断,就解决了这个问题。
   教训:这是一个真正的随机出现的问题,因为它是由于线程引起的。多线程带来的第一个问题就是不确定性,另外一个问题就是同步与共享的问题。如果没能很好的处理同步与共享,那么多线程会带来更多的问题,远多于它们所解决的问题。