引言
最近协助组里的小伙伴定位了几个比较“倒反天罡”的问题。事后聊起来,被问到最多的有两个问题:1. 你是怎么想到要这样定位的?2.总结文档该怎么写。这两个问题的答案其实是同一个,所以我写下这篇文章,希望能对大家有所启发。
框架思维
这类“倒反天罡”的问题有一个共同点,就是因果关系不清晰,甚至是混沌的,让人无从下手。可能有人会觉得我是因为之前积累了一定的经验,所以在面对这些问题时,根据之前相似的经验,从一开始就能清晰的看到解决问题的路。我可以很负责的说,事实并非如此,在刚接触到这类问题时,我和其他人一样懵逼。在面对这类问题时,我有一套思维框架,通过多次迭代表现
->分析
->目标
->行动
->反馈
这五个步骤,用反馈收集更多的信息,再将这些信息作为下次分析的输入,逐步接近问题的本质,就像剥洋葱一样,最后排除一切不可能,剩下的即使再不可思议,那也是真相。
。没错,又是这句我非常喜欢引用的一句名言。
这种思考问题的方式就是一种框架思维,它包含两部分核心概念。
1. 结构化的思维方式
人的思维很容易发散,发散的思维帮助人类产生创意,但却不利于解决具体的问题。解决问题本质上是寻找因果关系,尤其是埋藏较深的因果关系,需要的是有条理、持续性、体系化的思维,这样才能沿着一条脉络,不断深入将关系梳理清晰,进一步转化为行动,最后使问题得到解决。框架思维就是将杂乱、发散的思维限定在一个框架内,并将思维以一定的条理组织起来,最终形成体系化的思维。
2. 不限制思考内容
框架思维提供的不是解决某一个特定问题的知识或方法,而是作用于思维本身,套用元认知的说法,它其实就是对思维的思维。同时,它并不限制思考的内容,所以并不会因为使用了框架思维,导致认知被局限。实际上,很多人都陷入了一个误区,就是在思维方式上没有形成结构和体系,但思维的内容却过于依赖以往的经验,把自己装进了一个由以往经验画的安全圈内,导致认知受限。这与框架思维的核心刚好相反。
框架思维与bug定位
框架思维有很多种,今天只展开讲一种我在定位bug时常用到的。核心思想就是通过多次迭代收集反馈,并将反馈作为下一轮迭代的输入。随着迭代的深入,我们所收集的信息(即输入)越来越多,而输出(即表现)则会由不明确变得明确,最后找到输入和输出之间的对应关系,也就是因果关系,这个就是bug定位过程。解决bug就是去改变原因,让结果不再具备成立的条件。迭代过程如下图:
1. 表现
就是bug所表现出来的客观事实。包括文字描述、截图、视频、环境、报错信息、日志等。要注意的是,原始的表现信息一定要基于事实,要仔细甄别哪些是反馈人员的主观判断,比如“我要反馈一个兼容性问题”
,兼容性问题就是一个主观判断。表现是什么,比如白屏、代码报错、操作无反应、crash等。错误的输入是无法得到正确的输出的,如果错把判断当成事实,产生先入为主的想法,有可能导向一个错误的方向,浪费大量的时间。
2. 分析
基于表现,分析相关的原因,这一步需要记录两类问题:1.可能相关的原因。2.不确定的知识盲区。尽可能多的记录,不要过早下结论。
3. 目标
基于上一步分析的原因,设定这一轮要验证的目标。比如要确定某个api的调用结果,或者要排除哪几个因素的影响。
4. 行动
为上一步的目标设计验证方案。比如改变执行环境、加入日志、更改部分代码逻辑。
5. 反馈
收集上一步行动后的结果,主要关注三件事:
- 哪些目标的结论得到了验证
- 有没有产生新的问题
- 是否得出了问题的因果关系,如果没有,将结论与现有的表现结合,进入下一个迭代。
举个例子
这样说可能比较抽象,我用之前一个定位白屏问题的过程举例。完整的定位过程过于复杂,我只挑其中的几步来说明如何迭代,对这个问题感兴趣的可以移步组内小伙伴的文章。[白屏Bug复盘]
背景
是一个hybrid架构app中的h5页面,在一台iphone 12(iOS 15)测试机上,由A页面跳转到B页面,B页面会出现白屏,h5用的多页面路由方式,跳转会打开新的webview。
挑战点
- 只有一台测试机可复现,那台测试机和开发同学不在一个城市,无法对真机进行调试。
- 测试机所在地只有测试团队,不具备调试能力。
经过前期一系列的排查,最后将问题锁定到localStroage持久存储上。大致的逻辑如下:
- 页面A在state中设置
state.a = 'xxxx'
- 跳转前调用pinia的持久化插件,把state存入localStorage
- 进入B页面,pinia从localStorage中还原state
- 业务代码调用
state.a.replace(xx)
,a不存在,JS报错导致白屏。 - PS.页面A调用pina持久化存储后,跳转之前,通过调试代码读取了localStorage,确认state.a是存在的
开始迭代
迭代一
表现
页面A写入了localStorage,页面B读取到与A写入的不一致
分析
- 这个问题从表现上看是localStorage读写不一致的问题,核心问题就是“写”和“读”。
- 业务并没有直接调用localStorage,读和写都是通过pinia persistedstate插件来调用的,这个插件对业务来说是个黑盒,要尽量排除这一层的影响。
目标
- 确认是localStorage“写”还是“读”环节的问题。
- 排除pinia persisedstate的影响。
行动
pinia persistedstate最终还是调用localStorage的setItem/getItem
,所以只要去拦截这两个方法,将调用时间、参数、返回值等写入日志,最后分析日志,如果数据没问题,那就说明不是localStorage读写的问题,而是pinia插件的问题;如果数据有问题,说明在localStorage层就有问题了,与pinia插件无关,再看读写哪个环节数据与期望不符,所以这个方法能同时完成两个目标。
反馈
- A页面最后一次调用localStorage.setItem,传入的数据符合预期
- B页面第一次调用localStorage.getItem,读取到的数据不符合预期
这一轮迭代得到的结论就是与pinia无关,是localStorage读取的问题
迭代二
表现
同上轮的反馈
分析
从表现上看是读取的问题,这里就有一个疑问,如果读取到的不是最后一次写入的数据,那读到的数据是哪里来的?是数据更新有延迟,读取到了旧的数据,还是在读取前被污染了?通过日志发现A页面多次调用了setItem,如果能精确知道读取到的数据是哪一次写入的,就能解决上面的疑问。假如写入了w1、w2、w3三次。
- 如果读取到的是w2,说明更新有延迟,导致w3这次写入的内容未被实时读取到。
- 如果读取到的是x,说明数据被预期外的x写入污染了,接下来就可以把排查方向转为定位x写入是如何触发的。
目标
建立读写的数据映射关系。
行动
在每次调用setItem时都生成唯一的traceid,并记入日志,getItem时将traceid一起读出,这样通过traceid就能知道每次读取对应着哪次写入。
反馈
通过分析日志,发现A页面按时间写入tarce_1、trace_2、trace_3三次,而B页面只读取到了trace_2,而A页面则可以读取到trace_3。
这一轮得到的结论是在新的webview中localStorage更新有延迟。
迭代三
表现
同上轮的反馈
分析
localStorage是同步调用,按道理不应该有延迟,并且页面A也同步读取到了trace_3的数据,这是一个反常识的点。不过既然表现是有延迟,那核心问题就延迟多久,能否通过推迟读取时机读到最新数据。
目标
测量延迟时间。
行动
通过监听storage
事件,如果数据发生改变记录下时间。为了避免storage监听不到,同时做了轮询检测,如果改变同样可以记录下时间。
反馈
不管是事件,还是轮询,都没有记录下数据变化的日志。轮询5秒后,仍然只能读取到trace_2的数据。所以这里并不是数据更新有延迟,而是在新的webview里trace_2之后的数据根本没有更新。
迭代四
表现
同上轮的反馈
分析
两个webview的localStorage数据不一致,有两种可能:
- 两个webview访问的是两份独立的localStorage。但页面B读到了页面A写入的trace_2数据,说明localStorage是共享的,所以这个结论不成立。
- localStorage是持久存储,在app进程关闭后仍然能保留,数据是存在文件系统中的。localStorage本身是同步调用,但系统io可能是异步的,如果localStorage先同步写入webview的内存,再异步向文件系统中同步,在这期间当前webview可以读取到最新的数据,而新webview只能读取到已同步到文件系统中的内容,这时候就会出现不一致的情况。
目标
验证页面B读取到trace_2数据时,再回到页面A,页面A读取到的数据。
行动
页面A进入栈顶后,查看日志中getItem的数据
反馈
从页面B返回页面A后,仍然能够读取到trace_3的数据,验证了上面猜测,并且得到结论,在打开页面B后,页面A退出栈顶时,同步至文件系统中止,所以无论在页面B停留多久,都无法读到trace_3的数据。
最终结论
原因如下图所示。
了解到这个问题的本质后,发现这是系统io的问题,无法在应用层进行干预,所以解决的方案就是替换持久化存储方案,将localStorage改成hybrid sdk提供的app storage。
通过这个例子可以看出框架思维定义了思考问题的基本模式和步骤,在解决问题的过程中,有一种逐步接近本质的节奏感。
框架思维与知识分享
总结文档本质上是一种知识分享的形式,要写好知识分享文章一般分为四个部分。
- 清晰的问题定义。你所要解决的问题是什么,这个问题中的挑战点有哪些?
- 思考步骤。你是如何找到问题的解决方案的。
- 解决问题的方案以及过程中用的知识、工具、方法。
- 总结和引发思考。
如果要按重要程度给它们排个序,我认为是1 -> 4 -> 2 -> 3。
而上面的3、2、4分别对应了知识的三个层次。
鱼
鱼就是具体问题的答案,可以是一个方案,一种工具,人们不用太多思考就可以用它解决眼下遇到的具体问题。
渔
渔是找到问题答案的方法,需要受众有一定的思考,随着思考的深入,所能解决的问题的范围会扩大。比如一些方法论。
悟
悟只定义框架的核心部分,具体内容可以千变万化。旨在激发人自身的思考与感悟,允许仁者见仁,智者见智,每个人都可以结合自己的感悟去填充框架,生成自己的版本,而框架本身也会在后人的感悟中进化升级,真正的经久不衰是用进化适应变化,而不是从一开始就无懈可击。儒、释、道、各个流派的哲学等伟大思想都具体这样的特点。
知识分享可以先根据目的、内容和受众确定分享内容所处的层次,然后将内容组织成恰当的结构并呈现。当然还有一种我很喜欢的方式,就是以鱼开始、渔为主、悟结尾,抛砖引玉,引导受众逐步深入,最后回归到自身的思考上。