移动端性能监控
网络流量监控
- 哪个 进程 消耗的流量
- 哪个 线程 消耗的流量
- 哪个 业务 消耗的流量
- 消耗流量的具体 调用栈
常见方案对比
- TrafficStats/NetworkManagerService:
- Android 提供的 Public API 只能够查询自身应用 整体的流量
- 无法区分不同进程的流量
- tcpdump/libpacap/ebpf:
- 大名鼎鼎的
tcpdump
,可以实现功能强大的抓包和分析,而tcpdump
底层依赖的是libpcap
,而libpcap
底层依赖的是ebpf
- 但是
ebpf
只能在root权限下使用,无法应用于线上环境
- 大名鼎鼎的
- Fiddler/Charles:
- 这些工具的本质都是网络代理,在代理层做分析,适用于 本地Debug
- 无法应用于线上环境
- 编译插桩:
- 业界也有一些开源库,通过对常用的一些网络框架,例如
HttpUrlConnection
以及okhttp
,进行编译期插桩,这种方案足够稳定,满足大规模上线的要求 - 无法 hook native层 的流量消耗
- 业界也有一些开源库,通过对常用的一些网络框架,例如
- Java Socket setSocketImplFactory:
- 通过
Socket.setSocketImplFactory
可以代理Java
层的Socket
- 无法触及到 native层 的网络请求。
- 通过
Matrix Traffic
1. Hook Socket
在Android上,无论是Java层还是Native层,最后进行网络通讯的必经之路就是socket,只要能监控到所有socket的流量,理论上我们就能监控到所有的流量,并且能够区分出进程、线程、甚至是直接拿到调用栈。
Matrix的 PLT hook 已经用在了非常多监控工具上,自然就想到了使用 PLT hook 来 hook socket 的方法。
可能涉及到的socket的主要几个方法包括:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//构造方法
int socket(int domain, int type, int protocol);
//TCP链接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//下行流量
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
struct sockaddr *restrict src_addr,
socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//上行流量
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//关闭fd
int close(int fd);
另外需要注意的是,socket不止能使用socket的方法进行网络通讯,同样可以使用更一般的IO方法来进行通讯,所以我们进一步还需要hook一般fd进行IO的read/write
方法以及一般文件的 fclose
方法:
1
2
3
4
5
//下行流量
ssize_t read(int fd, void *buf, size_t count);
//上行流量
ssize_t write(int fd, const void *buf, size_t count);
int fclose(FILE *stream); //关闭fd
通过统计这些 IO 函数的返回值就可以分析上下行流量。
2. 区分 Socket 类型
在 步骤1 hook到的这些socket
的fd
包含多种情况:
- 可能是一般文件的 IO fd
- 因为
read/write
方法是文件fd
和socket fd
都可以使用的,而 文件fd
的 IO 显然不能计入流量
- 因为
- 属于 网络 fd,但可能是本地 socket
- Android系统中就有很多的IPC场景,就是通过本地socket实现的,例如使用最为广泛的touch事件,就是通过一对本地socket在应用进程和server进程中传输数据的;再例如,vsync信号等等其他许多非常重要的IPC功能,也都是通过本地socket实现的。
socket的构造方法
第二个参数type如果是AF_LOCAL或者AF_UNIX就表示该socket是本地socket
>> 存在监控工具的初始化问题
getsockname
getsockname
方法效率并不高,频繁调用可能带来性能问题,可以维护一个缓存,同一个
fd
只要没有被close,就只需查询一次,从而提高性能。
返回值
通过getsockname
方法,
- 判别
fd
是不是socket fd
:如果返回值是-1,并且错误值为ENOTSOCK
,则这个fd
不是一个socket
: - 获取到该
fd
绑定的sockaddr
,而通过sockaddr
中的sa_family
我们就能获取到一个fd
是否是网络socket
1
2
3
4
5
6
7
8
9
10
11
12
// 判断fd是否是网络socket
int isNetworkSocketFd(int fd) {
struct sockaddr addr;
socklen_t len= sizeof(addr);
int getSockNameRet = getsockname(fd, (struct sockaddr*) &addr, &);
if (getSockNameRet == 0) {
if (addr.sa_family != AF_LOCAL && addr.sa_family != AF_NETLINK) {
return 1;
}
}
return 0;
}
3. 性能优化
- socket 的 IO方法的执行频率高、性能敏感 » 卡顿 、ANR
- 数据统计的及时性要求不高
3.1 异步队列模型 - 生产者与消费者模式
- hook到各种方法后,仅在当前线程做的一些必要的工作
- 例如获取线程信息,堆栈信息
- 并且做好缓存后,就把该条流量消息扔到一个队列中
- 而有一个异步线程作为消费者,不断消费队列中的流量消息
- 直到队列为空才停止,从而尽最大程度的降低监控逻辑对原本逻辑的影响
异步队列模型简单的示意图为:
3.2 排除无需hook的so
Android系统许多模块的IPC功能都是用socket实现的,以及因为我们不可避免的hook了read/write这组方法,所以一般文件的IO也会被我们hook到。
所以我们需要对已知的,会有频繁本地IO的,但是又不可能产生网络流量的so加入白名单,Matrix Traffic不去hook这些so,虽然我们做了缓存,但这这会较大程度上减少流量信息的产生,从而减轻异步队列的负担。
比较典型的一些系统so例如:
libinput.so
- 负责Touch事件libgui
- 负责渲染UIlibsensor.so
- 负责传感器
Matrix Traffic默认已经对相当多的系统so进行了加白。 >> URL
当然,不止这些系统so,我们的应用也可能使用很多非系统的so,可以通过 addIgnoreSoFile
函数来忽略指定的so。
3.3 获取堆栈的优化
由于 3.1的设计模式,需要缓存一些堆栈信息,而把所有的堆栈信息全部保存成字符串,会带来较大的内存开销:
- 对于
Java
StackTrace
,使用 hash key来合并相同的堆栈 - 对于Native BackTrace,我们只保存backtrace的对象的指针,只当我们需要获取Native BackTrace时,再根据backtrace对象解成字符串。
耗电量监控
App 电量统计原理
电量计算公式
电量 = 功率 × 时间
其中需要注意一点的是, 功率 = 电压 × 电流。
而在数码产品中,元器件一般对电流比较敏感,而电压基本是恒定的,所以我们直接使用电流来代替功率,这也是我们经常说 “毫安时”($mA/h$)而不说 “千瓦时 / 度”($kW/h$)的原因。
Android 硬件模块的电量统计方式
App 电量 = SUM (硬件模块功率 × 模块时间)
>> 从 face-book Mistrusting OS Level Battery Levels 的说法来看,系统层面的电池报告也是基于估算的
Android 系统电量统计服务
Android 系统的电量统计工作,是由一个叫 BatteryStatsService
的系统服务完成的。
其中四个比较关键的角色:
- 功率:
power_profile.xml
,Android 系统使用此文件来描述设备各个硬件模块的额定功率,包括上面提到的多档位功率和 CPU 电量算需要到的各种参数值。 - 时长:
StopWatch
&SamplingCounter
,StopWatch
: 计算 App 各种硬件模块的使用时长SamplingCounter
: 采样统计 App 在不同 $CPU Core$ 和不同 $Cpu Freq$ 下的工作时长。
- 计算:
PowerCalculators
,每个硬件模块都有一个相应命名的PowerCalculator
实现,主要是用来完成具体的电量统计算法。 - 存储:
batterystats.bin
,电量统计服务相关数据的持久化文件。
BatteryStatsService 工作流程
时长统计流程
BatteryStats
类 持有一个Uid []
数组- 每一个 Uid 对应一个 App , 当安装或卸载时会更新这个映射关系
StopWatch
SamplingCounter
,当我们安装或者卸载 App 的时候,BatteryStats 就会更新相应的 Uid 元素以保持最新的映射关系。同时 BatteryStats 持有一系列的 StopWatch 和 SamplingCounter,当 App 开始使用某些硬件模块的功能时,BatteryStats 就会调用相应 Uid 的 StopWatch 或 SamplingCounter 来统计其硬件使用时长。