作者简介
张子天,去哪儿网平台事业部客户端技术总监。2011 年加入去哪儿网,曾担任无线机票 Android 总监、无线架构总监,目前主要负责 Qunar 客户端基础架构和客户端生态建设。
在大规模客户端开发过程中,大家经常会遇到各种层出不穷的稀奇古怪的问题,而往往问题也隐藏的比较深,不容易去发现和迅速解决。长期被各类问题困扰,是身为工程师的我们,不能忍受的。所以我们要想办法解放自己,解放生产力。
一、用户场景
首先我们考虑一个经常面对的场景。
这是一个非常常见的问题,小白用户,或者是说普通的用户,往往是没有这个能力去理解App是在什么状态下发生了一个什么问题的,甚至有时候是闪退还是其他的问题都是有可能搞不清楚。这次的情景中,我们的用户能提供的信息只有2个。
然而我们需要知道的信息有
在没有我们的问题细查的情况下,能够想到的最快的办法就是查崩溃日志,然后根据崩溃的信息去看到底用户是怎么崩溃的。然后再联系相关的负责人员去看怎么解决问题,然而现实永远是残酷的。
现阶段的客户端开发涉及到方方面面的角色,有native,有hybrid,有react-native,甚至某些场景是多种开发方式混合的,不同的业务也有不同的负责人,同一个团队也同时面向多个App产品做产出,所以当我们去定位问题的时候会一个个把各个负责人都拉到群里去讨论,排除,协助查日志。
最终我们会在深更半夜叫醒了一群无辜的小伙伴,效率低下的处理了一个可能也不是非常严重的故障。
二、如何获得新生
同样是这样的一个问题,我们只需要还原用户的使用轨迹,根据用户的交互和网络请求,往往一两个人就可以找到用户遇到的问题究竟是哪里产生的,再也不用追着用户问题他到底在哪里遇到了问题,怎么能复现这些用户可能都无法回答的问题。
1、技术细节
我们能够完成这样一个用户场景的还原,所有数据来自三个独立系统:
1. 无埋点统计
2. 性能监控
3. 异常监控
那么问题就来了:
1. 交互日志如何收集
2. 如何做网络监控
3. 如何将不同维度的数据串联
2、QAV_无埋点交互统计平台
做业务开发的伙伴们一定都遇到过这样的一个状况,feature开发的时间往往比做埋点统计之类的时间还要短一些,每次的业务迭代都会伴随着大量的业务不相干的逻辑处理,久而久之,对业务代码的可读性和合理重构产生了非常大的阻碍,而且还有一个致命的问题,一旦需要统计老版本的App的某些数据,我们就束手无策了。那么基于工程师精神(lan),我们就在想能不能在不影响业务开发的情况下,把这些数据统计类的事情完成了呢?
首先,我们可以看到要监控的事件(上图)。
其次,不论是什么平台,交互事件需要控件的唯一标识。
关于唯一标识,实际上我们也走了很长的路。
最开始的时候,我们采用的是view的id去作为一个唯一标识,这个方案的问题在于view的id相对于开发者来讲,是有意义的,然而这个意义不一定是和产品角度保持的一致,当App迭代的过程中,不可避免的修改了id的名字的时候,就无法进行对应,而且会有很多的view并没有id的情况出现,种种原因,view的id做唯一标识方案逐渐被淘汰。
另一个方案是用坐标来做标识,这个方案原本是应用在iOS场景上的,但由于后期iPhone出不同的尺寸的屏幕,故而坐标的方案需要大量的转换和对应,也没有办法做的非常灵活,所以这个方案也逐渐淘汰掉。
最后我们采用了更为优质的XPath的方式来进行锁定唯一标识,XPath的优势在于可以定义各种平台的场景下的唯一控件,并且将上述几种标识方式的缺点弥补。
值得一提的是,这里在Android上面要处理不同的厂商的ROM下,root布局不一致的问题,在iOS我们还根据某个view在parent中的坐标排序进行了稳定性的定位,以保证同一个控件尽可能的少的被误判成多个控件。
既然提到了是无埋点,那么不干扰业务的操作就十分的重要了。
我们通过AOP的手段去做这些监控数据的提取和处理,保证不打扰真正的业务代码的编写。
3、性能监控
我们从用户的行为说到了网络请求,那么同样的思路,我们需要用AOP的手段去把网络部分的监控数据获取到。
这里不得不提到的是在Android上和iOS上有着不同的实际情况,iOS上情况比较简单,由于系统提供了可以Runtime期Hook的API,我们可以很方便的用替换插桩的形式注入我们的代码:
而在Android上虽然同样有类似instant-run的方案可以做Runtime的AOP,但在dex的格式上注定不能真正的做到Runtime,只是预先安放了大量的空方法段,以备不时只需,但这样的方案有比较严重的性能浪费,故而我们并不想用这个方案去解决我们的问题。Compile期的AOP才是解决问题的根本途径。
制作使用插件,利用javeagent原理,hook住jvm进程,在构建的class转dex阶段,运用ASM框架,根据特定规则去转化或生成特定类,进而达到代码注入的功效。
Gradle插件。
1、在dex任务前插入自定义任务installInject,利用tools.jar(jdk中,自行拷贝)中VirtualMachine,attach到当前运行的pid,并且loadAgent指定的agentJar并传递参数inject=true和其他参数,这样agentJar就会在main方法前启动。
2、在assemble任务后插入自定义任务B,重新loadAgent,传递参数inject=false,不再Hook进程进行类的转化。
为什么最后要执行uninstallInject任务,因为所有通过ProcessBuilder启动的jvm都已经携带上agentJar了,为了不影响后续JVM的正常使用,必须使agentJar停止工作。
三、数据聚合
异常监控的内容不是本次分享的重点,不展开去详细描述。大家可以发现,在这个过程中,各个环节涉及到了3个不同的系统,那么如何将数据做好整合,也是一个要解决的难点。
首先是日志的上传机制,交互日志/网络请求日志经过压缩打包,在不同的场景下触发上传;崩溃或卡顿等异常日志则为实时上传。上传的数据包中会有本地事件的时间戳,用于后续的数据对齐。
那么如何串联数据呢,我们在每一个交互产生的时候,生成一个requestId,在每次有网络请求时,将这个requestId注入到网络请求中去。完成用户操作和网络请求的一对多的绑定。另外我们还为每次请求做一个uuid,在请求链路中会像TCP的报文包一样,逐层携带一个头信息,保证后续的请求链路的串联。
上面提到每个数据都有一个本地的时间戳,这里的时间戳会和上传日志的时间做一个校正差值,获得相对于Server的一个稳定的时间,而对于不同的数据来说,本地时间戳又可以保证数据顺序在同一时间系中的一致性。
至此,我们就将用户的操作场景可以做到一个还原,呈现出用户的时间线可以非常有效的解决未知因素较多的棘手问题,提高效率,减小沟通成本。
然而实际上问题细查只是整套系统的一个组成部分,是整个大的筋斗云中不同的系统之间的一个有机结合。整个筋斗云提供的是App完整的从开发到测试到运营等等完整的支撑,我们将App和业务进行了剥离,使得整个环节中可以复用的程度从代码级提升到了组件和系统级。
更多的内容我们将来会有更多的分享呈现,有大家感兴趣的方面欢迎共同探讨。