16 KiB
启动生命周期
本文定义从 UseAtomUI 到 Ready 的完整生命周期。总体结构与模块职责见启动系统架构;Splash 本身见
Splash。
1. UseAtomUI(AppBuilder 阶段)
public static AppBuilder UseAtomUI(this AppBuilder appBuilder, Action<IAtomUIBuilder> configure);
位于 AtomUI.Core,命名空间 AtomUI。同一 AppBuilder 只能调用一次,第二次调用抛 InvalidOperationException。
- 应用类型取
appBuilder.ApplicationType(Configure<TApp>()与Configure<TApp>(Func<TApp>)都会设置), 用于推导默认应用标识。 - 只登记:
UseAtomUI不执行配置回调、不创建 builder、不捕获 Prepare 输入、不访问 Dispatcher,也不启动任务。 任意应用配置及包/模块配置的副作用都留在平台服务就绪之后,不能以“配置只做声明”为理由提前执行未知应用代码。 - 配置与后台启动点:在
AfterPlatformServicesSetup中确认 UI Dispatcher 已就绪,再在 UI 线程执行配置回调, 捕获符合后台资格的 Prepare 不可变输入并启动后台任务,与后续 Application 创建并行。Dispatcher 的线程身份与计时 基准由平台初始化建立;配置间接创建AvaloniaObject属于受支持行为,不要求应用先把对象拆成纯静态元数据。 - 回调仍以配置为职责:普通宿主此时平台服务已就绪,但 Application 尚未构造、AtomUI 内核尚未提交,不能依赖
Application.Current、应用资源或已打开的TopLevel。字体注册与应用级语言注册仍在提交时执行;主题和主窗体创建 使用各自生命周期入口。回调异常在平台初始化路径原样同步抛出,不再从UseAtomUI登记调用中抛出。 - 不在平台回调中执行的情况:设计模式(
Design.IsDesignMode,预览器在调用BuildAvaloniaApp之前设置)与单线程运行时 (OperatingSystem.IsBrowser()/OperatingSystem.IsWasi()),全部改在Attach中同步完成。 - 不启动后台 Prepare 的情况:应用类型为 null 且回调中没有调用
WithApplicationId,或主题来源包含未声明后台安全的 自定义 resolver/手写编译程序。前者不能用占位应用标识读取用户目录;后者在需要显示 Splash 时于 UI 线程同步 Prepare, 保留SplashMode.Always的显示契约。官方生成主题与内部用户目录仍满足后台资格。 - 启动配置经
appBuilder.With(...)写入AvaloniaLocator(SetupUnsafe开头写入;headless 每个测试都会重新写入 同一个对象),守卫经AfterApplicationSetup注册(§8)。
2. 后台 Prepare
- 输入:初始主题请求、FollowSystem 的浅色 / 深色请求、主题定义来源(含用户主题目录)、应用标识,以及符合预热条件的 已启用包 definition 快照。后台任务不捕获或读取可变 builder。
- 包选择预热:仅
IsTrimmed && RuntimeFeature.IsDynamicCodeCompiled的提前后台路径,在主题准备前调用生成的WarmSelection。该方法用一个生成常量键执行GetSelectionMap().TryGetValue,即使该键已被裁掉也会创建外部映射;不枚举 候选键、不解析或构造 fragment、不加载主题资产、不执行OnCommit/OnFirstUse。此举将裁剪 R2R 的首次 TypeMap 建表移出 UI 提交期。非裁剪、NativeAOT、Browser/Wasi、旧 net8 注册后端与空包不做该预热;禁止后台准备的自定义主题来源 仍保持 §1 的线程边界。 - 映射复用:CoreCLR 的原始
TypeMapping.GetOrCreateExternalTypeMapping调用会重复建表。生成类的未标记GetSelectionMap使用线程安全、只保存成功结果的缓存,预热、selection、fragment resolve 与兼容 collector 共用同一实例; 创建失败后可重试。带GeneratedTypeMapAccessor标记的GetTypeMap保留原始调用体,供 Browser 与跨版本后端识别、改写, 不改变 key、触发类型或裁剪选择。 - 预热入口契约:
ControlPackageDefinition保留五、六参数构造器,默认没有预热回调。七参数构造器是隐藏的生成 ABI, 显式传入warmSelection即承诺纯数据、无副作用、线程安全,并能与超时后的同步 selection 并发。框架不通过反射鉴别 回调是否来自生成器;手写调用者若显式使用该入口,同样承担此契约。 - 工作:用"仅全局"模式的 schema registry(全局 Token 与算法,不含控件描述)加载主题目录、去掉配置中的控件段、 编译全局层,得到全局 Token 值与全局资源表。配置了 FollowSystem 时同时编译浅色、深色两套。
- 仅全局绑定:用户主题目录里的主题可以带控件段。"仅全局"模式下主题定义绑定器跳过控件段且不产生诊断;控件段只 影响控件层 Token,全局层结果不受影响。
- 输出:每种外观的全局层快照。提交时把对应快照作为初始主题编译的可复用父层传入,不重复编译全局层;Prepare 加载的 主题目录不含控件段,不在提交时复用,提交照常用完整 registry 加载目录。
- 纯数据约束:后台线程不得访问
Dispatcher、不得创建AvaloniaObject、不得设置 Avalonia 属性。违反约束时VerifyAccess会因 UI 线程已固定而抛出,失败传到Attach并按 §7 退化。 - 来源资格:启动期普通
IThemeDefinitionResolver(连同它返回的IThemeDefinitionSource.OpenRead)和手写CompiledThemeProgram默认在 UI 线程执行。resolver 实现IBackgroundThemeDefinitionResolver、或程序重写IsBackgroundPrepareSafe => true时,作者须保证整条回调链可在工作线程运行,并能与 30 秒超时后的 UI 线程回退并发。 生成器只给自身发射的纯数据程序设置此能力;旧二进制程序默认走 UI 线程。
3. Attach
public static void ApplicationStartup.Attach(Application application);
在 base.RegisterServices() 之后调用(通常由生成器生成,见 §8)。对同一实例幂等;没有 UseAtomUI 配置时直接返回,
纯 Avalonia 应用不受影响。步骤:
- 取得 builder:取本次平台服务就绪回调准备的 builder;没有准备结果时,新建 builder 并同步执行回调。
- 校验宿主:配置了
UseMainWindow但ApplicationLifetime不是IClassicDesktopStyleApplicationLifetime时抛出。 - 取得 Prepare 结果:已经启动后台任务的,等待完成(上限 30 s);没有后台任务且输入允许 Prepare 时,只有本次 会显示 Splash 才同步执行 Prepare,否则跳过(提交会在 UI 线程自行编译全局层)。
- 选外观:配置了 FollowSystem 时读取平台外观,选浅色或深色全局层;提交使用同一次读取的结果。
- 判定并显示 Splash(见 Splash):显示后驱动 Dispatcher 直到 Splash 首个渲染批次完成, 上限 5 s。
- 提交(§4)。
- 状态 →
Committed;登记主循环开始后的交接工作(§5)。
回调按实例执行:配置回调对每个 Application 实例执行一次。headless 测试按测试隔离时,同一 AppBuilder 会先后
创建多个应用实例;每次平台 setup 为该实例执行配置,Attach 只消费该次结果,不重复调用。没有经过平台配置回调的
实例在 Attach 中同步配置。各实例拥有独立的 builder 与内核状态。
4. 提交(一次)
- 应用级语言注册:应用类实现生成的语言 bootstrap 时调用其注册方法(与注册顺序无关,Catalog 与 Bundle 由 registry 排序)。
- 构建本地化宿主。
- 构建
ThemeManager:构建已启用包索引,保持片段与资源惰性,使用 Prepare 的全局层与完整主题目录。 - 创建自有服务(例如图片加载器)。
ApplicationScope.InitializeApplication:挂载资源与样式、初始化主题(复用全局层)、注册作用域、挂接自有服务。- 执行 initializer(
UseAlibabaSansFont等字体注册在此执行:字体在文字排版时才解析,提交期间不排版主窗体内容)。
步骤 5 的主题/语言资源宿主、全局 Style 分离和失败卸载责任由 应用资源宿主设计定义; 资源宿主验证记录当前实现证据及未覆盖的平台验收。 该设计保留 Initialize 前提交边界,不把资源可用性推迟到 AfterApplicationSetup、Loaded 或主循环。
包索引仅从已选中的登记项建立资产候选关系,不在索引构造期物化片段或资源。候选限定在所属包内并保持登记顺序; 追加包继续复用已有 entry/asset 对象。运行期 Type 身份键可以按弱类型键复用字符串,缓存值不得反向引用 Type、程序集或应用。
包的生命周期 context 在服务 Attach 前对所有新包就绪。Attach、initializer 或其他包的 OnCommit 若实际消费
控件/语义/资源,目标包先完成自己的 OnCommit,再执行一次 OnFirstUse,不会把未初始化 descriptor 返回。
尚未建立 scope 的服务 Factory 若尝试消费包会明确失败;同包重入及跨包提交循环也会失败并回滚。
包循环的普通提交遍历会跳过此前因实际消费而已提交的包,不会重复执行钩子。
任何一步失败都按已完成步骤逆序回滚(丢弃待处理注册、释放 ThemeManager、本地化宿主与自有服务、注销作用域);
服务 Factory 在 ApplicationScope 创建前失败也释放已构建的 ThemeManager 与包索引,
再按 §7 处理。提交成功后 Application.GetStartup()、GetThemeManager()、GetLocalizer() 等即可使用。
5. 主循环与交接
Attach 在提交后向 Dispatcher 投递一个回调,它在主循环开始后执行(此时 OnFrameworkInitializationCompleted 已返回、
生命周期已启动)。不是经典桌面生命周期(headless、single-view)时不投递,直接进入 Ready。判定主窗体:
- 配置了
UseMainWindow:在 UI 线程执行工厂(可 await,经MainWindowContext.Progress汇报进度),把返回的 窗口赋给desktop.MainWindow并显示;主窗体首个合成批次完成后才隐藏并关闭 Splash。应用同时自行给desktop.MainWindow赋值视为配置错误。 - 否则
desktop.MainWindow已赋值(传统写法、Prism):就是它。 - 否则视为没有主窗体:立即关闭 Splash,状态 →
Ready。
主窗体确定后状态 → Presenting;等它 Opened(上限 3 s)后的首个渲染批次(上限 3 s),然后立即隐藏并关闭 Splash,
状态 → Ready。传统宿主自行显示的主窗体在该首帧后才隐藏并关闭 Splash。两个主窗体等待超时只写 Warning 并直接交接,防止最小化或被遮挡的窗口让 Splash 永不关闭。没有 Splash 时同样按
此规则推进状态。主窗体在等待期间关闭、或用户关闭 Splash 触发启动取消时,关闭/取消信号会立即中断
Opened 与合成批次等待,不继续消耗 3 秒超时,也不记作成功首帧或 Ready。
6. 状态与进度
public interface IStartup
{
StartupState State { get; }
Task Completion { get; }
event EventHandler<StartupProgressEventArgs>? ProgressChanged;
}
经 Application.GetStartup() 取得;内核提交之前或提交失败后调用会抛出。状态:
Preparing → Committed → Presenting → Ready
└──────────┴────────────┴──→ Failed / Canceled
- 状态只前进;
Ready、Failed、Canceled是终态,进入后不可覆盖。 Completion在Ready时成功完成,失败时带原始异常,取消时为已取消。调用者可用WaitAsync(CancellationToken)取消自身等待,不影响应用启动。ProgressChanged在 UI 线程发布,参数含当前State与Detail;框架只在状态切换时发布,不伪造百分比。订阅不重放, 处理器异常逐个隔离并写 Warning 日志;进入终态后清空订阅。- 所有失败都写 Error 日志。
7. 出错与取消
| 失败点 | 处理 |
|---|---|
| 配置回调抛错 | 在平台服务就绪回调(或同步回退的 Attach)中抛出原异常;Configure 分段正常结束,不吞错继续启动。 |
| 后台 Prepare 失败(含 TypeMap 预热) | Warning 日志,不显示 Splash,照常提交。提交时仍执行真实 selection 与完整主题绑定;持续的映射或主题错误按提交失败处理,保留原异常。 |
| 等待 Prepare 超过 30 s | Warning 日志(疑似后台代码访问了 UI 线程),不显示 Splash,照常提交。 |
| Splash 创建或驱动失败 | Error 日志,跳过 Splash,启动继续。 |
| Splash 首帧等待超过 5 s | Warning 日志,启动继续。 |
| 提交失败 | 回滚(§4)。有 Splash:切换错误态并驱动 Dispatcher 直到用户关闭,然后从 Attach 重抛原异常,传到应用 Main。无 Splash:立即重抛。 |
UseMainWindow 工厂抛错 |
状态 → Failed;有 Splash 时切换错误态,用户关闭后把原异常投递到 Dispatcher 抛出,传到 Main。 |
| 交接前用户关闭 Splash | 状态 → Canceled,取消 MainWindowContext.CancellationToken,调用 Shutdown()。提交期间的关闭请求在主循环开始后处理。 |
| 主窗体在首帧前被关闭 | 状态 → Canceled。 |
| 主窗体首帧超过 3 s 未到 | Warning 日志,直接交接。 |
错误态只显示错误消息,不显示任何框架文字,因此不依赖本地化。
8. 挂接生成器与守卫
生成器(AtomUI.Generator,ApplicationStartupHookGenerator)为每个具体的 partial Application(继承
Avalonia.Application、顶层、非泛型)生成:
public override void RegisterServices()
{
base.RegisterServices();
global::AtomUI.ApplicationStartup.Attach(this);
}
- 编译中不存在
AtomUI.ApplicationStartup时不生成。 - 类中已手写
RegisterServices重写:方法体调用了ApplicationStartup.Attach时不生成;没有调用时报错误诊断ATOMUIGEN039,提示在base.RegisterServices()之后调用。抽象类自身不报(无法实例化也无法自行修复,由派生类挂接)。 - 抽象、泛型、嵌套、非 partial 的类不生成,由运行时守卫给出修复说明。
- 同一程序集可以有多个应用类(测试工程常见),每个都生成。
- NuGet 消费者经 buildTransitive 自动获得生成器;仓库内工程需要显式 Analyzer 引用,普通测试工程不引用生成器,测试 应用类手写上面的重写。
守卫:UseAtomUI 注册的 AfterApplicationSetup 回调在 Initialize 之后、OnFrameworkInitializationCompleted 之前
执行;本实例没有执行 Attach 即抛出 InvalidOperationException,说明"把应用类声明为 partial,或在
base.RegisterServices() 之后调用 ApplicationStartup.Attach(this)"。
约束:不要在应用构造函数中创建控件或访问主题——构造函数早于 RegisterServices,此时内核尚未提交。
9. 宿主差异
| 宿主 | 配置时机 | Splash | 说明 |
|---|---|---|---|
| 桌面(经典生命周期) | 平台就绪后、Application 构造前 | 按显示判定 | 主路径 |
| headless 测试 | 每次平台 setup 就绪后、Application 构造前 | 否(无经典生命周期) | 每个实例各执行一次;提交后直接 Ready |
| 设计器 | Attach 中同步执行 |
否 | 仍然提交,预览器能渲染 AtomUI 控件 |
| 浏览器 / WASI | Attach 中同步执行 |
否 | 避免单线程阻塞等待造成死锁;提交后直接 Ready |
10. 内部入口 AttachWith
ApplicationStartup.AttachWith(Application, Action<IAtomUIBuilder>)(internal)按显式配置直接提交:不提前执行、不做 Prepare、
不显示 Splash,同一实例重复调用抛出。只供测试与仓库内部工具使用;应用一律走 UseAtomUI + Attach。