AppManager 与 Manager trait
主线位置:三条主线共用的那个对象。启动主线上
Builder::build建出它;调用主线上Webview::on_message从它取 invoke key、ACL 和命令处理函数;事件主线上AppManager::emit是起点。本页讲它里面有什么、谁拿着它、Managertrait 怎么让各种句柄共享一套方法;这些字段各自的机制在别的页里。
flowchart TB am["Arc AppManager"] am --> authority["runtime_authority ACL"] am --> winmgr["window: 窗口表"] am --> wvmgr["webview: webview 表, invoke_handler"] am --> lis["listeners 事件监听器"] am --> plg["plugins PluginStore"] am --> st["state StateManager"] am --> res["resources_table"] am --> ctx["config, assets, pattern"] appn["App"] --> am hdl["AppHandle"] --> am win["Window"] --> am wv["Webview"] --> am
实验程序和平台同 Builder、App 与 AppHandle:M2 Max、macOS 27、rustc 1.99.0,tauri 2.12.5,真实的 Wry 运行时,隐藏窗口,debug 构建。
AppManager 里有什么
pub struct AppManager<R: Runtime> {
pub runtime_authority: Mutex<RuntimeAuthority>,
pub window: window::WindowManager<R>,
pub webview: webview::WebviewManager<R>,
#[cfg(all(desktop, feature = "tray-icon"))]
pub tray: tray::TrayManager<R>,
#[cfg(desktop)]
pub menu: menu::MenuManager<R>,
pub(crate) plugins: Mutex<PluginStore<R>>,
pub listeners: Listeners,
pub state: Arc<StateManager>,
pub config: Config,
#[cfg(dev)]
pub config_parent: Option<std::path::PathBuf>,
pub assets: Box<dyn Assets<R>>,
pub app_icon: Option<Vec<u8>>,
pub package_info: PackageInfo,
/// Application pattern.
pub pattern: Arc<Pattern>,
/// Global API scripts collected from plugins.
pub plugin_global_api_scripts: Arc<Option<&'static [&'static str]>>,
/// Application Resources Table
pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
/// Runtime-generated invoke key.
pub(crate) invoke_key: String,
pub(crate) channel_interceptor: Option<ChannelInterceptor<R>>,
/// Sets to true in [`AppHandle::request_restart`] and [`AppHandle::restart`]
/// and we will call `restart` on the next `RuntimeRunEvent::Exit` event
pub(crate) restart_on_exit: AtomicBool,
}
字段按来源分三类。with_handlers 把它们一个个填进去:
| 字段 | 来自 | 讲它的页 |
|---|---|---|
runtime_authority | Context.runtime_authority(编译期解析好的 ACL) | 运行时检查 |
config、assets、app_icon、package_info、pattern、plugin_global_api_scripts | Context | generate_context!、tauri:// 协议与 CSP |
window.windows、webview.webviews | 运行时建窗口时插入 | Window、Webview、WebviewWindow |
webview.invoke_handler、invoke_initialization_script、invoke_key | Builder | on_message 与分发 |
plugins | Builder.plugins,之后 AppHandle::plugin 追加 | 插件 |
state | Builder.state,之后 Manager::manage 追加 | 状态 |
resources_table | 空表 | 状态 |
listeners | 空表 | 本页下文 |
with_handlers 里唯一的逻辑是开头一段:开了 isolation feature、pattern 是 Isolation 时,用 uuid::Uuid::new_v4() 重新生成隔离密钥(「generate a random isolation key at runtime」),所以编译进二进制的那个值不会被用到,见 CSP、原型冻结与隔离模式。
几乎每个字段都各自包着 Mutex,AppManager 本身不加锁。这样 emit 时锁监听器表、on_message 时锁 ACL、插件钩子时锁插件表,互不阻塞;代价是锁的粒度和顺序要靠代码约定,插件表就因此有一个能死锁的用法,见插件。
AppManager 只建一次,放在 Arc 里。App、AppHandle、每个 Window、每个 Webview 都拿着一个 Arc<AppManager> 和一个 AppHandle 克隆:
pub struct Webview<R: Runtime> {
pub(crate) window: Arc<Mutex<Window<R>>>,
/// The webview created by the runtime.
pub(crate) webview: DetachedWebview<EventLoopMessage, R>,
/// The manager to associate this webview with.
pub(crate) manager: Arc<AppManager<R>>,
pub(crate) app_handle: AppHandle<R>,
pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
use_https_scheme: bool,
}
所以命令里不管拿到的是 AppHandle、Window 还是 Webview,都能走到同一个管理器。
窗口表、webview 表和关窗
窗口和 webview 各在一张 Mutex<HashMap<String, _>> 里,键是标签。get_webview、windows、webviews 都是锁住表、克隆出来(Window、Webview 的克隆只是几个 Arc 的引用计数加一)。
Manager::get_webview_window 不查窗口表,查的是 webview 表,再看这个 webview 所在的窗口是不是「一窗一 webview」:
fn get_webview_window(&self, label: &str) -> Option<WebviewWindow<R>> {
self.manager().get_webview(label).and_then(|webview| {
let window = webview.window();
if window.is_webview_window() {
Some(WebviewWindow { window, webview })
} else {
None
}
})
}
所以在 unstable feature 下用 Window::add_child 往一个窗口里放了多个 webview 之后,按任何一个标签都拿不到 WebviewWindow,只能用 get_window / get_webview 分别拿。这两个方法本身也只在 unstable 下存在。窗口和 webview 的拆分见 Window、Webview、WebviewWindow。
窗口销毁时,on_event_loop_event 先调 on_window_close,再把事件交给插件和 run 的回调:
pub(crate) fn on_window_close(&self, label: &str) {
let window = self.window.windows_lock().remove(label);
if let Some(window) = window {
for webview in window.webviews() {
self.webview.webviews_lock().remove(webview.label());
self.listeners().remove_webview_listeners(webview.label());
self
.state
.get::<crate::ipc::channel::ChannelDataIpcQueue>()
.remove_webview_entries(webview.label());
}
}
// window listeners are removed by the window's own event handler after it emits `tauri://destroyed`,
// since the run callback (which calls this function) runs before the window event handlers
}
窗口从表里删掉,它的每个 webview 也从表里删掉,同时删掉这个 webview 的事件监听器和它在 ChannelDataIpcQueue 里还没被取走的大消息(见 Channel)。
2.12.0 之前,on_window_close 只把 webview 从表里删掉。PR #15604 先补上了 JS 侧监听器,CHANGELOG:「Remove a webview's JS event listeners from the backend Listeners map when that webview is destroyed. Previously the entries keyed by the source webview label lingered after the webview was dropped, so they could never be delivered and leaked until the app exited — forcing apps to manually unlisten() before closing a window.」两周后 PR #15617 把 Rust 侧绑在窗口或 webview 上的监听器也一并清理(「so listen/once handlers registered on a Window, Webview or WebviewWindow no longer leak after it is closed」),方法改名为现在的 remove_webview_listeners,并在 on_window_close 末尾加了 remove_window_listeners(label)。这一句随后又被提交 078a0e044(「fix(core): Rust destroyed event not firing」)挪走了:on_window_close 在窗口自己的 tauri://destroyed 事件发出之前运行,提前删掉窗口监听器,Rust 侧就收不到 destroyed 事件。上面代码末尾那两行注释就是这次修正留下的。三处改动都在 tauri-v2.12.0 里。
Manager:一个封口的 trait
Manager 的方法(app_handle、config、get_webview_window、manage、state、path、add_capability …)几乎都有默认实现,实现者只需要提供 resources_table 一个方法。它能有默认实现,是因为它的父 trait 提供了访问 AppManager 的途径:
pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
pub(crate) mod sealed {
// ...
/// A running [`Runtime`] or a dispatcher to it.
pub enum RuntimeOrDispatch<'r, R: Runtime> {
/// Reference to the running [`Runtime`].
Runtime(&'r R),
/// Handle to the running [`Runtime`].
RuntimeHandle(R::Handle),
/// A dispatcher to the running [`Runtime`].
Dispatch(R::WindowDispatcher),
}
/// Managed handle to the application runtime.
pub trait ManagerBase<R: Runtime> {
fn manager(&self) -> &AppManager<R>;
fn manager_owned(&self) -> Arc<AppManager<R>>;
fn runtime(&self) -> RuntimeOrDispatch<'_, R>;
fn managed_app_handle(&self) -> &AppHandle<R>;
// ... android / ios
}
}
ManagerBase 在一个 pub(crate) 模块里,crate 外面写不出它的实现,于是也就实现不了 Manager。试一下:
mod sealed_try {
struct MyManager;
impl tauri::Manager<tauri::Wry> for MyManager {
fn resources_table(&self) -> std::sync::MutexGuard<'_, tauri::ResourceTable> {
unimplemented!()
}
}
}
error[E0277]: the trait bound `MyManager: tauri::sealed::ManagerBase<tauri_runtime_wry::Wry<EventLoopMessage>>` is not satisfied
--> src/main.rs:219:39
|
219 | impl tauri::Manager<tauri::Wry> for MyManager {
| ^^^^^^^^^ unsatisfied trait bound
|
help: the trait `tauri::sealed::ManagerBase<tauri_runtime_wry::Wry<EventLoopMessage>>` is not implemented for `MyManager`
...
= note: `Manager` is a "sealed trait", because to implement it you also need to implement `tauri::sealed::ManagerBase`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it
= help: the following types implement the trait:
tauri::AppHandle<R>
tauri::App<R>
tauri::WebviewWindow<R>
tauri::Webview<R>
tauri::Window<R>
rustc 自己认出了这是一个 sealed trait,并列出了全部五个实现者。封口的目的写在模块注释里:「Prevent implementation details from leaking out of the [Manager] trait」。manager() 返回的 &AppManager 是内部类型,放在公开 trait 上,外部代码就能随意摆弄窗口表和 ACL。
五个实现者的差别只在两处:
| 类型 | runtime() | resources_table() |
|---|---|---|
App | run 之前是 Runtime(&R),之后退回 handle | app 级 |
AppHandle | RuntimeHandle(R::Handle) | app 级 |
Window | Dispatch(R::WindowDispatcher) | 这个窗口自己的 |
Webview | 交给自己的 app_handle | 这个 webview 自己的 |
WebviewWindow | 交给内部的 Webview | 内部 webview 的 |
runtime() 决定「建窗口」这类操作怎么送到运行时:App 在 run 之前还握着运行时本身,可以直接调;其余的都只能经 handle 或 dispatcher 发消息,回到主线程上执行(见 tauri-runtime-wry 与事件循环)。resources_table() 分三级是为了让资源跟着窗口或 webview 一起回收,见状态。其余所有方法,不管从哪个句柄调,都落到同一个 AppManager 上。
命令处理函数和插件处理函数
Webview::on_message 做完 invoke key 和 ACL 检查后,调的是 AppManager 上的两个方法:
pub fn run_invoke_handler(&self, invoke: Invoke<R>) -> bool {
(self.webview.invoke_handler)(invoke)
}
/// Runs the plugin [`crate::plugin::Plugin::extend_api`] hook if it exists. Returns whether the invoke message was handled or not.
///
/// The message is not handled when the plugin exists **and** the command does not.
pub fn run_plugin_invoke_handler(&self, plugin: &str, invoke: Invoke<R>) -> bool {
self
.plugins
.lock()
.expect("poisoned plugin store")
.run_invoke_handler(plugin, invoke)
}
应用的处理函数(generate_handler! 生成的那个闭包)不加锁直接调;插件的处理函数要先锁住整张插件表,同步插件命令执行期间这把锁一直被持有。分流的条件和「Command not found」见 on_message 与分发,插件那一侧见插件。
事件:emit 先送 JS,再调 Rust 监听器
pub(crate) fn emit<S: Serialize>(
&self,
event: EventName<&str>,
payload: EmitPayload<'_, S>,
) -> crate::Result<()> {
#[cfg(feature = "tracing")]
let _span = tracing::debug_span!("emit::run").entered();
let emit_args = match payload {
EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?,
EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?,
};
let listeners = self.listeners();
listeners.emit_js(self.webview.webviews_lock().values(), &emit_args)?;
listeners.emit(emit_args)?;
Ok(())
}
payload 只序列化一次(EmitArgs::new 里 serde_json::to_string),然后:
emit_js:遍历 webview 表,只对在这个事件上登记过 JS 监听器的 webview 调webview.emit_js,生成一段脚本eval进去。没有 JS 监听器的 webview 不会收到任何 eval。JS 监听器登记在Listeners的js_event_listeners: HashMap<webview 标签, HashMap<事件名, HashSet<JsHandler>>>里,由 JS 的listen经plugin:event|listen写入。emit:再调 Rust 侧的监听器。
Listeners::emit_filter 对 Rust 监听器是直接调用:
match self.inner.handlers.try_lock() {
Err(_) => self.insert_pending(Pending::Emit(emit_args)),
Ok(lock) => {
if let Some(handlers) = lock.get(&emit_args.event) {
let handlers = handlers.iter();
let handlers = handlers.filter(|(_, h)| match_any_or_filter(&h.target, &filter));
for (&id, Handler { callback, .. }) in handlers {
maybe_pending = true;
(callback)(Event::new(id, emit_args.payload.clone()))
}
}
}
}
if maybe_pending {
self.flush_pending()?;
}
- 监听器在调用
emit的那个线程上、在emit返回之前同步执行,没有队列也没有线程切换。监听器里做重活,emit的调用方就等着。 - 调用期间监听器表被锁着。监听器里再
listen、unlisten或emit,try_lock拿不到锁,这些操作进pending队列,等当前这轮调用完再flush_pending。所以在监听器里emit另一个事件,不会死锁,但会排在当前这一轮之后。 - JS 那一侧正相反:
Webview::eval经 dispatcher 发一条WebviewMessage::EvaluateScript(在主线程上就地执行,在别的线程上经 proxy 排队,见 tauri-runtime-wry 与事件循环),脚本由 webview 自己异步执行,emit不等它。
实验在 setup 里登记一个 Rust 监听器,再从三处发同一个事件:setup 里、async 命令 async_cmd 里、页面 JS 调 plugin:event|emit:
app.listen("ping-evt", |e| log(format!("rust listener ping-evt {}", e.payload())));
app.emit("ping-evt", "from setup")?;
#[tauri::command]
async fn async_cmd(app: tauri::AppHandle) -> Result<String, String> {
log("app command async_cmd, emitting ping-evt");
app.emit("ping-evt", "from async_cmd").map_err(|e| e.to_string())?;
Ok("done".into())
}
await t('plugin:event|emit', () => I.invoke('plugin:event|emit', { event: 'ping-evt', payload: 'from js' }));
setup 那一次和 async_cmd 那一次的输出:
379ms [main] rust listener ping-evt "from setup"
...
506ms [tokio-rt-worker] app command async_cmd, emitting ping-evt
506ms [tokio-rt-worker] rust listener ping-evt "from async_cmd"
监听器跟着 emit 的调用方走:setup 在主线程上,async_cmd 在 tokio 的工作线程上。plugin:event|emit 需要 core:event:allow-emit 权限(下面的输出来自加了 core:event:default 能力的构建;没加时 debug 构建的拒绝原文是 event.emit not allowed. Permissions associated with this command: core:event:allow-emit, core:event:default)。
emit,Rust 监听器在哪个线程上跑?页面上 invoke('plugin:event|emit', { event: 'ping-evt', payload: 'from js' })。同步命令在 macOS 上跑在主线程(见异步运行时)。日志里 rust listener ping-evt "from js" 那一行的线程名是什么?
动脑筋答案
507ms [main] plugin command ping
507ms [tokio-rt-worker] plugin command ping_async
508ms [tokio-rt-worker] rust listener ping-evt "from js"
tokio-rt-worker,不是 main。core:event 插件的 emit 命令是 async fn(event/plugin.rs 里 async fn emit<R: Runtime>(app: AppHandle<R>, event: EventName, payload: Option<JsonValue>)),async 命令被 respond_async_serialized 丢进 async_runtime,函数体里的 app.emit(..) 在 tokio 工作线程上执行,监听器就在那里被同步调用。同一页面上的同步插件命令 ping 则在 main 上。
小结
AppManager只有一个,放在Arc里,App、AppHandle、Window、Webview都拿着它;字段大多各自加锁。内容分三类:Context带来的(配置、资源、ACL、pattern)、Builder带来的(命令处理函数、插件、初始状态、各种钩子)、运行时填的(窗口表、webview 表、监听器、资源表)。Manager依赖pub(crate)模块里的ManagerBase,crate 外实现不了,rustc 直接称之为 sealed trait;五个实现者只在runtime()和resources_table()上不同。get_webview_window查的是 webview 表,多 webview 的窗口拿不到WebviewWindow。- 关窗时
on_window_close删窗口、删 webview、删 webview 的监听器和未取走的 Channel 数据;监听器清理是 2.12.0(#15604、#15617)加的。 emit先给有 JS 监听器的 webview 各 eval 一段脚本,再在调用方线程上同步调 Rust 监听器;监听器里的listen/emit进 pending 队列。JS 发的事件经 async 的plugin:event|emit,Rust 监听器跑在 tokio 工作线程上。