一、Android 数据流量监控的发展
在 Android 4.3 以前,系统是通过读取 /proc/uid_stat/{uid}
文件来获取统计数据,在 Android 4.3 之后,被 /proc/net/xt_qtaguid/stats
取代。
但在 Android 4.3 之前,/proc/net/xt_qtaguid/stats
也是存在的,在 Android 3.0 的内核中,有一个专门的模块(kernel/net/netfilter/xt_qtaguid)来维护这个文件。在 4.3 之后部分rom (小米 6,Android 7.1.1)没有保留 /proc/uid_stat/
文件夹。
二、实现流量统计
统计数据流量可以通过三个方法:
- 调用系统API TrafficStats
- 手动读取流量统计文件
- Tcpdump抓包 + wireshark分析
第三种方法需要 root,不做讨论。
2.1 系统 API TrafficStats
TrafficStats 一层提供了如下方法:
- getMobileTxBytes(): 发送的数据流量
- getMoibleRxBytes(): 接收的数据流量
- getUidRxBytes(int uid) : 通过 uid 获取发送的流量(包含数据流量和 Wi-Fi 流量)
- getUidTxBytes(int did) : 通过 uid 获取发出的流量(包含数据流量和 Wi-Fi 流量)
四个方法并不能直接计算出一个单独的应用使用了多少流量。但是可以间接计算:
- 1.用户打开应用
- 2.动态注册 监听「网络状态」 broadreceiver,当监听到用户开启或关闭流量后跳转到 4
- 3.判断当前用户是否正在使用数据,若是进行 4
- 4.当用户使用移动流量时,选择适当的时间间隔统计 :A 时间下的 和 B 时间下的 getUidRxBytes(uid) + getUidTxBytes(uid) 两个的差值
getUidRxBytes(uid)
源码:
public static long getUidTxBytes(int uid) {
// This isn't actually enforcing any security; it just returns the
// unsupported value. The real filtering is done at the kernel level.
final int callingUid = android.os.Process.myUid();
if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
return nativeGetUidStat(uid, TYPE_TX_BYTES);
} else {
return UNSUPPORTED;
}
}
private static native long nativeGetUidStat(int uid, int type);
在 Java 层会检查当前应用的 uid 和传入的参数是否相等(不讨论系统 uid),如果相等才会调用相应的 native 方法:nativeGetUidStat。但是在 内核层 才是真正的检查,为什么要这样呢?因为在 Java 层很容易通过 反射 拿到 nativeGetUidStat 方法,这显然不符合系统安全性设计。
2.2 手动读取流量统计文件
下面要用到 uid,可通过 adb 快速获取:
adb shell
ps | grep "your.app.package.name"
结果如下:
u0_a134 2115 216 997764 64172 sys_epoll_ b6cb6894 S your.app.package.name
uid = 134 + 10000
也可通过 PackageManager 获取:
PackageManager manager = getPackageManager();
ApplicationInfo info = manager.getApplicationInfo("your.app.pacakge.name",0);
int uid = info.uid;
读取 /proc/net/xt_qtaguid/stats
的数据,通过 adb 查看:
adb shell
cat /proc/net/xt_qtaguid/stats | grep -E "rmnet.*10134"
// idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
22 rmnet0 0x0 10134 0 4466 18 2819 25 4466 18 0 0 0 0 2819 25 0 0 0 0
23 rmnet0 0x0 10134 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet
是移动端口。
我们需要的是第6、8列 ` rx_bytes
tx_bytes` ,解析出来这两列数据即可,同一个 Uid 也就是一个 app 会有多个进程信息。手动读取该文件:
File statsFile = new File("/proc/net/xt_qtaguid/stats");
try {
BufferedReader reader = new BufferedReader(new FileReader(statsFile));
String line;
while ((line = reader.readLine()) != null) {
Log.d(TAG, line);
}
} catch (IOException e) {
Log.e(TAG, "error : " + e);
}
2.1 提及的「系统安全性策略」在这里也有体现:我们只能读到该文件的第一行(头部)以及和本应用(相同uid)有关的行。因为我们读取的虚拟文件,并不是真实的文件。
2.3 注意
无论是第一种思路还是第二种思路都需要注意:记录流量的文件会在手机重启后清零。