状态:已实施(Windows / Android / iOS 的实机验证见 §6) 适用范围:
core/src/protocol_handler.h、protocol_handler.cpp、protocol_handler_dispatch.h、core/src/platform/*/protocol_handler_*,以及 leanflutterprotocol_handler插件 (子模块bindings/dart/protocol_handler)的原生部分 核实基准:2026-11-09
ProtocolHandler 处理的是入站 URL:应用自己的 scheme 被系统交给本进程。出站链接是
UrlOpener 的事,两者不共享代码。
系统单例,私有 PIMPL,无资源 ID,继承 EventEmitter<ProtocolHandlerEvent>。
- 初始 URL:启动本进程的那个 URL,
GetInitialUrl()读取,只设置一次。之后到达的每个 URL 都发ProtocolUrlReceivedEvent(GetUrl()),经EmitAsync落在 UI 线程。 - 晚到的启动 URL:系统可能在绑定的启动代码之后才交付启动 URL(macOS 实测:Dart 读
getInitialUrl()比 AppKit 交付 GURL 早约 300 ms)。只要GetInitialUrl()已经回答过 nullopt,晚到的启动 URL 除了成为初始 URL,还会作为事件再发一次,不会丢。 - 无人监听时排队:没有监听器时到达的 URL 进入有界队列(32 条,满了丢最旧的),第一个
监听器加入时(
StartEventListening)按顺序补发。监听器全部移除后再到达的 URL 同样排队。 订阅本身不发初始事件,初始 URL 只能主动读。 Register(scheme)先按 RFC 3986 校验并转小写,再交平台。各平台的含义不同(§3), 但false一律表示「调用后这个 scheme 的 URL 不会交给本应用」。
启动 URL 在大多数绑定加载 core 之前就到了:Flutter 在引擎启动后、Dart 第一次调用时
才加载 cnativeapi;Android 的 intent 和 iOS 的 openURL 交给宿主拥有的 Activity /
delegate。所以 core 提供两条宿主通道,由一段在引擎启动时就运行的原生代码接上:
| 平台 | 宿主做什么 | core 侧 |
|---|---|---|
| macOS | 插件注册时 dlopen cnativeapi.framework 并调 nativeapi_protocol_handler_host_attach() |
构造单例时装 kAEGetURL 处理器;NSApplicationDidFinishLaunchingNotification 之前到达的 GURL 算启动 URL |
| iOS | 同上,再把 launch options / scene connection options 的 URL 以 is_launch=1、openURL / openURLContexts 以 is_launch=0 交给 nativeapi_protocol_handler_host_deliver |
只缓存与派发 |
| Android | Java 类 dev.libnativeapi.nativeapi.ProtocolHandlerBridge 调 nativeAttach(Context) / nativeDeliver(String, boolean) |
Java_dev_libnativeapi_nativeapi_ProtocolHandlerBridge_* 两个 JNI 入口 |
| Windows | 第二个实例在 wWinMain 开头把 URL 转给正在运行的实例后退出 |
读命令行得到启动 URL;消息窗口接收转发 |
宿主约定是稳定 ABI,改名就是破坏性变更:
- C 符号(
protocol_handler_dispatch.h,不进API_HEADERS):void nativeapi_protocol_handler_host_attach(void)、void nativeapi_protocol_handler_host_deliver(const char* url, int is_launch)。 Apple / Android 的默认符号可见性让它们从cnativeapi导出。 - JNI:类名与两个
static native方法签名见上表。 - Windows 转发:找类名为
NativeAPI.ProtocolHandler、标题为接收方完整可执行文件路径(小写) 的 message-only 窗口,发WM_COPYDATA,dwData = 0x4E415055('NAPU'),负载是不带结尾 NUL 的 UTF-8 URL。接收窗口建在拥有窗口、泵 Win32 消息的那个线程上(FindUiThreadId), 并对WM_COPYDATA放开 UIPI 过滤。
Flutter 侧的接入在 leanflutter protocol_handler 0.3(Dart API 与 0.2 相同,底层是本模块)。
其他宿主(原生 C++ 应用)在 macOS 上于应用完成启动前调 ProtocolHandler::GetInstance() 即可,
在移动端自行实现上表的转发。
| 平台 | IsSupported() |
Register() |
URL 来源 |
|---|---|---|---|
| macOS | true | 检查 CFBundleURLTypes 是否声明;声明了就 LSRegisterURL 并返回 true,不改用户的默认处理程序 |
kAEGetURL Apple event |
| Windows | true | 写 HKCU\Software\Classes\<scheme>(URL Protocol、DefaultIcon、shell\open\command = "<exe>" "%1"),SHChangeNotify;MSIX 包内返回 false,由清单的 uap:Protocol 声明 |
命令行中第一个 scheme 至少两个字符的参数(排除 C:\… 路径);WM_COPYDATA |
| Android | 宿主接入后 true | PackageManager.queryIntentActivities 查本包是否有该 scheme 的 VIEW 过滤器 |
宿主转发的 VIEW intent |
| iOS | 宿主接入后 true | 检查 CFBundleURLTypes |
宿主转发的 app / scene delegate 回调 |
| Linux、OpenHarmony、WASM | false | false | 无 |
Android 的 Context 优先取桥接类给的,其次取 NativeActivity 宿主 android::Initialize 的。
C ABI 与六个绑定由 codegen 生成:register、getInitialUrl(可空字符串)与事件监听。
Dart 的 protocol_handler 门面在其上提供 ProtocolListener、listeners / hasListeners、
onUrlReceived 流;流与监听器共用一个原生监听,最后一个取消时退订,之后到达的 URL 回到 core 排队。
- Linux:桌面文件的
MimeType=x-scheme-handler/<scheme>与单实例转发(D-Bus)都未做;protocol_handler原本也不支持 Linux。 - 注销:没有
Unregister。源插件也没有;Windows 的注册表项留在 HKCU。 - Android 冷启动去重:插件以 intent 对象身份和
FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY避免最近任务重开时重放启动 intent;进程被杀后系统恢复 Activity 的情形未验证。
core/tests/protocol_handler_test.cpp:经宿主通道验证初始 URL、晚到启动 URL、32 条队列 溢出与顺序、监听器移除后排队与补发、scheme 校验。protocol_handler的flutter test:监听器的增删与快照、getInitialUrl、非法 scheme。- macOS 实机(
protocol_handler/example,debug,App Sandbox 开):从链接冷启动时初始 URL 正确 且同时收到事件;运行中打开链接收到事件;register对已声明 scheme 返回 true。 - iOS 模拟器、Android 模拟器、Windows:见提交说明;未列出的即未验证。
- 改宿主约定(C 符号、JNI 类、Windows 窗口类 /
dwData)时同步改插件,并在两边的 CHANGELOG 标为破坏性变更。 - 新平台的 URL 来源区分「启动」与「运行中」,并在单例构造时就开始捕获。
- 晚到的启动 URL 不能丢:保持 §1 的「已回答 nullopt 则同时发事件」规则。