前言
在 Android 开发中合理利用网络不浪费用户流量是每个良心 APP 的目标,收集 APP 的流量使用数据是重要的一环,毕竟没有数据支撑做的优化都是纸上谈兵.一般用户查看流量统计特别都是针对 3G/4G ,会去下载比如 360*** 之类的第三方软件来进行监控,其实 Android 系统提供了一个类给开发者使用,它就是 TrafficStats.
本文的目标是以简单快捷地介绍 TrafficStats 读取本地文件获取应用流量统计的过程,关于其他详细方法和系统服务的相关内容都不做详细解读.
TrafficStats
类注解
/**
* Class that provides network traffic statistics. These statistics include
* bytes transmitted and received and network packets transmitted and received,
* over all interfaces, over the mobile interface, and on a per-UID basis.
*
* These statistics may not be available on all platforms. If the statistics are
* not supported by this device, {@link #UNSUPPORTED} will be returned.
*
* Note that the statistics returned by this class reset and start from zero
* after every reboot. To access more robust historical network statistics data,
* use {@link NetworkStatsManager} instead.
*/
- TrafficStats 提供网络流量统计.这些统计包括字节的上传/接收和数据包的上传/接收,
- 流量统计在 Android2.2 之前是不可用的,如果系统版本过低会返回 UNSUPPORTED (-1).
- 数据统计会在每次手机启动后从零开始算,如果要访问更多详细的网络状态数据就使用 NetworkStatsManager.
TrafficStats 提供的方法有很多,可以通过阅读官方文档了解,我们统计应用流量使用情况通常使用的是 getUidRxBytes().
下面从一个例子开始了解 TrafficStats 如何读取 APP 流量使用情况的,该例子主要通过调用 TrafficStats.getUidRxBytes() 根据应用的 Uid 获取该应用接收字节数.
long uidRxBytes = TrafficStats.getUidRxBytes(android.os.Process.myUid());
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("uidRxBytes", "当前进程流量:"+ (TrafficStats.getUidRxBytes(
android.os.Process.myUid()) - uidRxBytes));
}
getUidRxBytes()
统计的字节包括 TCP 和 UDP.
// TrafficStats.java
public static long getUidRxBytes(int uid) {
// This isn't actually enforcing any security; it just returns the
// unsupported value. The real filtering is done at the kernel level.
// 获取当前调用所在进程的 UID
final int callingUid = android.os.Process.myUid();
if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
try {
return getStatsService().getUidStats(uid, TYPE_RX_BYTES);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
} else {
return UNSUPPORTED;
}
}
// NOTE: keep these in sync with android_net_TrafficStats.cpp
private static final int TYPE_RX_BYTES = 0;
private static final int TYPE_RX_PACKETS = 1;
private static final int TYPE_TX_BYTES = 2;
private static final int TYPE_TX_PACKETS = 3;
private static final int TYPE_TCP_RX_PACKETS = 4;
private static final int TYPE_TCP_TX_PACKETS = 5;
- 首先调用 getStatsService() 获取 INetworkStatsService 实现类的实例.
- 然后调用 INetworkStatsService.getUidStats() 获取接收的字节数.
注意 getUidStats() 中参数 TYPE_RX_BYTES 表示获取的统计数据类型,具体类型根据字节和数据包分为 6 个如上面代码所示,根据注释这些类型的值要与 android_net_TrafficStats.cpp 即与 C++ 层同步.
首先看 getStatsService().
getStatsService()
// TrafficStats.java
private synchronized static INetworkStatsService getStatsService() {
if (sStatsService == null) {
sStatsService = INetworkStatsService.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
}
return sStatsService;
}
很明显这里是通过 AIDL 的方式跨进程与系统服务 NetworkStatsService 进行通信,下面看下 AIDL 文件的定义,下面截取部分 AIDL 常用方法.
INetworkStatsService.aidl
// INetworkStatsService.aidl
// /frameworks/base/core/java/android/net/INetworkStatsService.aidl
interface INetworkStatsService {
// ...
long getUidStats(int uid, int type);
long getIfaceStats(String iface, int type);
long getTotalStats(int type);
}
接着我们查看系统服务 NetworkStatsService.
NetworkStatsService
NetworkStatsService 源码路径为 /frameworks/base/services/core/java/com/android/server/net/NetworkStatsService.java
下面截取 getUidStats() 相关代码.
// NetworkStatsService.java
public class NetworkStatsService extends INetworkStatsService.Stub {
private final boolean mUseBpfTrafficStats;
@VisibleForTesting
NetworkStatsService(Context context, INetworkManagementService networkManager,
AlarmManager alarmManager, PowerManager.WakeLock wakeLock, Clock clock,
TelephonyManager teleManager, NetworkStatsSettings settings,
NetworkStatsObservers statsObservers, File systemDir, File baseDir) {
// ...
mUseBpfTrafficStats = new File("/sys/fs/bpf/traffic_uid_stats_map").exists();
// ...
}
// ...
@Override
public long getUidStats(int uid, int type) {
return nativeGetUidStat(uid, type, checkBpfStatsEnable());
}
private boolean checkBpfStatsEnable() {
return mUseBpfTrafficStats;
}
// ...
private static native long nativeGetUidStat(int uid, int type, boolean useBpfStats);
}
NetworkStatsService.getUidStats() 内部调用了本地方法 nativeGetUidStat().对应的类是 com_android_server_net_NetworkStatsService.cpp ,源码路径是
/frameworks/base/services/core/jni/com_android_server_net_NetworkStatsService.cpp
注意到调用 nativeGetUidStat() 时第二个参数是 checkBpfStatsEnable() 返回的 boolean 值.该方法的返回值是 mUseBpfTrafficStats ,且 mUseBpfTrafficStats 是在 NetworkStatsService 初始化的时候通过判断文件 /sys/fs/bpf/traffic_uid_stats_map 是否存在来初始化的.这个参数与 eBPF 流量监控有关,可以在 Android 开源项目 AOSP 官方文档eBPF 流量监控了解.
com_android_server_net_NetworkStatsService.cpp
首先看常量 QTAGUID_UID_STATS,由于我们是根据 Uid 查看流量的,所以本地方法会通过解析在系统路径 /proc/net/xt_qtaguid/ 下的 stats 文件来读取流量.
static const char* QTAGUID_UID_STATS = "/proc/net/xt_qtaguid/stats";
接着看 NetworkStatsService 本地方法 nativeGetUidStat 在 com_android_server_net_NetworkStatsService.cpp 中对应方法 getUidStat()
getUidStat()
// com_android_server_net_NetworkStatsService.cpp
static jlong getUidStat(JNIEnv* env, jclass clazz, jint uid, jint type, jboolean useBpfStats) {
struct Stats stats;
// 初始化 stats
memset(&stats, 0, sizeof(Stats));
if (useBpfStats) {
if (bpfGetUidStats(uid, &stats) == 0) {
return getStatsType(&stats, (StatsType) type);
} else {
return UNKNOWN;
}
}
if (parseUidStats(uid, &stats) == 0) {
return getStatsType(&stats, (StatsType) type);
} else {
return UNKNOWN;
}
}
getUidStat() 首先根据 useBpfStats 决定调用 bpfGetUidStats() 还是 parseUidStats() 来获取流量统计数据并写入结构体 Stats 中,关于 BPF 的内容本文暂不关注,首先看结构体 Stats 的内容然后看 parseUidStats().
结构体 Stats
结构体 Stats 在头文件 BpfUtils 中, BpfUtils 在源码路径为
/system/netd/libbpf/include/bpf/BpfUtils.h
// BpfUtils.h
struct Stats {
uint64_t rxBytes;
uint64_t rxPackets;
uint64_t txBytes;
uint64_t txPackets;
uint64_t tcpRxPackets;
uint64_t tcpTxPackets;
};
parseUidStats()
// com_android_server_net_NetworkStatsService.cpp
static int parseUidStats(const uint32_t uid, struct Stats* stats) {
// proc/net/xt_qtaguid/stats
FILE *fp = fopen(QTAGUID_UID_STATS, "r");
if (fp == NULL) {
return -1;
}
char buffer[384];
char iface[32];
uint32_t idx, cur_uid, set;
uint64_t tag, rxBytes, rxPackets, txBytes, txPackets;
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
if (sscanf(buffer,
"%" SCNu32 " %31s 0x%" SCNx64 " %u %u %" SCNu64 " %" SCNu64
" %" SCNu64 " %" SCNu64 "",
&idx, iface, &tag, &cur_uid, &set, &rxBytes, &rxPackets,
&txBytes, &txPackets) == 9) {
if (uid == cur_uid && tag == 0L) {
stats->rxBytes += rxBytes;
stats->rxPackets += rxPackets;
stats->txBytes += txBytes;
stats->txPackets += txPackets;
}
}
}
if (fclose(fp) != 0) {
return -1;
}
return 0;
}
- 首先通过 fopen 打开文件 /proc/net/xt_qtaguid/stats 获取 FILE 指针 *fp
- 然后从 *fp 中读取数据放到 buffer 中
- 调用 sscanf 读取 buffer 中的数据并写入结构体 stats
- 最后调用 fclose 关闭读取文件 stats 的流
我们先来看下 stats 文件的数据结构:
- idx : 序号
- iface : 代表流量类型(rmnet表示2G/3G, wlan表示Wifi流量,lo表示本地流量)
- acct_tag_hex :线程标记(用于区分单个应用内不同模块/线程的流量)
- uid_tag_int : 应用uid,据此判断是否是某应用统计的流量数据
- cnt_set : 应用前后标志位:1:前台, 0:后台
- rx_btyes : receive bytes 接受到的字节数
- rx_packets : 接收到的任务包数
- tx_bytes : transmit bytes 发送的总字节数
- tx_packets : 发送的总包数
- rx_tcp_types : 接收到的tcp字节数
- rx_tcp_packets : 接收到的tcp包数
- rx_udp_bytes : 接收到的udp字节数
- rx_udp_packets : 接收到的udp包数
- rx_other_bytes : 接收到的其他类型字节数
- rx_other_packets : 接收到的其他类型包数
- tx_tcp_bytes : 发送的tcp字节数
- tx_tcp_packets : 发送的tcp包数
- tx_udp_bytes : 发送的udp字节数
- tx_udp_packets : 发送的udp包数
- tx_other_bytes : 发送的其他类型字节数
- tx_other_packets : 发送的其他类型包数
文件 stats 记录的数据类型还是挺多的,但从刚才第二步我们看到 parseUidStats() 只读前面九个类型,下面我用真机测试了一下并打印出 stats 文件并只截取我的测试 APP 即 UID 为 10279 的部分数据.
86 wlan0 0x0 10279 0 111013243 96471 4123723 64450 111007355 96447 5888 24 0 0 4119051 64426 4672 24 0 0
87 wlan0 0x0 10279 1 925202712 785236 32010086 529023 925186713 785149 13021 53 2978 34 31956732 528426 53354 597 0 0
92 wlan0 0xa00400000000 10279 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
93 wlan0 0xa00400000000 10279 1 1366 10 2264 8 1366 10 0 0 0 0 2264 8 0 0 0 0
94 wlan0 0xa00500000000 10279 0 96009 791 100452 798 96009 791 0 0 0 0 100452 798 0 0 0 0
95 wlan0 0xa00500000000 10279 1 217521 1791 228940 1828 217521 1791 0 0 0 0 228940 1828 0 0 0 0
214 lo 0x0 10279 0 4136 47 0 0 0 0 0 0 4136 47 0 0 0 0 0 0
215 lo 0x0 10279 1 3908 44 156 2 0 0 0 0 3908 44 0 0 156 2 0 0
218 lo 0xa00500000000 10279 0 88 1 0 0 0 0 0 0 88 1 0 0 0 0 0 0
219 lo 0xa00500000000 10279 1 352 4 0 0 0 0 0 0 352 4 0 0 0 0 0 0
从 stats 的数据结构可以看出一个 UID 记录流量区分主要通过流量类型iface
、线程标记acct_tag_hex
、前台或后台cnt_set
来区分,其中 acct_tag_hex 为 0x0 时是记录该应用从上次开机以来所有接收的流量字节数.由于 parseUidStats() 是不需要区分前后端或者流量类型的,所以直接通过累加的方式把 acct_tag_hex 为 0x0 的所有数据都对应添加到结构体 stats 中即可.
现在阅读完 parseUidStats() 回到 getUidStat() 中.把数据写到结构体 Stats 后调用 getStatsType().
getStatsType()
// com_android_server_net_NetworkStatsService.cpp
static uint64_t getStatsType(struct Stats* stats, StatsType type) {
switch (type) {
case RX_BYTES:
return stats->rxBytes;
case RX_PACKETS:
return stats->rxPackets;
case TX_BYTES:
return stats->txBytes;
case TX_PACKETS:
return stats->txPackets;
case TCP_RX_PACKETS:
return stats->tcpRxPackets;
case TCP_TX_PACKETS:
return stats->tcpTxPackets;
default:
return UNKNOWN;
}
}
enum StatsType {
RX_BYTES = 0,
RX_PACKETS = 1,
TX_BYTES = 2,
TX_PACKETS = 3,
TCP_RX_PACKETS = 4,
TCP_TX_PACKETS = 5
};
getStatsType() 跟简单就是根据枚举 StatsType 的类型返回对应的值, StatsType 对应的就是上文 TrafficStats 的类型.
到这里我们简单了解 TrafficStats 如何读取系统文件得到应用的流量统计,对我们实现或者优化我们自己的流量监控机制有一个基本的概念.
参考资料
Android 流量优化(一):模块化流量统计
Android流量统计