简单了解 Android 流量统计之 TrafficStats

前言

在 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流量统计

你可能感兴趣的:(简单了解 Android 流量统计之 TrafficStats)