# 统计的收集 · 跨线程汇集成一份报告

> getStats 这一次调用看着简单，背后却要从多条线程上的多个子系统各取一部分指标、合并成一份报告，还要缓存以防频繁调用。这一章读 rtc_stats_collector.h，讲清 RTCStatsCollector 的异步收集、分线程产出、合并与缓存机制。

- 作者：David（道雾轩）
- 专栏：音视频与 WebRTC 详解（https://daiw.org/manual/webrtc.md）
- 最后更新：2026-07-15
- 原文：https://daiw.org/manual/webrtc/stats-collection
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 统计的收集 · 跨线程汇集成一份报告

[上一章](https://daiw.org/manual/webrtc/stats-report)看了 [`RTCStatsReport` 的结构](https://daiw.org/manual/webrtc/stats-report)。但这份报告**从哪来**？`getStats()` 看着是一次简单调用，背后却是一次**跨多线程、从多个子系统取数、合并、缓存**的复杂协作。这一章看 `RTCStatsCollector` 怎么干这活——它也把[第 10 部分的线程模型](https://daiw.org/manual/webrtc/synchronization)照进了现实。

## 为什么收集是异步的

先看接口（真实源码 `rtc_stats_collector.h`）：

```cpp
// rtc_stats_collector.h(节选,真实源码)
class RTCStatsCollector {
  // 注意:不是"返回报告",而是"传入回调,报告好了再回调你"
  void GetStatsReport(scoped_refptr<RTCStatsCollectorCallback> callback);
  // ... Produce*_s 系列:分别产出各类 stats
};
```

<Callout type="info">
  **`GetStatsReport` 是**异步回调**式的（传 callback、不同步返回），因为一份完整报告的数据**散落在不同线程上**——必须去各条线程“[取回属于它的那部分](https://daiw.org/manual/webrtc/synchronization)”，没法在调用线程当场凑齐。** 回想[第 10 部分的铁律](https://daiw.org/manual/webrtc/synchronization)：[每份数据归属某条线程，只能在那条线程访问](https://daiw.org/manual/webrtc/thread-and-taskqueue)。而一份 stats 报告要的数据偏偏来自[三条线程](https://daiw.org/manual/webrtc/threading-model)：

  - [ICE candidate / candidate-pair / transport 指标](https://daiw.org/manual/webrtc/stats-report) —— 归 [network 线程](https://daiw.org/manual/webrtc/threading-model)。
  - [收发流的编解码 / 帧率 / 码率指标](https://daiw.org/manual/webrtc/stats-report) —— 归 [worker 线程](https://daiw.org/manual/webrtc/call-object)（[`Call` 那边](https://daiw.org/manual/webrtc/call-object)）。
  - [数据通道 / PeerConnection 级信息](https://daiw.org/manual/webrtc/data-channel-controller) —— 归 [signaling 线程](https://daiw.org/manual/webrtc/peer-connection-object)。

  于是收集必然是“[分头去各线程取、再合并](https://daiw.org/manual/webrtc/thread-and-taskqueue)”的异步过程——这就是它回调式、而非同步返回的原因。**API 的异步形状，是底层线程模型的直接后果**（[又一个“地基塑造上层”的例子](https://daiw.org/manual/webrtc/rtc-base-recap)）。
</Callout>

## 分线程产出、再合并

读源码会看到一系列 `Produce*_s` 方法（`_s` = signaling 线程那部分），各管一类 stats：

```cpp
// rtc_stats_collector.h(节选,真实源码)
void ProduceCertificateStats_s(...);        // 证书
void ProduceDataChannelStats_s(...);        // 数据通道
void ProduceIceCandidateAndPairStats_s(...);// ICE 候选与路径
void ProduceMediaSourceStats_s(...);        // 媒体源
void ProducePeerConnectionStats_s(...);     // PC 级
void ProduceRTPStreamStats_s(...);          // 收发 RTP 流
// ...以及在 network / worker 线程上收集的对应部分
```

收集流程大致是：

```mermaid
flowchart TD
    APP["应用调 getStats(callback)"] --> COL["RTCStatsCollector"]
    COL -->|"派发到 network 线程"| NET["取 ICE/transport 指标"]
    COL -->|"派发到 worker 线程"| WK["取 Call 的收发流/编解码指标"]
    COL -->|"signaling 线程本地"| SIG["取 PC/DataChannel/证书指标"]
    NET --> MERGE["合并成一份 RTCStatsReport"]
    WK --> MERGE
    SIG --> MERGE
    MERGE -->|"回调"| APP
```

各线程各自产出“[自己那部分 stats](https://daiw.org/manual/webrtc/synchronization)”（[在数据的归属线程上安全地读](https://daiw.org/manual/webrtc/thread-and-taskqueue)），再[汇总](https://daiw.org/manual/webrtc/scoped-refptr)成一份完整的 [`RTCStatsReport`](https://daiw.org/manual/webrtc/stats-report)，最后回调给应用。**这正是[第 10 部分“数据归属线程 + `PostTask` 跨线程”](https://daiw.org/manual/webrtc/thread-and-taskqueue)的一次完整实战**：不加锁，而是把收集任务派发到每份数据所在的线程。

## 缓存：防止频繁调用压垮系统

一个重要细节：报告会**缓存一小段时间**（真实源码注释 `reports are cached for cache_lifetime_ ms`）：

<Callout type="info">
  **`getStats()` 若被频繁调用（如仪表盘每秒刷新、甚至更密），每次都[跨三条线程重新收集](https://daiw.org/manual/webrtc/stats-collection)会很贵——所以 `RTCStatsCollector` 把刚产出的报告**缓存 `cache_lifetime_` 毫秒**，期间再调用直接返回缓存，不重新收集。** 这是个经典的“[用缓存换开销](https://daiw.org/manual/webrtc/rate-statistics)”权衡：stats 是**监控**用途，几十毫秒的陈旧完全可接受，但省下的[跨线程收集开销](https://daiw.org/manual/webrtc/thread-and-taskqueue)很可观。缓存过期后的下一次调用才真正重新收集、刷新缓存。所以你若每秒调好几次 `getStats`，大部分调用其实吃的是缓存——这是**有意的性能设计**，不是 bug。需要“保证新鲜”的场景，源码另有[显式刷新的路径](https://daiw.org/manual/webrtc/stats-collection)。
</Callout>

## 和第 1 部分的 StatsCollector 呼应

[第 1 部分介绍 `PeerConnection` 组件时](https://daiw.org/manual/webrtc/stats-collector)，`RTCStatsCollector` 作为其中一员出过场——那时说它“[负责 getStats](https://daiw.org/manual/webrtc/stats-collector)”。这一章补齐了细节：它不是简单地读几个变量，而是一个**跨线程的收集协调器**，把散落三处的指标[安全汇集](https://daiw.org/manual/webrtc/synchronization)成 [W3C 标准报告](https://daiw.org/manual/webrtc/stats-report)。它是[控制面组件](https://daiw.org/manual/webrtc/peer-connection-object)，但要伸手到[媒体面（worker）和网络面（network）](https://daiw.org/manual/webrtc/threading-model)取数——因此天然是异步的。

## 小结与下一章

`getStats()` 背后是 **`RTCStatsCollector`** 的一次跨线程收集：它**异步回调**式工作（`GetStatsReport(callback)`），因为报告数据[散落在 network / worker / signaling 三线程](https://daiw.org/manual/webrtc/threading-model)，必须[各去归属线程取回、再合并](https://daiw.org/manual/webrtc/thread-and-taskqueue)——一系列 `Produce*_s` 方法分头产出各类 stats，汇总成一份 [`RTCStatsReport`](https://daiw.org/manual/webrtc/stats-report)。这是[第 10 部分“数据归属线程 + `PostTask`”纪律](https://daiw.org/manual/webrtc/synchronization)的实战，也解释了 [getStats 为何是异步 API](https://daiw.org/manual/webrtc/observability-overview)。报告**缓存 `cache_lifetime_` 毫秒**，避免频繁调用反复触发昂贵收集。

快照支柱看完了。下一章转向另一支柱——**黑匣子 `RtcEventLog`**。👉 [RtcEventLog 事件日志](https://daiw.org/manual/webrtc/rtc-event-log)。

## 参考资料

- [1] libwebrtc 源码.`pc/rtc_stats_collector.h` / `.cc`（`GetStatsReport`、`Produce*_s`、缓存）。
- [2] 关联[第 1 部分统计收集器](https://daiw.org/manual/webrtc/stats-collector)、[第 10 部分线程模型](https://daiw.org/manual/webrtc/synchronization)、[RTCStatsReport](https://daiw.org/manual/webrtc/stats-report)。
- [3] W3C getStats 标准。
