ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

WinUI XAML 关闭机制设计:WindowsXamlManager、DispatcherQueue 与线程级运行时生命周期管理

WinUI XAML 关闭机制设计:WindowsXamlManager、DispatcherQueue 与线程级运行时生命周期管理 WinUI XAML 关闭机制设计WindowsXamlManager、DispatcherQueue 与线程级运行时生命周期管理【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml本篇技术文章基于 WinUI 仓库中的设计文档 xaml-shutdown.md完整解析 WinUI 3 中 XAML 框架的关闭Shutdown机制两类应用WinUI Desktop 与 Islands 应用下 XAML 对象的生命周期时间线、WinAppSDK 1.5 引入的“XAML 跟随 DispatcherQueue 关闭”新模型、私有 APIIFrameworkApplicationPrivate.ShutdownModel的源码实现以及线程投毒、进程投毒、泄漏测试等关键问题的现状与解决方案。读完本文你可以掌握如何正确让 XAML 运行时在指定线程上有序清理、避免增量泄漏并理解仓库中相关私有 API 与测试的实现细节。一、核心目标与非目标设计文档开篇明确了 XAML 关闭机制的两条设计目标让调用方能够在一个线程上轻松地清理 XAML且没有增量式 / 逐实例泄漏关于泄漏的更多讨论见下文“泄漏测试”小节在合适的场景中与“有序关闭organized shutdown”计划集成。同时文档明确列出了当前的非目标Non-Goals不支持完全卸载 microsoft.ui.xaml.dll 及其他 WinUI 3 DLL不承诺在使用 XAML 并运行到关闭后内存占用会归零net zero。这两条非目标非常重要它意味着 XAML 首次使用后会永久占用一笔“设计性泄漏by-design leak”的基础内存但机制要确保的是——随着 Island 的反复创建与销毁内存工作集不再持续攀升而是达到一个平台期plateau。二、背景两类 WinUI 应用与三个核心对象与本文主题相关的应用有两种形态WinUI Desktop 应用XAML 拥有并管理顶层窗口和消息泵message pump应用通过调用Application.Start来运行Islands 应用窗口和消息泵由应用或其他框架自己拥有XAML 只承担 GUI 的一部分应用使用的是DesktopWindowXamlSourceAPI 或XamlIslandAPI。当应用使用 Xaml Islands 时有三个实际是四个相关的关键对象Application—— 应用通常创建一个派生自Microsoft.UI.Xaml.Application的类模板里通常叫 App为了让工具链正确挂载IXamlMetadataProvider必须自定义这个类。如果应用不提供自己的 Application 对象XAML 会使用默认的那个。创建自定义 Application 对象时必须在第一个WindowsXamlManager被实例化之前完成因为WindowsXamlManager在发现没有可用的 Application 对象时会自己创建一个默认的。Application 对象是进程全局的整个进程只能有一个。它的线程模型很特殊它的许多 API 会作用于当前线程上所处的 DXamlCore。WindowsXamlManager—— 表示 XAML 运行时在该线程上的实例自 WinAppSDK 1.5 起每个线程有零个或一个WindowsXamlManager 对象XAML 保持活跃直到 DispatcherQueue 的关闭流程完成。WindowsXamlManager.Close在新模型下是一个 no-op空操作。XamlIsland—— WinAppSDK 1.7 新增其实例即一个“Xaml Island”一块可以嵌入到其他框架中的 XAML 内容矩形。它取代了DesktopWindowXamlSource。它会隐式创建一个WindowsXamlManager。DesktopWindowXamlSource—— 一种只能连接到一个子 HWND 上的 Xaml Island。XamlIsland类型取代了它因为前者更通用。它提供了一个Initialize方法允许应用将其附加到一个 HWND 上。三、Islands 应用的关闭时间线App Lifecycle以下是文档给出的、应用创建与清理 Xaml Islands 时对象生命周期的粗略时间线App 调用DispatcherQueueController.CreateOnCurrentThread()App 创建自定义 Application 对象App 构造函数App 创建初始的 WindowsXamlManagerDXamlCore::Initialize()消息泵Message PumpApp 创建 XamlIslandCXamlIslandRoot::Initialize()创建 InteropCompositor创建 ContentIslandApp 调用XamlIsland.Initialize()App 调用XamlIsland.Close()App 调用DispatcherQueueController.ShutdownQueue()DispatcherQueue.ShutdownStarting触发Application 执行它自己的清理DispatcherQueue.FrameworkShutdownStarting触发XAML 关闭所有未关闭的XamlIsland对象随之相关的ContentIsland及相关对象被关闭XAML 排空剩余的异步工作包括触发挂起的UIElement.Unloaded消息XAML 清理其线程本地状态XAML 的 TLS 槽位被清除此后DXamlCore::GetCurrent将失败DispatcherQueue.PlatformShutdownStarting触发所有活跃的 WinAppSDK 平台对象关闭自身包括 Compositor、islands 和输入对象从源码结构看这条时间线中的“FrameworkShutdownStarting 触发 XAML 清理”环节在实现层面由WindowsXamlManager内部的一个线程本地核心对象来承接。WindowsXamlManager_Partial.h 中定义了XamlCore基类派生出XamlCoreLegacyShutdown与XamlCoreNewShutdown两个子类每线程至多一个实例存储在 thread_local 的tls_xamlCore中负责管理 XAML 运行时在线程上的生命周期。它持有一个EventRegistrationToken m_frameworkShutdownStartingTokenWindowsXamlManager_Partial.h并声明了虚函数OnFrameworkShutdownStarting——正是 DispatcherQueue 关闭流程中 XAML 自动清理的入口点此外还有ReadyForEarlyShutdown()虚函数用于区分两种关闭模型下是否允许“提前关闭”。四、WinUI Desktop 应用的关闭时间线Desktop App Lifecycle在 WinUI Desktop 应用中应用创建的是一个 XAML Window 对象它拥有一个顶层窗口窗口内嵌一个占据整个客户区的DesktopWindowXamlSource而DesktopWindowXamlSource内部再配置一个 XamlIsland。使用默认模板构建时时间线如下App生成的wWinMain调用Application.Start()microsoft_ui_xaml!FrameworkApplication::StartDesktopXAML 调用DispatcherQueueController.CreateOnCurrentThread()XAML 调用Application.Start()回调其中用户的应用App被创建XAML 创建 WindowsXamlManagerDXamlCore 初始化XAML 触发Application.OnLaunchedApp 创建 WindowWindow 创建DesktopWindowXamlSource作为子窗口附加底层上DesktopWindowXamlSource创建一个 XamlIsland 来完成这项工作XAML 运行消息循环最终最后一个 Xaml Window 被关闭Window.Close被触发XAML 关闭最后一个 Xaml Window 内部拥有的DesktopWindowXamlSource和 XamlIsland 对象随之ChildSiteBridge、ContentIsland及相关对象被关闭App 一直运行到消息循环退出WM_QUIT等XAML 调用DispatcherQueueController.ShutdownQueue()DispatcherQueue.ShutdownStarting触发Application 执行它自己的清理DispatcherQueue.FrameworkShutdownStarting触发XAML 关闭所有应用显式创建的未关闭XamlIsland对象“内部那个”已经被关闭随之ChildSiteBridge、ContentIsland及相关对象被关闭XAML 排空剩余的异步工作包括触发挂起的UIElement.Unloaded消息XAML 调用ShutdownAllPeers()断开 XAML 对象的连接打断引用图通常触发对应用对象的Release()调用和析构XAML 清理其线程本地状态XAML 的 TLS 槽位被清除此后DXamlCore::GetCurrent将失败XAML 释放其对应用 App 对象的引用通常是最后一个引用调用App::~App()App::window被清除通常这是对 window 对象的最后一个引用调用App的Window::~Window()XAML 触发XamlShutdownCompletedOnThread事件向应用信号XAML 在该线程上已经完成收尾DispatcherQueue.PlatformShutdownStarting触发所有活跃的 WinAppSDK 平台对象关闭自身包括 Compositor、islands 和输入对象对比两条时间线可以看出两者的本质差异Desktop 应用多出了ShutdownAllPeers()、对 App 对象的最后释放以及XamlShutdownCompletedOnThread事件的触发。该事件在 IDL 层是WindowsXamlManager的正式 API属于Microsoft.UI.Xaml.WinUIContract, 6契约runtimeclass WindowsXamlManager : Windows.Foundation.IClosable { static Microsoft.UI.Xaml.Hosting.WindowsXamlManager InitializeForCurrentThread(); [contract(Microsoft.UI.Xaml.WinUIContract, 6)] { event Windows.Foundation.TypedEventHandlerWindowsXamlManager, XamlShutdownCompletedOnThreadEventArgs XamlShutdownCompletedOnThread; static Microsoft.UI.Xaml.Hosting.WindowsXamlManager GetForCurrentThread(); } }摘自 microsoft.ui.xaml.coretypes2.idl应用侧的用法是订阅XamlShutdownCompletedOnThread事件后当 DispatcherQueue 关闭流程走到 FrameworkShutdownStarting 且 XAML 完成线程级清理时即可在事件回调中确认“XAML 在这个线程上已经彻底收尾”从而安全地回收线程资源。事件源的实现见 WindowsXamlManager_Partial.cpp新模型下由XamlCoreNewShutdown::OnFrameworkShutdownStarting在Close()之后调用RaiseXamlShutdownCompletedOnThreadEvent(args)触发WindowsXamlManager_Partial.cpp。五、WinAppSDK 1.5 的关闭模型变更与 ShutdownModel 源码解析WinAppSDK 1.5 中团队做了一个关键变更让 XAML 在线程上保持运行直到 DispatcherQueue 关闭。但旧代码路径仍然保留。私有 APIIFrameworkApplicationPrivate.ShutdownModel控制这一行为当该属性被设为Version1时XAML 使用旧模型——线程上所有WindowsXamlManager和DesktopWindowXamlSource对象都关闭后运行时异步地自行关闭。这一设计在 IDL 中有明确定义ShutdownModel枚举定义于 microsoft.ui.xaml.private.idl[contract(Microsoft.UI.Xaml.PrivateApiContract, 1)] [webhosthidden] enum ShutdownModel { Version1 1, Version2, };而IFrameworkApplicationPrivate接口暴露了该属性microsoft.ui.xaml.private.idlinterface IFrameworkApplicationPrivate { Windows.Foundation.Collections.IVectorViewMicrosoft.UI.Xaml.Window Windows{ get; }; Microsoft.UI.Xaml.ShutdownModel ShutdownModel; ... };实现层的核心证据有三处默认值为新模型Version2。FrameworkApplication_Partial.h 中static constexpr xaml::ShutdownModel DefaultShutdownModel { xaml::ShutdownModel_Version2 }; xaml::ShutdownModel m_shutdownModel { DefaultShutdownModel };即当前代码库默认走的是“XAML 跟随 DispatcherQueue 关闭”的新模型put_ShutdownModelImpl仅接受 Version1 / Version2 两个取值FrameworkApplication_Partial.cpp。模型选择决定创建哪种线程核心。在 WindowsXamlManager_Partial.cpp 中初始化线程状态时按当前 ShutdownModel 分支Version1 创建XamlCoreLegacyShutdown否则创建XamlCoreNewShutdown。两个子类的行为差异精确对应文档描述。XamlCoreLegacyShutdown旧模型WindowsXamlManager_Partial.cpp维护线程上所有未关闭的WindowsXamlManager列表m_managersGetForCurrentThread()恒返回nullptr旧模型下不存在“每线程唯一 manager”的概念并且当线程上所有 manager 都关闭时ReadyForEarlyShutdown()返回 true允许提前异步关闭。XamlCoreNewShutdown新模型WindowsXamlManager_Partial.cpp注释明确写着 “In this model, theres one WindowsXamlManager per thread, and it stays alive until the DispatcherQueue shuts down. Closing a WindowsXamlManager is a no-op.” 其RegisterManager保证每线程至多一个 managerOnManagerClosed为空操作ReadyForEarlyShutdown()恒返回false——即永远不会提前关闭总是等到 DispatcherQueue 的FrameworkShutdownStarting阶段才由OnFrameworkShutdownStarting完成线程清理并触发XamlShutdownCompletedOnThread事件。从源码结构看XamlCore基类还持有一个进程级原子计数s_instancesInProcessWindowsXamlManager_Partial.h对应文档中“每线程计数 每进程计数”的修复手段见下一节。六、System XAML Islands 的关键问题与修复进度文档随后罗列了 XAML Islands 生命周期/关闭中几个最大的问题及处理进度。此处逐项继承并补充源码佐证。6.1 线程上第一个 WindowsXamlManager 必须最后被销毁已修复FIXEDSystem XAML 中线程上第一个WindowsXamlManager必须是最后一个被销毁的。修复后WindowsXamlManager现在维护每线程计数和每进程计数只有当每线程计数归零时才清理线程上的 XAML。该修复在 WinUI 3Lifted XAML中完成。源码中XamlCore基类的inline static std::atomicint s_instancesInProcess计数见上文即为“每进程计数”的实现载体。6.2 WindowsXamlManager 关闭是异步的通过要求 DispatcherQueue 缓解MITIGATED在 System Xaml 中应用必须自己排空消息队列例如用PeekMessage/DispatchMessage以确保所有工作完成如果线程退出前没有做到这一点大量对象会泄漏。在 Lifted Xaml 中XAML 框架现在会在 DispatcherQueue 的关闭流程中自动清理自身。对 Islands 类应用应用必须在首次使用 XAML 之前先用DispatcherQueueController.CreateForCurrentThread()在线程上创建 DispatcherQueue对 WinUI Desktop 应用框架会自动完成这件事。更详细的讨论可参见 xaml-islands-and-dispatcherqueue.md。源码层面XamlCore基类持有m_dispatcherQueue/m_dispatcherQueue3的引用并注册m_frameworkShutdownStartingTokenWindowsXamlManager_Partial.h这就是“XAML 挂在 DispatcherQueue 关闭流程上”的实现基础。6.3 线程投毒Thread poisoning对 XAML 已修复FIXED FOR XAML在 System XAML Islands 中WindowsXamlManager在一个线程上使用过又关闭之后该线程上就不能再使用 XAML 了当时还显式加了一个阻止。Lifted Xaml 现在支持这个场景先关后重开。文档同时保留了一个重要限定WinAppSDK 中可能还有其他对象在这个场景下尚未正常工作因此尚不宣称 WinAppSDK 场景下的线程投毒问题已端到端修复。6.4 进程投毒Process poisoningXAML 不能在进程内重启未修复NOT FIXED一旦进程内所有WindowsXamlManager都被释放XAML 会卸载其元数据metadata。如果应用试图在该进程中再次使用 XAML将无法工作——因为 MUXC 的元数据已经被卸载。这是当前机制的一个硬限制XAML 生命周期是进程内一次性的。6.5 Application 生命周期已修复FIXEDSystem XAML 会持有对应用 Application 对象的引用直到进程退出时 DLL 卸载才做最后的释放。这个清理时机通常太晚不是一个好的清理时点。Lifted XAML 的改进当所有 WindowsXamlManager 对象都不存在时XAML 会释放其对应用 Application 对象的引用XAML 对应用 Application 对象保留一个弱引用weak-ref如果应用在自己 Application 对象仍存活的情况下又启动了新的 WindowsXamlManagerXAML 可以继续复用那个 Application 对象在这种状态下MUX::Application::Current()API 会返回 null。6.6 Application / WindowsXamlManager 启动纠缠Startup entanglement未修复NOT FIXED在 System XAML 中如果应用想使用自己的自定义 Application 类型必须按顺序执行创建自定义 Application 对象调用WindowsXamlManager.InitializeForCurrentThread()调用MUX.Application.LoadComponent如需要。过去一种常见的且曾被推荐的做法是在第 2 步于自定义 Application 的构造函数里完成。文档将这种“启动顺序强耦合”标记为尚未修复的问题。七、FAQ这既适用于 islands 应用也适用于桌面应用吗一个 WinUI Desktop 应用本质上就是一个顶层 HWND、里面放一个占据整个客户区的DesktopWindowXamlSource。所以 WinUI Desktop 应用与使用 islands 的 win32 应用之间的差别其实不大。关键差异在于WinUI 3 桌面应用会调用Application.Start()它会自动创建WindowsXamlManager并运行消息泵而 win32 应用需要自己做这些。为什么需要 DispatcherQueue参见仓库中的专题文档 xaml-islands-and-dispatcherqueue.md。八、开放问题Open Issues8.1 Application 与 WindowsXamlManager 对象的 API 形态变更团队计划最终调整 Island 场景下WindowsXamlManager与Application的 API 形态当前文档使用的是 System XAML 的 API 形态。从当前代码看XamlShutdownCompletedOnThread事件与GetForCurrentThread()静态方法正是这一演进方向的已落地部分见第五节 IDL 摘录。8.2 测试运行中的“假”关闭Fake XAML shutdown during test runsXAML 核心测试在测试之间执行的是“假关闭”fake shutdown。新的XamlIslandTests测试套件中有一些执行“真关闭”的测试。8.3 泄漏测试Leak Testing由于目前还不支持卸载全部 WinUI 二进制应用首次使用 Xaml Islands 时必须支付一笔永久性内存开销某种意义上这是“设计性泄漏”。但仍要确保不存在“随着 islands 来来去去内存使用随时间飙升”的泄漏。文档给出了一个思想实验如果一个应用创建并销毁 island 100 次第 100 次销毁后的内存使用应与第 99 次销毁后相同——工作集应达到平台期。文档指出目前只有一个针对基本 island 场景的泄漏检查测试XamlIslandTests::ShutdownWithLeakDetection。在仓库中可以确认该测试确实存在位于 XamlIslandTests.cpp其核心流程是通过XamlIslandTestHelper.StartAppOnCurrentThread(RO_INIT_MULTITHREADED)启动应用反复执行 island 的创建与关闭随后调用Microsoft::UI::Xaml::DxamlCoreTestHooks::PerformProcessWideLeakDetection(20 /*max stacks to show*/)XamlIslandTests.cpp做进程级泄漏检测最后发送WM_QUIT让 UI 线程退出。文档同时承认该验证还应扩展到更复杂的场景并提到过去发现这很难因为 IXP 对象也牵涉其中——团队计划在“有序关闭”工作中与 IXP 团队讨论两个团队之间如何做泄漏测试。8.4 microsoft.ui.xaml.dll 暂时无法完全卸载目前尚不支持 XAML 在线程或进程中完全自清理——例如内存使用不需要降回到 XAML 使用之前的水平也不支持完全卸载 microsoft.ui.xaml.dll。这与第二节列出的非目标一致。8.5 具体场景说明文档留下了一条待办希望用具体现实场景来解释这些机制。例如当 Windows 资源管理器打开一个上下文菜单时它是如何做的它创建一个新的 XamlIslandRoot 还是复用已有的它创建新线程还是复用旧线程此处尚无结论仅作为开放问题记录。8.6 对最终开发者的价值是什么文档给出了方向性回答团队正朝着这样的世界努力——一个组件可以在任意线程上启停 XAML只需与进程内其他组件做最少或零的协调。而在现状下应用必须自己做大量的簿记工作bookkeeping才能做到在多线程、多种生命周期组合下使用 Xaml Islands。本文介绍的“每线程唯一 WindowsXamlManager DispatcherQueue 关闭驱动 XamlShutdownCompletedOnThread完成事件”的组合正是这一目标的当前落点。九、小结机制速查表机制 / 问题状态关键机制与源码位置第一个 Manager 必须最后销毁已修复每线程 每进程计数XamlCore::s_instancesInProcessWindowsXamlManager_Partial.h关闭异步性已缓解XAML 挂载到 DispatcherQueue 的 FrameworkShutdownStarting 阶段WindowsXamlManager_Partial.h线程投毒对 XAML 已修复新模型支持“关闭后重开”场景WindowsXamlManager_Partial.cpp进程投毒进程内重启 XAML未修复元数据卸载后不可再用Application 生命周期已修复最后引用在线程关闭时释放弱引用保活复用启动顺序纠缠未修复自定义 Application 必须先于首个 Manager 创建关闭模型选择双模型并存私有 APIIFrameworkApplicationPrivate.ShutdownModel默认Version2FrameworkApplication_Partial.h泄漏验证单一测试覆盖XamlIslandTests::ShutdownWithLeakDetectionXamlIslandTests.cpp对开发者而言实践中最重要的一条规则是在 Islands 场景下使用 XAML 的线程上先创建 DispatcherQueue再使用 XAML最后通过关闭 DispatcherQueue 来完成 XAML 的全部收尾并可用XamlShutdownCompletedOnThread事件作为线程资源可安全回收的信号。【免费下载链接】microsoft-ui-xamlWinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.项目地址: https://gitcode.com/GitHub_Trending/mi/microsoft-ui-xaml创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表