macOS 菜单栏实时网速。原生 Swift / SwiftUI 写的纯菜单栏应用——不占 Dock、没有窗口、没有第三方依赖,编译产物就是一个可以直接双击的 NetSpeed.app。
方式一:下载编译好的应用(推荐)
到 Releases 下载 NetSpeed-1.0.0-macos-arm64.zip,解压后把 NetSpeed.app 拖进「应用程序」。
首次打开会被系统拦下。 本应用是 ad-hoc 签名、未做 Apple 公证(公证需要付费开发者账号),系统会提示「无法验证开发者」或「已损坏,应移到废纸篓」。这不是应用有问题——任何人自己编译出来的 macOS 应用都是这个待遇。两种解决办法:
-
右键点图标 → 选「打开」→ 在弹窗里再点一次「打开」。 只需做一次。
-
或者在终端执行一次,清掉整个包的隔离标记:
xattr -dr com.apple.quarantine /Applications/NetSpeed.app
方式二:自己编译(本机产物没有隔离标记,不会被 Gatekeeper 拦)
git clone https://github.com/SimplifeX/netspeed-macos.git
cd netspeed-macos
./build.sh install # 编译并装到 /Applications构建逻辑就是一条 swiftc 命令,外加 lipo 与 codesign:没有 Makefile、没有 SwiftPM 工程、没有第三方依赖,build.sh 只有 90 行。./build.sh 出 dist/NetSpeed.app,./build.sh run 编译并启动,ARCHS="arm64 x86_64" ./build.sh 出通用二进制(需要 Intel 机器也能跑时)。
环境要求:macOS 13 Ventura 或更新,Apple Silicon(M 系列)。源码构建需要 Xcode Command Line Tools(xcode-select --install)。
菜单栏上默认两行,实时刷新:
↑1.2M
↓12.4M
拆成两行是为了省宽度:同样内容横排要 90pt 上下,两行只要 40pt 上下。面板里「菜单栏排两行」可以关掉,回到 ↓12.4M ↑1.2M 的单行排。
点开是完整面板:
| 区块 | 内容 |
|---|---|
| 顶部 | 当前承载流量的网卡名 + IPv4(点一下复制) |
| 速率 | 上/下行大数字,等宽数字不跳动 |
| 曲线 | 近 60 秒走势 + 窗口内峰值 |
| 会话 | 本次运行的累计接收 / 发送 / 时长 |
| 网卡明细 | 每个网卡的实时速率,主接口打勾 |
| 设置 | 刷新间隔、单位、菜单栏显示项、菜单栏排两行、菜单栏边距、计速网卡、开机自启 |
只在统计系统默认路由所在的那张网卡,不是所有网卡求和——开了 VPN 时 utun0 和 en0 承载同一份流量,求和会翻倍。面板里可以手动锁定某张网卡。
只统计网络接口的原始收发字节,不含进程归属(想看「哪个 App 在占带宽」用活动监视器,面板右下角有直达按钮);上行包含 TCP ACK,所以纯下载时上行不会是 0。
左右留白由应用自己定。 默认 3pt:状态项整块 42pt,实际左空 4pt;面板里「菜单栏边距」可以切回系统标准的 8pt(整块 52pt,左空 9pt)。
| 留白 | 状态项整块 | 实际左空 |
|---|---|---|
| 标准 8pt | 52pt | 9.0pt |
| 窄 3pt(默认) | 42pt | 4.0pt |
做法是不走 SwiftUI 的 MenuBarExtra,应用自己持有 NSStatusItem、自己把两行标签画成一张模板图写进 button.image、自己把宽度定为「图宽 + 左右各 pad」。statusItem.length 没用:把它设成 40pt,菜单栏上实际占 56pt,系统会在内容视图外面再加一圈 8pt;只有压按钮 frame,内容视图才会贴着按钮走。顺带标签刷新也就不再经过 SwiftUI,数值一变只重画一张小位图。
开销的大头不是读网卡,是面板视图被反复重新测量。
菜单栏应用每秒刷新一次数据。如果面板里的视图一直挂着并订阅采样数据,主线程每个 tick 都要沿 NSHostingView.updateConstraints → _sizeThatFits → AttributeGraph 把整棵树重算一遍——面板关着也一样(MenuBarExtra 的 .window 面板就是这样,采样栈里能看到 PopUpButton.body.getter / ForEach / HStack)。「面板没打开就不费电」并不成立。
同一支二进制,只切换「面板内容是否常驻」的 A/B 结果:
| CPU(面板关闭,15 秒窗口,3 轮) | 峰值内存 | |
|---|---|---|
| 面板内容常驻 | 1.18 / 2.00 / 1.95 %(中位 1.95%) | 98 MB |
| 关闭即拆(本应用的做法) | 0.45 / 0.75 / 0.83 %(中位 0.75%) | 81 MB |
三轮中位值之比 2.6×。为此做了三件事:
- 面板关掉就把内容拆了。 用
if visibility.isVisible把面板视图包住,采样与设置对象只在展开时才挂上,关着的时候没有任何视图订阅采样数据。重建一次几毫秒,且只发生在点击时。 - 标签刷新绕开 SwiftUI,并且先比字符串再量字宽。 数值没变的那几秒里 CoreText 一次都不动,文本格式化也不重复算。
- 削减每 tick 的重复系统调用。
sysctl缓冲区复用、SCDynamicStore句柄缓存、地址查询从「每接口一次」改成每 tick 批量一次、地址格式化走inet_ntop而非getnameinfo、定时器回调直接MainActor.assumeIsolated不再每秒分配一个任务。
剩下的 ~0.5% 里约 0.2% 是本底(采样间隔放到 5 秒也还有 0.215%),另外 ~0.3% 是轮询本身:每秒一次 sysctl + getifaddrs 读网卡计数器。网络栈没有「字节数变了」的通知,要再往下压只能改事件驱动,只能保留轮询。
绝对数字跟机器负载强相关,看倍率比看绝对值有意义。应用自带
--cpu [秒数]让进程量自己(走getrusage,不依赖外部工具),--keep-panel-alive --cpu 20复现上表第一行。另有--selftest/--diagnose/--render/--shot等自检开关,./verify.sh把取数链路、状态栏存在、面板版式、双行更窄、占位稳定串起来跑一遍。
MIT © 2026 Simplife


