江赛,听云研发总监,负责听云移动端产品的研发工做。在 OSC 第 55 期广州源创会上发表了题为《移动端 APM 产品研发技能》的演讲。现场介绍移动端 APM 产品底层技术细节与实现方法, 演示如何经过在代码中埋点来解决移动 APP 的性能问题 ;分享在实际产品开发中碰到的问题和一些经验,以及一些技术细节。java
移动端 APM 产品,从字面上来理解,APM(application performance monitor)就是应用性能相关的监测,可随着如今产品的边界愈来愈模糊,监测的范围不只包括 performance,还包括用户行为,以及在稳定性、卡顿、崩溃这些方面的数据都有监测,已经远远超过 performance 这一个角度,毕竟产品结构愈来愈大了。程序员
因此对于这样一个产品,要作数据监控和数据分析,它的基本前提是什么呢?就是必需要采集大龄的数据,包括一些基本的数据。将这些数据放在不一样的维度分析。浏览器
举个例子,从网络的角度来讲,有用户反馈某个产品在某个运营商范围接入的状况下,网络性能不好。这个数据就会直接从报表里面去体现,由于会采集到一些基本的网络数据,也会采集到其余的不一样的维护数据,而后这些问题就会展示出来。安全
从这张图来看,数据是咱们产品的一个移动研究方向,并且咱们的产品会支持苹果、Android 还有 Web 这三端。会采集的数据包括:网络数据、交互行为数据、稳定性相关数据和一些其余的数据(例如采集手机的信号。这些数据会有一些不一样的应用,好比说运营商,它在部署各类基站的时候,会有一个参考值,就是哪一个地方信号不太好,它会在那里部署基站,可是怎样知道信号很差呢?不可能在每个角落都放一台手机看信号如何。此时咱们的产品就能够完成这个任务,移动端能够采集到这些信号,而后根据不一样的地域来分析手机信号分布状况),这就是采集数据的大概内容。服务器
而后往下细分会有更多类别。例如网络数据,从应用层的数据来看,主要是采集 HTTP/HTTPS 的数据,但又不只仅是 HTTP/HTTPS 数据,好比说一条 HTTP 请求,假如从 Web 上或者是浏览器中输入一个网址,咱们会把全部的 HTTP 请求内容分析出来,例如出去包的长度、回来包的长度和 response 的时间等等。若是出现错误的时候,还会把 response 的包和头部信息打印出来,会把 HTTP 协议请求所有分析一遍,分析字节大小,响应时间,还有错误这些状况。而后还会往下分析,好比 HTTP 请求访问以前须要作 TCP 连接的所用时间。网络
这些数据正常状况下是没有办法采集的,须要特定的技术,这个也是今天我要分享的内容 —— 咱们是如何抓取这些底层数据的。架构
还有一个是页面加载的数据,页面的加载包含三种数据(页面加载、浏览器渲染和 DOM 加载)。Android 和 iOS 会经过 JS 注入监控一些数据,和监测一些页面加载的详细数据。app
关于交互行为数据,举个例子,产品会监控用户在一个应用里的一些点击行为,像一系列的滑动,对菜单的选中。好比说点击一个按钮之后,若是它的响应时间过长,通常阈值是 3 秒钟,若是点击完按钮 3 秒后才处理完,咱们会自动把事件抓取并上报。如今咱们还能够作到,当监测到卡顿之后,会自动去把当前的操做截屏(能够作一秒钟 10 帧的截屏)。经过一秒钟 10 帧的数据而生成的动画,也就能看到卡顿的时候所在的页面。这个产品暂时还没发布,但技术上已经实现了。如今关键的问题是普通的截屏会很是影响性能和耗电,如今能作到 1 帧数据在 5 毫秒左右,效率很是高,截屏速度也很是快。负载均衡
关于稳定性,稳定性就是崩溃和 ANR(卡顿)相关的。有一些开源项目能够支持这种需求,因此相似崩溃、ANR 这些数据的采集难度不大。函数
收集了不一样的源数据之后,就会接触到不一样的维度,这些维度包括地域、运营商、接入方式、设备、操做系统、应用版本以及其余一些维度数据。根据这些维度数据和一些自定义的相关信息,会作特定的网络数据监控。经过这个,就能够看到对应的不一样源数据在不一样的维度组合下的结果,好比能够选择某一个地方、某一个运营商或者某个设备在某种接入方式上,它的 HTTP 请求效率,这就是基本源数据以及基本数据的应用。
不少应用厂商也尝试本身抓取这些庞大的数据,但若是用传统的方式来作,就意味着须要打不少的点,好比说一段代码,须要在 excute 进入的地方打一个点,出去的地方也打一个点,同时还要把参数抓取下来作参数的解析,这就意味着若是手工来作这种工做,工做量会很是大,由于全部监控的地方都要埋点,并且一旦这段代码发生变化,也就要从新去修改埋点的代码,并且从新去埋点,也会致使工做量很是大。
所以作数据采集的时候,咱们有一个基本原则:尽可能不让程序员作任何事情。添加一行初始化代码就够了。那么如何采集到这些数据?这就是数据采集的基础,自动埋点技术。这些埋点的操做不须要本身作,会经过程序自动完成。下面介绍几种自动埋点的方法。
主要经过如下的技术手段实现:
下面对每个技术细节展开进行讲述:
对于 ByteCode 的处理,支持 Java ByteCode 的注入以及 Dalvik ByteCode 的注入。在内应用层会提供 Hook 方法来 Hook 分析 C/C++ 代码,JavaScript 相关的会经过 JS 注入的方式来采集数据。
看起来比较抽象,下面一一展开来描述:
对于 Android 程序员来讲,大部分代码都是用 Java 写的,拓展名是 .java 的文件。但真正打包编译完之后,会生成 apk 文件。若是你把它解压会看到有一个 dex 文件,由于如今的包愈来愈大了,可能会有多个 dex 文件,那么这些 .java 文件是怎么变成 dex 文件的,这个过程是如何的?
编译的过程是首先从 .java 文件到 class 文件,而后 class 文件再到 dex 文件。.java 文件到 class 文件是经过 javac 编译,而后再经过 Android SDK 下的一个工具 dx 将 class 文件编译成 dex 文件。
在 Android 的虚拟机里面,正常状况下编译完之后,Java 虚拟机里面执行的是 .class 文件(即 Java Bytecode),可是在 Android 的 Dalvik 虚拟机或者 ART 里,不能直接执行 Java Bytecode,所以须要将 Java Bytecode 作一次转换,转成 Dalvik Bytecode。该过程就是使用 dx 这个工具转换的,并且是在编译的时候完成。其实就是不一样的格式表述,.class 文件只是用了另一种字节码的格式来表述。这个东西看似很简单,但若是了解编译的过程,就能够作不少的事情。 class 文件生成了之后,尚未转成 dex 文件这一步,就能够经过 ASM 技术,对 Java Bytecode 进行改写,从而插入要监控的代码。
下面经过一个实际的例子来说述。
先来看代码:
Example Java source: Foo.java class Foo { public static void main(String[] args) { System.out.println("Hello, world"); } public int method(int i1, int i2) { int i3 = i1 * i2; return i3 * 2; } }
这段代码的功能很简单,里面有一个方法,传进来两个参数,先将这两个参数相乘,再把结果除以 2 返回。经过 javac 把它编译成 Java Bytecode,而后用 javap 能够看到 Java Bytecode 的指令。这是一个很简单的 Java Bytecode 指令,取得两个参数,而后作乘积。imul 指令就是 Java Bytecode 的一个基本指令,以后就是把两个参数压栈,imul 指令会 pop 出栈底的两个数。
$ javac Foo.java $ javap -v Foo public int method(int, int); flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=3 0: iload_1 1: iload_2 2: imul 3: istore_3 4: iload_3 5: iconst_2 6: imul 7: ireturn LineNumberTable: line 6: 0 line 7: 4
能够看到,方法的名字和参数都没变。其实 Java Bytecode 和 Dalvik Bytecode 很大的一个区别就在这里,Java Bytecode 须要借助堆栈来模拟这种操做(乘法、除法),经过栈来临时存放这些变量,但在 Dalvik Bytecode 里就不是经过栈来实现,而是经过寄存器实现。看一个栈的操做示例:
Stack Before After value1 result value2 ... ... ... (imul指令对栈的操做)
先是传入两个变量 value1 和 value2,imul 执行完之后就把结果加到栈里边,这就是一个典型的栈操做。
由于 Java Bytecode 没有办法在安卓手机上运行,所以须要将 Java Bytecode 继续经过 dx 工具把它编译成 Dalvik Bytecode。不少时候你们都是经过编译工具进行编译,没有尝试经过手工进行编译,建议能够尝试一下。经过 dx 就能够把 class 文件编译成一个 dex 文件,而后经过 dexdump 命令,把 dex 文件 dump 出来。能够看到,刚才的 Java Bytecode 里几行乘法指令,在这就就变成了一行指令。
能够看到,首先指令长度变小了,第二 Dalvike Bytecode 引入了寄存器的概念。而 Java Bytecode 的函数调用所有是经过栈来模拟的。这种方式对代码性能,以及代码结构大小有影响,并且寄存器自己的性能要比栈高不少。
再看一下,刚刚那三行代码两次 pop 操做,一次乘积,一次 push 操做,如今变成这样一个操做。就是这个指令,通过目标计算器,源计算器,操做完之后,存在源计算机,如今变成这种形式。
下面来看一下 Java Bytecode 与 Dalvik Bytecode 的对比:
Java Bytecode 和 Dalvik Bytecode 有什么区别?前者用的是栈,后者用的是寄存器。
这些对于自动插码技术有什么做用?前面提到的指令级插码又有什么做用?其实这些是基本工做,首先要对Java Bytecode 很是的熟悉,以后要了解整个编译过程。
这个代码就是经过动做分析 Java Bytecode 注入的,反编译出来就是这样。咱们须要分析一些关键的方法,还有特定方法,找到函数的头和尾,插入须要的代码,第一步为获取开始时间;第二,获取完成的时间,以后进行上报。像作一些错误处理,会对异常进行捕捉,这样就能够自动分析你的 Bytecode 来作注入。
还有一个特殊的状况,就是须要监控的是这个调用,或者说监控这个调用的反馈值,这些状况都会出现。但全部的变化都是基于对 Bytecode 上下文的理解,而后插入对应的指令。这个技术不是咱们首创的,ASM 技术已经有不少年了,各位能够去看一些开源的 ASM 项目。
还有一个技术,Java Bytecode 注入是咱们产品如今主要的注入方法,可是也还有不少其余注入的方法,下面要讲的就是另一种的方式 —— 经过 .smali 注入,具体的逻辑以下图所示:
经过一些 smali 反编译工具,转成 smali 文件,静态分析这些文件,分析完之后会作代码的注入,而后从新打包,再加一个签名就能够了。smali 不是 Android 官方的 Bytecode,是一个开源的 Bytecode。
这些你们都不陌生,作 APP 开发不少时候会用这些工具帮助分析一些事情。一样你也能够借鉴一些新的思路,经过这种方式分析 APK。认为存在恶意行为就分析。另外还能够作动态调试,把一些参数打印出来。
好比说写了一个工程,能够作一个定制,写一个简单的SDK。分析一个 APP 的时候,须要分析其网络行为,就把 SDK 注入进去,而后打包,以后看网络访问过程中访问的什么主机、IP。若是有加密,那就经过另一个话题对流作解密,通常的状况下,传输的数据均可以看到。
由于 Android 中不少代码不必定是用 Java 写的,也能够用 C/C++ 写。这种代码不能经过 Bytecode 的方式来注入。看下面这张图
这是一个普通的调用关系,调用者调用被调用者执行,执行完之后返回。这是正常的处理流程。但若是要监测这个被调用的方法,想要拿到参数,以及这个方法执行多长时间,还想知道这个返回值,如何实现?逻辑上很简单,把被调用方法头几行指令作修改。把指令改为 JMP 指令,JMP 到这个监控方法里面,经过 hook 的方式作跳转。这里作参数、相关函数的记录,作完之后再从新按照这个轨迹返回。
如何作到这一步呢?首先,把头几行作跳转。这须要对 ARM 指令,对各类架构比较熟悉才能作到。大部分程序员都学过汇编指令,但遇到的时候以为很复杂。实际上并不复杂,只是接触的少,其实 ARM 32 指令很少。根据后面 3 位,4 位能够作区分。还有一些分值指令,数学预算指令。那么,分析这些指令的时候,首先对于指令架构要很熟悉,并且,要知道源计算机,目标计算器在哪里。好比说,最终跳转指令的时候,要知道跳转怎么计算,24 位 offset 怎么跳转,24 位怎么转换为绝对地址。若是把基本概念弄明白,不要求会写,就能够作下面的事情了。
先看一下刚刚说的方法怎么作到的。
须要改写这个方法的头两行指令,头两行指令替换成这样的指令。PC 指令就是当前运行时的逻辑地址,PC 寄存器。由于 ARM 32 会作一个预加载,这个会指向下两行指令。若是将 PC 指令减 4,就是变为 PC 加 4,这个操做是把下一行指令移到 PC 寄存器中。若是改写 PC 寄存器就实现了跳转,虽然只有两行代码,可是能够想到这其实要花很长的时间。
这须要了解 ARM 指令,知道这个 ARM 指令执行的过程,还要知道经过修改 PC 指令实现跳转。经过改写头两行指令,就能够把它跳转到任何地址。并且这个地址就是 4 字节,32 位,4G 空间。能够跳转到任何函数,但这还没结束。后两行作了之后,要把头两行移到另一个地方。可是,移动指令的时候由于一些指令自己就是依赖 PC 指令,因此要去作指令的修复。所以更多的工做其实就是在修复这些被移走的指令。下面的例子是一个 B 指令修改,是写实际代码的一部分。
来看一下这一行代码是什么意思。123,2 个 0 是 8 位,8123,高位是 0,0,F。若是是 31 到 32 位,咱们如今取的值是实际上就是取这 4 位,1234,取 4 位的值,经过这一行指令取这个值。而后经过 4 个值区分这些指令类型。取出来了之后,若是这个是 A,能够看一下 B 指令的方式,1010,这个是 1010,一个是1,就是 BR 指令,跳出去再跳回来。若是无条件这里就是 0,1010 正好是多少就是 10,就是 A,若是是 B指令。B 指令跳转依赖寄存器,首先算出来这个地址,把绝对地址存在这里,头一行指令在这里。
若是要真正把这个弄明白,能够经过编写 C 代码作到。若是作到这样以为颇有成就感,把系统的 malloc,或者是 new 给 hook 住,能够监测全部的 native 内存申请和释放。
将 hook 技术应用在产品上面,发现不少的产品都是依赖这个技术的。好比安全方面,不少产品也是经过这种方式作的。还有经过这种方式来作一些底层资源修改和调度,这个能够用在不少的方面。由于技术是为了产品服务的,只要把技术弄明白就能够了,最终仍是会产品化。这是像我这种作不少年技术的人切身的体会。有时候也是会沉迷在技术里面,总以为作一些产品的工做就是浪费时间。如今想一想,并不如此。
最后一点,前面讲的这些,都是一些自动嵌码技术,包括 Java 应用,还有 C++ 应用。数据都是自动采集的。在编译时插码,在运行时使用 hook,这些均可以作,由于产品已经很成熟。听云如今运行着 5 亿终端,有一些大的电商类也已经在用听云的 SDK。
举个例子,想经过听云对 TCP 层的监测结果来观察负载均衡调度状况,同一个主机有一堆 IP,正常状况下是没有办法拿到这个结果的。咱们不只能够拿到 DNS 时间,还能够拿到 DNS 结果,真实 IP 是什么,经过这些状况能够看到负载均衡服务器,即调度出来的结果状况以及 IP 分布状况,另外还有 TCP 三次握手时间,SSL 握手时间等。
这些数据都很是的有用。安卓程序员常常纠结使用哪些网络库,是 urlconnection,仍是 okhttp。分别都有什么优缺点。这个咱们就给大家作了一个强大的技术验证。
第一个问题,好比说,在程序里面连着发了 10 个 request。如今 HTTP 访问的传输层都是基于 TCP,但每发一次 request 都要作一次 TCP 链接吗?仔细想一想,对于同一个地址确定没有必要,这样作就是浪费时间。而后遇到的就是 TCP 复用技术,经过这种技术,就能够监测对于一个同一个目标地址发生多少次 TCP connect 操做,这就知道在这个访问时间内有没有复用以前的链接。因此,就能够得出一个指标数据,即发生了多少次 TCP 链接。
下图是 APM 产品
经过这种技术能够监测一些关键指标数据,由于采起底层原数据,不少点就会把这个原数据还原出应用场景,客户想出来的场景比咱们多。这些原数据都是最宝贵的数据,而且最关键的是不须要你再去作额外的工做,也是 APM 的价值所在。
今天讲的内容比较抽象,讲的是研发过程当中的一些经验,技巧和总结。这个技术可能对各位如今的工做不会有直接的帮助,由于太底层,但也但愿能够给各位对本身工做的方式带去必定的思考。不管怎样,仍是须要把底层的知识弄明白,毕竟这对于写代码有帮助。