📊 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
核心职责
- 周期性轮询(Polling)
- 数据持久化存储
- 流量告警机制
- 多维度统计(接口、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_DEFAULT 或 SET_FOREGROUND。
3. 流量统计 Widget
实时显示当前 App 的上传/下载速度。
⚡ 性能优化
- eBPF Zero-Copy:内核态数据直接映射到用户态,无需拷贝
- 双缓冲机制:读写分离的 Stats Map(Map A / Map B)
- 缓存机制:TrafficStats 结果缓存 1 秒,避免频繁 IPC
- 增量更新:只记录变化量,减少存储空间
希望这份详尽的解析帮助您深入理解了 Android 网络流量统计的完整架构!从内核 eBPF 到应用层 API,Android 构建了一套高效、准确、灵活的流量监控体系。如果您想深入研究某个具体部分(如 eBPF 程序编写、NetworkStatsService 的持久化机制、或流量告警的实现),随时告诉我!📊🚀