android网络流量统计

18 阅读10分钟

📊 Android 网络流量统计架构全解析

Android 网络流量统计是一个多层次、高度优化的系统,从内核层到应用层提供了完整的流量监控和统计能力。让我为您详细解析:


🏗️ 整体架构图

┌─────────────────────────────────────────────────────────────┐
│                    应用层 (App Layer)                        │
│  - TrafficStats API (简单查询)                               │
│  - NetworkStatsManager (历史数据、按时间段查询)              │
└──────────────┬──────────────────────────────────────────────┘
               │ Binder IPC
               ↓
┌─────────────────────────────────────────────────────────────┐
│         Framework 层 (System Server)                        │
│  - NetworkStatsService                                      │
│    * 周期性轮询 (Poll)                                      │
│    * 持久化存储 (NetworkStatsCollection)                    │
│    * 流量告警 (Data Usage Alert)                            │
│  - NetworkStatsFactory (数据解析)                           │
└──────────────┬──────────────────────────────────────────────┘
               │ JNI
               ↓
┌─────────────────────────────────────────────────────────────┐
│           Native 层 (libnetworkstats)                       │
│  - BpfNetworkStats.cpp                                      │
│  - 读取 eBPF Map                                            │
│  - 解析统计数据                                              │
└──────────────┬──────────────────────────────────────────────┘
               │ eBPF System Calls
               ↓
┌─────────────────────────────────────────────────────────────┐
│              内核层 (Linux Kernel)                          │
│  ┌────────────────────────────────────────────────────┐    │
│  │  eBPF 程序 (Android 9+, Kernel 4.9+)               │    │
│  │  - cgroupskb/ingress/stats                        │    │
│  │  - cgroupskb/egress/stats                         │    │
│  │  - 挂载点: /sys/fs/cgroup/                        │    │
│  └────────────┬───────────────────────────────────────┘    │
│               │ 写入                                        │
│               ↓                                            │
│  ┌────────────────────────────────────────────────────┐    │
│  │  eBPF Maps (内存中的 Hash Table)                   │    │
│  │  - cookie_tag_map    (Socket → UID/Tag)           │    │
│  │  - uid_stats_map     (UID 流量统计)               │    │
│  │  - app_uid_stats_map (App 流量统计)               │    │
│  │  - iface_stats_map   (网卡流量统计)               │    │
│  │  - tag_stats_map     (Tag 流量统计)               │    │
│  └────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌────────────────────────────────────────────────────┐    │
│  │  老旧设备 (Android 8-, Kernel < 4.9)               │    │
│  │  - xt_qtaguid (Netfilter 模块)                    │    │
│  │  - /proc/net/xt_qtaguid/stats                     │    │
│  └────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

