文章

移动端性能监控

网络流量监控

  • 哪个 进程 消耗的流量
  • 哪个 线程 消耗的流量
  • 哪个 业务 消耗的流量
  • 消耗流量的具体 调用栈

常见方案对比

  1. TrafficStats/NetworkManagerService
    • Android 提供的 Public API 只能够查询自身应用 整体的流量
    • 无法区分不同进程的流量
  2. tcpdump/libpacap/ebpf
    • 大名鼎鼎的 tcpdump,可以实现功能强大的抓包和分析,而 tcpdump 底层依赖的是 libpcap,而libpcap底层依赖的是ebpf
    • 但是 ebpf 只能在root权限下使用,无法应用于线上环境
  3. Fiddler/Charles
    • 这些工具的本质都是网络代理,在代理层做分析,适用于 本地Debug
    • 无法应用于线上环境
  4. 编译插桩
    • 业界也有一些开源库,通过对常用的一些网络框架,例如HttpUrlConnection以及okhttp,进行编译期插桩,这种方案足够稳定,满足大规模上线的要求
    • 无法 hook native层 的流量消耗
  5. 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到的这些socketfd 包含多种情况:

  • 可能是一般文件的 IO fd
    • 因为 read/write 方法是文件fdsocket 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,就只需查询一次,从而提高性能。

16975304881731697530487229.png

返回值

16975305431651697530542730.png

通过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到各种方法后,仅在当前线程做的一些必要的工作
    • 例如获取线程信息,堆栈信息
  • 并且做好缓存后,就把该条流量消息扔到一个队列中
  • 而有一个异步线程作为消费者,不断消费队列中的流量消息
  • 直到队列为空才停止,从而尽最大程度的降低监控逻辑对原本逻辑的影响

异步队列模型简单的示意图为:

16975312251671697531224967.png

3.2 排除无需hook的so

Android系统许多模块的IPC功能都是用socket实现的,以及因为我们不可避免的hook了read/write这组方法,所以一般文件的IO也会被我们hook到。

所以我们需要对已知的,会有频繁本地IO的,但是又不可能产生网络流量的so加入白名单,Matrix Traffic不去hook这些so,虽然我们做了缓存,但这这会较大程度上减少流量信息的产生,从而减轻异步队列的负担。

比较典型的一些系统so例如:

  • libinput.so - 负责Touch事件
  • libgui - 负责渲染UI
  • libsensor.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 的系统服务完成的。

其中四个比较关键的角色:

  1. 功率power_profile.xml,Android 系统使用此文件来描述设备各个硬件模块的额定功率,包括上面提到的多档位功率和 CPU 电量算需要到的各种参数值。
  2. 时长StopWatch & SamplingCounter
    • StopWatch : 计算 App 各种硬件模块的使用时长
    • SamplingCounter : 采样统计 App 在不同 $CPU Core$ 和不同 $Cpu Freq$ 下的工作时长。
  3. 计算PowerCalculators,每个硬件模块都有一个相应命名的 PowerCalculator 实现,主要是用来完成具体的电量统计算法。
  4. 存储batterystats.bin,电量统计服务相关数据的持久化文件。

BatteryStatsService 工作流程

image-20231023181131681

时长统计流程
  • BatteryStats类 持有一个 Uid [] 数组
    • 每一个 Uid 对应一个 App , 当安装或卸载时会更新这个映射关系
    • StopWatch SamplingCounter

,当我们安装或者卸载 App 的时候,BatteryStats 就会更新相应的 Uid 元素以保持最新的映射关系。同时 BatteryStats 持有一系列的 StopWatch 和 SamplingCounter,当 App 开始使用某些硬件模块的功能时,BatteryStats 就会调用相应 Uid 的 StopWatch 或 SamplingCounter 来统计其硬件使用时长。

本文由作者按照 CC BY 4.0 进行授权