2018年01月02日分享于pFinder邮件组,作为《参加工作以来的成长(一)》的附件。
高效快速地找到一个复杂系统中问题发生的根本原因,是每个工程师的必修课。当我们面对这样一些问题时,往往不知道复杂系统的全貌和所有细节,这种状况下应该怎么办?对于一个训练有素的工程师,肯定不是头脑发热,不知所措,或者到处去找人询问,而是从有限的问题本身出发,不断地反问自己,如果要找到问题的答案需要知道什么信息,层层地挖掘出更多的相关信息,从而找到问题的症结。这是一个结合深度搜索和广度搜索的技能,并且需要恰到好处的结合,想得太深可能会让自己陷入一个死胡同,关注的太广可能会让自己迷失在太多的细节当中,而忘记了来时的路,和最终要达成的目的。
前段时间仅花了两天的时间就找到了一个手机应用偷跑流量的根本原因,着实兴奋了一整天。解决了问题是一方面,发现自己在高效地找到一个完全不熟悉的问题发生的根本原因方面有了长进是更重要的另一方面。一直思量着把这个过程记录下来,却不得空,今天总算可以静下心来写写了。
我被委派的任务是调查一个手机应用偷跑流量的问题,具体来讲,测试人员测试发现公司的安卓手机应用在预装的定制机上,未启动的情况下就已经开始有流量消耗,并且测试人员反映发现偷跑流量时有跟个推(国内的一个推送服务)相关的日志。
收到任务时,我的情况是,1) 对安卓只有一年的开发经验,而且是断断续续地利用业余时间学习;2) 对安卓的推送机制基本不了解,仅对推送的过程了解一些;3) 对如何预装应用完全不清楚;4) 对公司复杂的手机应用知道一些跟WebView相关的部分,估摸着看过的代码最多也就几千行,而整个应用仅核心两个包的代码就有十几万行,附属提供功能的依赖包至少有一百多个,整个依赖的包有近两千个,整个应用的代码量在百万行甚至上千万行的数量级上,整个应用的完整的构建需要两个多钟头(当然局部的修改不会进行完整的构建)。
搞清楚问题和清楚自己所处的状况是解决问题的第一步,千万不要盲目。解决复杂系统中的问题,跟做书本上简化了条件的题目不同,但方法论大体还是一致的。从小做数学题时,老生常谈的就是审题,清楚题目的条件和限制。简单题目之所以简单,主要是解题的所有信息完全可以装在大脑中,只需要自己审清题,大多数情况下问题就迎刃而解了。复杂问题的信息是不可能全部装在脑子中的,需要去挖掘和搜索。搞清楚问题是要明确目标,并时刻铭记目标,防止自己走偏。清楚自己所处的状况更重要,这样可以帮助自己找出阶段性的目标,逐渐将原始问题的未知条件转化为已知。
面对这个具体的任务时我首先问了自己以下的几个问题:
1) 在定制机上预装的手机应用未启动时,有流量消耗,那么对于非预装的应用呢?
2) 如果非预装应用没有这样的问题,那么预装的手机应用在未启动之前与非预装应用有什么差别呢?
3) 对于未启动前的阶段是安装阶段,这个是常识,那么安装阶段安卓平台做了哪些事情呢?
4) 如果非预装应用也有这样的问题,那么问题也一定出在安装阶段,同样也需要知道问题3的答案。
5) 如果个推在偷跑流量,那么一定得有后台运行的服务被启动,否则谁去打印日志呢?进一步这个服务为什么会在安装阶段被启动?这两个疑问也都指向了问题3。
在清楚问题和我所处的状况下,有正常逻辑的人都不难问出以上的问题。将这些问题用纸(或者云笔记)记录下来极为重要,因为尝试着去回答每个问题时,你还会问出自己更多的问题,更多的问题增加了广度,让自己对问题了解得更多,但同时也很容易丢失目标,不要让自己大脑不停去重复想前面提出的5个问题,大脑会很累。写下来还有个好处是一天没有解决完的问题,第二天可以快速重新捡起来。
说完写下来的重要性(重要的事情总要说三遍),接下来回头看下提出的5个问题的特点。第一,它们跟原始问题中的几个关键因素直接相关,比如预装、流量消耗、打印日志、个推服务。第二,它们是对原始问题的分解,彼此之间有很强的逻辑关系,这一方面可以让思考变得严密,另一方面也更加具体。第三,它们运用我有限的常识和对比的方法,引入了更多的我熟悉但还没有透彻掌握的因素,比如启动前的步骤只有安装,打印日志一定有后台服务运行。总结下来,提问的时候,首先要寻找原始问题中的关键词,然后结合自己的常识,运用逻辑和对比的方法,引入更多的因素,分解出更多的问题。
除了将分解的5个问题写到一页纸上以外,还要另外新建5页纸,将5个问题分别作为5页纸的标题,为后续的问题分解、搜索和寻找答案做记录。
做好准备工作后,可以暂时放下思考原始的问题,转而从第一个问题开始,各个击破。
第一个问题中,整体是在问,非预装应用在相同的条件下,是否也有流量消耗。可是在解决这个问题前,还有几个问题令我充满困惑:
1) 如何复现原始的问题?
2) 提到复现,就需要知道怎么能做一个预装应用?
3) 预装应用被装在了设备的哪个目录下?
4) 非预装应用被装在了设备的哪个目录下?
5) 预装应用有哪些区别于非预装应用的特点?
6) 如何做非预装应用的对比测试?
提出这些问题的动机和方法同提出前面5个问题的动机和方法类似,我进行了第二层的深度分解。这一层的问题进一步具体,同样需要用纸记录下来,逐个地回答。同一层级的问题之间有个特点,前面有提到,就是彼此之间有很强的逻辑关系。可能其中的某几个问题还比较抽象,但只要有一个问题足够具体,利用现有的认知做简单的搜索可以得到答案,那么就不必要进一步加深分解的层次,以免节外生枝,消耗不必要的脑力。
拿上面分解的6个问题来看,问题1和6都比较抽象,剩下的很具体,尝试着回答了2到5,那么会把1和6更佳的具体化,至少在自己的大脑中是如此。相比于3到5,问题2也显得不够具体,所以先回答3到5是个不错的想法。
回答未知的问题,以前的人们只能翻书,效率很低,现在有搜索引擎,虽然很快,但也需要能有效利用。这就要提到如何增强自己的搜索能力,如何综合利用多个搜索引擎的结果,如何快速定位自己关心问题的答案。回答这几个问题同样可以重复前面采用的方法,进行问题分解,为避免跑题,在此只列举一些我个人的经验。首先,用中文关键词在百度或者Bing中搜索,搜索出来的答案可以辅助拓展关键词,但不可以全用,大多数时候不准确,不全面。其次,准确地将关键词翻译成英文,在Bing或者Google上搜索,高质量的答案一般来自于官方文档、Stackoverflow、Google论坛,博客上面的答案可以用来辅助拓展关键词和充实答案。再有,学会关键词转换,转换关键词的过程其实是转换思考问题角度的过程,在很多山穷水尽的时候,转换思路或许能柳暗花明。还有,启用搜索引擎的高级搜索功能,只找某几个网站的搜索结果,或者要求关键词严格匹配,直接滤除掉低质量的结果,减少干扰。最后,浏览搜索结果,看相关度的时候,要始终想着要解决的问题,以免被带偏。问题越具体,越容易找到相关度高的结果。
在百度中搜索以下两组关键词:
在Bing上搜索以下三组关键词:
- Android preload application installation location
- Android application installation location
- Android application installation folder
百度中第一组搜索关键词的头五名:
- 怎样知道安卓系统上安装的应用程序其所在文件夹…_百度知道
- 安卓手机如何彻底删除预装应用_百度经验
- 教你怎么把安卓应用软件放到系统根目录system/app下 – 飞…_博客园
- 安卓手机安装后的安装包文件怎么找_百度经验
- Android应用程序的安装位置 – CSDN博客
百度中第二组搜素关键词的头五名:
- 怎样知道安卓系统上安装的应用程序其所在文件夹…_百度知道
- android APK应用安装过程以及默认安装路径 – CSDN博客
- Android开发出来的APP在手机的安装路径是? – CSDN博客
- 安卓软件安装目录在什么位置?-其他软件-ZOL问答堂
- 各种APP的安装路径在哪? – Sony Xperia Z3/Compact 安…_机锋论坛
快速的浏览10篇帖子,有些提到/data/app,有些提到/data/data,有些提到/system/app,还有些提到/system/priv-app。各个帖子试图去解决的问题不同,可能不是对我关心问题的正面回答,但不妨碍我将它们作为候选答案和新的搜索关键词。在浏览帖子的过程中,我了解到有人问过安卓手机如何彻底删除预装应用_百度经验,我获得的认识是系统应用可能很难删除。在浏览帖子Android应用程序的安装位置 – CSDN博客时,我发现标题虽然相关,但内容在谈应用如何能放在外部空间,根据过去对存储分级的认识,我可以联想到默认的应用应该放在内部空间中,这时候勾起了我强烈的兴趣去了解安卓系统的存储结构。面对这种情况,如果时间充裕,不妨搜索一下,以满足自己的好奇心,如果时间不允许,最好就此打住,以免花太多的时间在拓展问题上。浏览的所有帖子中,android APK应用安装过程以及默认安装路径 – CSDN博客的质量最高,不妨把这个链接留下,写到自己创建的笔记中。
综合中文的搜索结果其实已经基本得到了问题3和4的答案,在笔记中做下记录。问题3的答案是/system/app和/system/priv-app,问题4的答案是/data/app。如果觉得这两个问题太小,也可以合并成一个笔记。为进一步确认我们的判断,阅读下英文的搜索结果,发现有人正面回答这个问题,比如下面的几个帖子:
- Where in the file system are applications installed?
- where is .apk location for apps that are installed on sdcard?
- Where does Android app package gets installed on phone
确认得出的结论正确后,还需要到设备上实际操作一下,得到最终的确认。帖子Where does Android app package gets installed on phone中的一个回答者提到了adb shell pm list packages -f这条命令,去执行一下,并且在笔记中做记录。
问题5相比于问题3和4相对复杂些,复杂性在于要从两个本身就有些复杂的概念中寻找差异。类比地来想,问题3和4分别在问“中国人住在地球的哪里”、“美国人住在地球的哪里”,而问题5却要回答“中国人和美国人有什么差别”。第一类问题具体到了被考察对象(预装应用或者中国人)千百万个属性中的一个。第二类问题却没有框定哪些属性。对于复杂概念的属性,人们往往还会根据复杂概念本身跟外部的依赖关系、用途而对属性进行进一步抽象、分类,以便于容易认知和讨论。拿中国人和美国人来讲,可以讨论的属性没有穷尽,但可讨论的抽象出的类别相对有限,比如生物特征、生活方式、历史习俗、社会结构、思维方式等。实际上简单想想都会觉得,问题5的复杂度肯定比回答中国人和美国人的差别要低得多了,然而后者虽然客观来讲更困难,但在感情上却是容易的。思考它不仅可以辅助拓展问题5的搜索关键词还能够明确我所关心的差别方向。经过这一番类比,我能想到去限定问题5的类别对比范围,我更在乎启动前的差别,至于启动后,暂时可以不必了解,没差别最好。想到这里,我可以写下以下的几组搜索关键词
Difference normal app preload app Android
Difference normal app preload app installation Android
Difference normal app system app Android
Difference normal app system app before start Android
预装应用 非预装应用 差别 安卓
预装应用 非预装应用 差别 安装 安卓
预装应用 非预装应用 差别 启动前 安卓
经过在Bing和百度中的搜索,我找到一些相关的答案,很多在谈权限的问题。不过搜索过程中,我同时还发现我的用词不够准确,大家很少叫normal app,而更多用的是user app;preload app也很少用,而更多用的system app。中文中大家更倾向于用普通应用和系统应用。看到系统,我又想起了我在前面找到问题3和4的答案,于是我改进自己的搜索关键词
Difference user app system app Android
Difference user app system app permission Android
Difference /data/app /system/app Android
普通应用 系统应用 差别 安卓
普通应用 系统应用 启动前 差别 安卓
/system/app /data/app 差别 安卓
多次重新搜索(re-search)后,终于找到看起来靠谱的两个答案。第一篇是StackExchange上面,有人问“What are the differences between a system app and user app?”,高票的答案解释清楚了主要是拥有系统权限的不同。这样的答案可以促进自己知识的结构化,却不对解决问题产生直接帮助,因为我还是不知道什么系统权限有差别。跟系统硬件相关的权限有很多,到底由哪个造成。从头到尾读安卓的系统权限相关的文档,或者是拿一个一个的系统权限常量串到代码库中去搜索都不太现实。在我即将准备进一步优化搜索关键词的时候,第二篇CSDN上的一篇博客“System APP 与普通 APP 简析”清晰地为我指明了方向。博客中的第二点不仅说了权限有差别,并且说了是广播的接收权限有差别,系统应用即便用户没有打开应用,也一样可以收到广播消息,而普通应用必须打开应用后才能收到广播消息。
问题答案找到这里,我虽然只能回答第一个大问题的3个小问题,但是似乎已经距离找到答案很近了。我已经将可能出问题的范围缩小到了广播接收器和权限,外加一个条件就是个推,于是我兴奋地去个推包的AndroidManifest.xml中找所有的广播接收器和权限声明语句。看到以下几行可疑度比较高的语句。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<receiver android:name=".PushReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.intent.action.USER_PRESENT" />
<!-- 省略 -->
</intent-filter>
</receiver>
权限一行的声明在说,应用在请求启动完成时的某种权限。广播接收器的声明在说,接收器在匹配到以下的三种事件发生的广播情况下能执行一定的任务,包括android.intent.action.BOOT_COMPLETED、android.net.conn.CONNECTIVITY_CHANGE和android.intent.action.USER_PRESENT。这时候,我再去找官方文档阅读印证我的理解是否正确。阅读后发现这三个事件过滤器的确是只针对系统应用才能产生效果的。进一步确信后,我创建一个修复分支,删除这几行代码后,需要验证是否真的就是这几行代码起的作用。对于这个验证,我有两点预期断言(Assertion):1) 在预装应用中不再出现偷跑流量的问题 2) 对于非预装应用的推送功能并没有因为这几行代码的删除而执行异常。第一个断言属于功能性测试,为了修复不满足需求的漏洞,第二个断言属于回归测试,为了避免修复引入新的问题。
做功能性的测试我需要先搭建测试环境,如果测试环境搭建好了,我预期基本上问题1, 2, 6的答案也就都有了。在这个问题方面,因为测试,外组有专门的人员负责,对我而言也只是个临时的任务,于是我在尝试搭建测试环境失败后,将任务转给了他。由于他人在美国,我需要明确要他做的事情和预期,在转交任务前,我做的测试尝试包括:
1) 找问题2的答案,
2) 做初步的回归测试。
回归测试的环境是现成的,我将带着修复的应用装到普通用户应用区域,完整地测试了推送功能和查看错误日志,并无异常发生,所以回归测试在我这里算是过了,后期还需要我们组专门的测试人员进行重复测试。
找问题2的答案,相比问题5难度要小,不过执行操作比较多,而且存在着损坏硬件的风险。系统区是ROM区,只有ROOT权限才可以读写,并且启动加载的操作系统内核程序也存放在这个区域,因而操作风险较大。我尝试了两三次向一台ROOT过的机器强制覆盖应用都导致整个手机系统重启,并产生了系统崩溃的报告。为了尽快完成修复测试,我将带着修复的应用转给了外组的测试人员,他在五天之后给出了测试结果,平均每天的流量消耗是零。
问题已解决,有闲暇的时间里,我将剩下的一些问题做了一些简单搜索、思考和整理,以增加我对系统局部模块的认识,就不在这里赘述。
回顾整个问题解决的过程,积累结构性的知识和特定的领域细节固然重要,但反思和迭代自己解决问题的能力和方法更能让人感到一种成长的愉悦。如果特定领域的知识积累到一定程度,比如对安卓系统非常精通的人,大概看到这样问题的发生,就已经推测出可能出问题的方向,一点都不需要这个复杂的搜索、重新搜索的过程,而是直接去代码中找了。然而,现实中,所有的技术更新迭代的速度超乎我们的想象得快,做到对一切细节都能把握得很清楚是件不可能的事情,这个时候练就快速在大脑中索引和找到细节的能力就显得更加重要。学会反问自己恰当的问题和良好的记录习惯是快速掌握这个技能的核心要素。另外,我想这个技能应该也是精通某项技术不可或缺的能力,换句话说,如果很难以习得这样的能力,基本和精通无缘了。学科领域细分的今天,只有跨领域才能做出点东西,习得这样的能力也只是开始,与此同时,这个技能也是快速介入其它相关领域的好途径。