1️⃣ 应用层 API: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.
 * <p>
 * These statistics may not be available on all platforms. If the statistics are
 * not supported by this device, {@link #UNSUPPORTED} will be returned.
 * <p>
 * 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.
 */
public class TrafficStats {
    static {
        System.loadLibrary("framework-connectivity-tiramisu-jni");
    }

    private static final String TAG = TrafficStats.class.getSimpleName();
    /**
     * The return value to indicate that the device does not support the statistic.
     */
    public final static int UNSUPPORTED = -1;

    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
    @Deprecated
    public static final long KB_IN_BYTES = 1024;
    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
    @Deprecated
    public static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
    @Deprecated
    public static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
    @Deprecated
    public static final long TB_IN_BYTES = GB_IN_BYTES * 1024;
    /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */
    @Deprecated
    public static final long PB_IN_BYTES = TB_IN_BYTES * 1024;

    /**
     * Special UID value used when collecting {@link NetworkStatsHistory} for
     * removed applications.
     *
     * @hide
     */
    public static final int UID_REMOVED = -4;

核心方法

    /**
     * Return number of bytes transmitted by the given UID since device boot.
     * Counts packets across all network interfaces, and always increases
     * monotonically since device boot. Statistics are measured at the network
     * layer, so they include both TCP and UDP usage.
     * <p>
     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
     * <p>
     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
     * report traffic statistics for the calling UID. It will return
     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
     * historical network statistics belonging to other UIDs, use
     * {@link NetworkStatsManager}.
     *
     * @see android.os.Process#myUid()
     * @see android.content.pm.ApplicationInfo#uid
     */
    public static long getUidTxBytes(int uid) {
        try {
            return getStatsService().getUidStats(uid, TYPE_TX_BYTES);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return number of bytes received by the given UID since device boot.
     * Counts packets across all network interfaces, and always increases
     * monotonically since device boot. Statistics are measured at the network
     * layer, so they include both TCP and UDP usage.
     * <p>
     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return
     * {@link #UNSUPPORTED} on devices where statistics aren't available.
     * <p>
     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
     * report traffic statistics for the calling UID. It will return
     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
     * historical network statistics belonging to other UIDs, use
     * {@link NetworkStatsManager}.
     *
     * @see android.os.Process#myUid()
     * @see android.content.pm.ApplicationInfo#uid
     */
    public static long getUidRxBytes(int uid) {
        try {
            return getStatsService().getUidStats(uid, TYPE_RX_BYTES);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

示例代码

// 获取当前 App 的流量
long txBytes = TrafficStats.getUidTxBytes(Process.myUid());
long rxBytes = TrafficStats.getUidRxBytes(Process.myUid());

// 获取移动网络总流量
long mobileTx = TrafficStats.getMobileTxBytes();
long mobileRx = TrafficStats.getMobileRxBytes();

// 获取总流量
long totalTx = TrafficStats.getTotalTxBytes();
long totalRx = TrafficStats.getTotalRxBytes();

2️⃣ System Server:NetworkStatsService

核心职责

  1. 周期性轮询(Polling)
  2. 数据持久化存储
  3. 流量告警机制
  4. 多维度统计(接口、UID、Tag、时间段)

轮询机制

    /**
     * Periodic poll operation, reading current statistics and recording into
     * {@link NetworkStatsHistory}.
     */
    @GuardedBy("mStatsLock")
    private void performPollLocked(int flags, @Nullable PollEvent event) {
        if (!mSystemReady) return;
        if (LOGV) Log.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")");
        Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked");

        if (mSupportEventLogger) {
            mEventLogger.logPollEvent(flags, event);
        }

        final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0;
        final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0;
        final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0;

        performPollFromProvidersLocked();

        // TODO: consider marking "untrusted" times in historical stats
        final long currentTime = mClock.millis();

        try {

轮询触发条件

  • 定期轮询:默认 30 分钟
  • 📡 网络状态变化:切换 WiFi/移动网络
  • 🚨 流量告警:达到预设阈值
  • 📊 系统事件:开关机、UID 移除、dumpsys 命令

数据存储结构

/data/system/netstats/
├── netstats_xt.bin        # 接口级别统计 (Interface)
├── netstats_uid.bin       # UID 级别统计
└── netstats_uid_tag.bin   # UID + Tag 统计

3️⃣ 内核层:eBPF vs xt_qtaguid

eBPF(Android 9+,Kernel 4.9+)

优势

  • 性能极高:内核态直接处理,无需上下文切换
  • 实时性好:每个数据包都被精确统计
  • 可扩展性强:通过 Map 轻松扩展统计维度

核心 eBPF 程序

#define TAG_SYSTEM_DNS 0xFFFFFF82
    if (tag == TAG_SYSTEM_DNS && uid == AID_DNS) {
        uid = sock_uid;
        if (match == DROP_UNLESS_DNS) match = PASS;
    } else {
        if (match == DROP_UNLESS_DNS) match = DROP;
    }

    // If an outbound packet is going to be dropped, we do not count that traffic.
    if (egress.egress && (match == DROP)) return DROP;

    StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};

    uint8_t* counterSet = bpf_uid_counterset_map_lookup_elem(&uid);
    if (counterSet) key.counterSet = (uint32_t)*counterSet;

    uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
    uint32_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);

    if (!selectedMap) return PASS;  // cannot happen, needed to keep bpf verifier happy

    do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
    update_stats_with_config(*selectedMap, skb, &key, egress, kver);
    update_app_uid_stats_map(skb, &uid, egress, kver);

    // We've already handled DROP_UNLESS_DNS up above, thus when we reach here the only
    // possible values of match are DROP(0) or PASS(1), however we need to use
    // "match &= 1" before 'return match' to help the kernel's bpf verifier,
    // so that it can be 100% certain that the returned value is always 0 or 1.
    // We use assembly so that it cannot be optimized out by a too smart compiler.
    asm("%0 &= 1" : "+r"(match));
    return match;
}

eBPF Maps 结构

// app_uid_stats_map:   key:  4 bytes, value: 32 bytes, cost: 1062784 bytes    =  1063Kbytes
// uid_stats_map:       key: 16 bytes, value: 32 bytes, cost: 1142848 bytes    =  1143Kbytes
// tag_stats_map:       key: 16 bytes, value: 32 bytes, cost: 1142848 bytes    =  1143Kbytes
// iface_index_name_map:key:  4 bytes, value: 16 bytes, cost:   80896 bytes    =    81Kbytes
// iface_stats_map:     key:  4 bytes, value: 32 bytes, cost:   97024 bytes    =    97Kbytes
// dozable_uid_map:     key:  4 bytes, value:  1 bytes, cost:  145216 bytes    =   145Kbytes
// standby_uid_map:     key:  4 bytes, value:  1 bytes, cost:  145216 bytes    =   145Kbytes
// powersave_uid_map:   key:  4 bytes, value:  1 bytes, cost:  145216 bytes    =   145Kbytes
// packet_trace_ringbuf:key:  0 bytes, value: 24 bytes, cost:   32768 bytes    =    32Kbytes
// total:                                                                         4962Kbytes
// It takes maximum 4.9MB kernel memory space if all maps are full, which requires any devices
// running this module to have a memlock rlimit to be larger then 5MB. In the old qtaguid module,
// we don't have a total limit for data entries but only have limitation of tags each uid can have.
// (default is 1024 in kernel);

// 'static' - otherwise these constants end up in .rodata in the resulting .o post compilation
static const int COOKIE_UID_MAP_SIZE = 10000;
static const int UID_COUNTERSET_MAP_SIZE = 4000;
static const int APP_STATS_MAP_SIZE = 10000;
static const int STATS_MAP_SIZE = 5000;
static const int IFACE_INDEX_NAME_MAP_SIZE = 1000;
static const int IFACE_STATS_MAP_SIZE = 1000;
static const int CONFIGURATION_MAP_SIZE = 2;
static const int UID_OWNER_MAP_SIZE = 4000;
static const int INGRESS_DISCARD_MAP_SIZE = 100;
static const int PACKET_TRACE_BUF_SIZE = 32 * 1024;
static const int DATA_SAVER_ENABLED_MAP_SIZE = 1;

关键 Maps

  • 🔑 cookie_tag_map:Socket Cookie → (UID, Tag) 映射
  • 📊 uid_stats_map:UID 流量统计(RX/TX Bytes & Packets)
  • 🏷️ tag_stats_map:Tag 流量统计(用于 Socket Tagging)
  • 🌐 iface_stats_map:网卡接口流量统计

xt_qtaguid(老旧设备)

文件路径/proc/net/xt_qtaguid/stats

缺点

  • ❌ 性能较差(Netfilter 钩子开销大)
  • ❌ 无法精确统计某些场景(如 eBPF offload)
  • ❌ 已在 Android 9+ 废弃

4️⃣ 特殊机制:Socket Tagging

什么是 Socket Tagging?

允许应用为 Socket 打上自定义标签(Tag),实现更细粒度的流量统计

使用场景

// 在发起网络请求前
TrafficStats.setThreadStatsTag(0xDEADBEEF); // 设置 Tag
try {
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    // 这个连接的流量会被统计到 Tag 0xDEADBEEF
} finally {
    TrafficStats.clearThreadStatsTag(); // 清除 Tag
}

实际应用

  • 📱 多账号 App:区分不同账号的流量
  • 🎮 游戏 App:区分游戏内不同功能的流量(聊天 vs 战斗)
  • 📺 视频 App:区分视频流量 vs 广告流量

内核实现

     * Tag the socket with the specified tag and uid. In the qtaguid module, the
     * first tag request that grab the spinlock of rb_tree can update the tag
     * information first and other request need to wait until it finish. All the
     * tag request will be addressed in the order of they obtaining the spinlock.
     * In the eBPF implementation, the kernel will try to update the eBPF map
     * entry with the tag request. And the hashmap update process is protected by
     * the spinlock initialized with the map. So the behavior of two modules
     * should be the same. No additional lock needed.
     */
    int tagSocket(int sockFd, uint32_t tag, uid_t chargeUid, uid_t realUid);

    /*
     * The untag process is similar to tag socket and both old qtaguid module and
     * new eBPF module have spinlock inside the kernel for concurrent update. No
     * external lock is required.
     */
    int untagSocket(int sockFd);

5️⃣ 464xlat 流量校正

问题背景

在 IPv4-only App 通过 464xlat 使用 IPv6-only 网络时,会出现流量统计偏差:

  • IPv4 包头:20 字节
  • IPv6 包头:40 字节
  • 差异:每个包多 20 字节

校正机制

    /**
     * Calculate and apply adjustments to captured statistics for 464xlat traffic.
     *
     * <p>This mutates stacked traffic stats, to account for IPv4/IPv6 header size difference.
     *
     * <p>UID stats, which are only accounted on the stacked interface, need to be increased
     * by 20 bytes/packet to account for translation overhead.
     *
     * <p>The potential additional overhead of 8 bytes/packet for ip fragments is ignored.
     *
     * <p>Interface stats need to sum traffic on both stacked and base interface because:
     *   - eBPF offloaded packets appear only on the stacked interface
     *   - Non-offloaded ingress packets appear only on the stacked interface
     *     (due to iptables raw PREROUTING drop rules)
     *   - Non-offloaded egress packets appear only on the stacked interface
     *     (due to ignoring traffic from clat daemon by uid match)
     * (and of course the 20 bytes/packet overhead needs to be applied to stacked interface stats)
     *
     * <p>This method will behave fine if {@code stackedIfaces} is an non-synchronized but add-only
     * {@code ConcurrentHashMap}
     * @param baseTraffic Traffic on the base interfaces. Will be mutated.
     * @param stackedTraffic Stats with traffic stacked on top of our ifaces. Will also be mutated.
     * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both.
     * @hide
     */
    public static void apply464xlatAdjustments(NetworkStats baseTraffic,
            NetworkStats stackedTraffic, Map<String, String> stackedIfaces) {
        // For recycling
        Entry entry = null;
        for (int i = 0; i < stackedTraffic.size; i++) {
            entry = stackedTraffic.getValues(i, entry);
            if (entry == null) continue;
            if (entry.iface == null) continue;
            if (!entry.iface.startsWith(CLATD_INTERFACE_PREFIX)) continue;

            // For 464xlat traffic, per uid stats only counts the bytes of the native IPv4 packet
            // sent on the stacked interface with prefix "v4-" and drops the IPv6 header size after
            // unwrapping. To account correctly for on-the-wire traffic, add the 20 additional bytes
            // difference for all packets (http://b/12249687, http:/b/33681750).
            //
            // Note: this doesn't account for LRO/GRO/GSO/TSO (ie. >mtu) traffic correctly, nor
            // does it correctly account for the 8 extra bytes in the IPv6 fragmentation header.
            //
            // While the ebpf code path does try to simulate proper post segmentation packet
            // counts, we have nothing of the sort of xt_qtaguid stats.
            entry.rxBytes += entry.rxPackets * IPV4V6_HEADER_DELTA;
            entry.txBytes += entry.txPackets * IPV4V6_HEADER_DELTA;
            stackedTraffic.setValues(i, entry);
        }
    }

📈 统计维度总结

维度说明API
UID按应用程序统计TrafficStats.getUidTxBytes(uid)
Tag按 Socket 标签统计TrafficStats.setThreadStatsTag()
Interface按网卡接口统计(rmnet0, wlan0 等)TrafficStats.getTxBytes(iface)
Set前台/后台流量分离NetworkStats.SET_FOREGROUND
Network按网络类型(WiFi, 移动数据)NetworkStatsManager.queryDetails()
Time按时间段查询历史数据NetworkStatsManager

🎯 实战场景

1. 数据用量监控 App

NetworkStatsManager statsManager = context.getSystemService(NetworkStatsManager.class);
NetworkStats stats = statsManager.queryDetails(
    ConnectivityManager.TYPE_MOBILE, 
    subscriberId,
    startTime, 
    endTime
);

2. 防火墙 App(限制后台流量)

通过设置 uid_counterset_map,将 UID 标记为 SET_DEFAULTSET_FOREGROUND

3. 流量统计 Widget

实时显示当前 App 的上传/下载速度。


性能优化

  1. eBPF Zero-Copy:内核态数据直接映射到用户态,无需拷贝
  2. 双缓冲机制:读写分离的 Stats Map(Map A / Map B)
  3. 缓存机制:TrafficStats 结果缓存 1 秒,避免频繁 IPC
  4. 增量更新:只记录变化量,减少存储空间

希望这份详尽的解析帮助您深入理解了 Android 网络流量统计的完整架构!从内核 eBPF 到应用层 API,Android 构建了一套高效、准确、灵活的流量监控体系。如果您想深入研究某个具体部分(如 eBPF 程序编写、NetworkStatsService 的持久化机制、或流量告警的实现),随时告诉我!📊🚀