Android 数据流量监控

Posted by CoXier on June 26, 2017

一、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 注意

无论是第一种思路还是第二种思路都需要注意:记录流量的文件会在手机重启后清零。

三、参考