# AtomUI 主题系统架构 本文是 AtomUI 主题系统唯一的长期架构设计文档。它定义最终运行时模型、公开配置语义、主题文件处理、 Token 编译、动态作用域、资源发布、事件契约、AOT 边界和验收标准。阶段性分析、迁移过程和任务进度 不写入本文,统一放在 `docs/superpowers/`。 本文描述目标架构。实现过程中不得为了保留旧主题系统 API 而偏离这些约束。 ## 1. 设计目标 主题系统必须满足以下不可协商的约束: - 建立完整的 Seed Token、Map Token、Alias Token、Control Token、算法和嵌套配置模型。 - AtomUI 的公开 API、内部类型、资源键和文档统一使用 `Control` 术语。 - `ThemeSnapshot` 是运行时 Token 值的唯一真源。 - 全局主题和局部主题使用同一个配置合并器、编译器、缓存和提交协议。 - 主题变更先完成整个受影响作用域子树的编译,再在 UI 线程完成无通知引用交换;任何外部观察者只能在 Publish 阶段看到完整新状态。 - `ThemeConfig`、`ControlThemeConfig` 和 `ThemeRequest` 是深度不可变值,动态更新通过替换完整配置表达。 - AtomUI Token appearance 与 Avalonia `ActualThemeVariant` 在根作用域和每个局部作用域中始终一致。 - 控件级配置只参与目标控件自身的 Token 计算,不形成控件内容树资源作用域。 - 每个主题作用域只拥有一个稳定的 Token 资源 Provider。 - AXAML 中共享 Token 始终使用统一的 `{atom:SharedTokenResource TokenName}` 语法,Control identity 不泄漏到 每个资源引用名称中。 - Token 资源查询热路径不分配对象,不执行字符串解析、反射或 LINQ。 - 局部主题只复制发生变化的数据,不能按作用域复制完整全局 Token 和全部 Control Token 值。 - 缓存、作用域图、事件订阅和异步编译任务都必须有确定的容量或释放边界。 - 普通 Popup 通过 Avalonia 逻辑树继承主题;独立 TopLevel 必须通过显式 owner context lease 获得局部主题。 - 无 owner 的静态 Dialog、Notification 等 API 只能使用根主题,不能推断调用位置的局部上下文。 - 内置正常路径不使用运行时程序集扫描、`Activator.CreateInstance` 或 Token 属性反射。 - 第一个可见帧之前完成全局主题准备和挂载。 - 不保留旧主题系统的兼容对象、克隆查询 API、伪生命周期事件或可变资源旁路。 ## 2. ThemeConfig 设计结论 ### 2.1 Ant Design 参考模型与 Avalonia 映射 本架构的主题语义基于 Ant Design `6.4.5`(commit `66f2b2c72a`)、 `@ant-design/cssinjs 2.1.2` 和 `@ant-design/cssinjs-utils 2.1.2` 的源码,而不是只参考公开 API 名称。需要保留的基本原理是: - ConfigProvider 对 Token 按 key 合并,对 Control 配置先按 identity、再按字段合并;`Algorithms` 整体替换。 - `Inherit=false` 只切断父 ConfigProvider,仍从控件库默认主题基线开始计算。 - Token 按 Seed、Map、Alias、Control 四层派生,Control 可以覆盖自身消费的全局 Token。 - 算法是有序 derivative 链;每个算法接收同一份 Seed 和前一算法的 Map 结果。 - 主题计算和组件计算按完整内容缓存,缓存身份包含算法链和所有会改变结果的配置。 - 嵌套主题依赖上下文隔离;脱离上下文的新渲染根不能自动获得调用点主题。 - 样式结构保持稳定,主题切换只替换作用域内 Token 值。 Web 专属实现不进入 AtomUI:CSS selector hash、CSS Variable 命名、style tag 注入、CSS 序列化、SSR 提取、CSP nonce 和 React hook 生命周期均不属于 Avalonia 主题系统。它们在 AtomUI 中分别由静态 ControlTheme、稳定资源 key、snapshot-backed ResourceProvider、Avalonia 逻辑树和显式资源宿主生命周期替代: ```text Ant Design: static component CSS + scoped CSS variables AtomUI: static ControlTheme + scoped snapshot resources ``` 因此 AtomUI 不需要 `zeroRuntime` 或 hashed style 开关。ControlTheme 本来就在构建期编译,运行时只计算和发布 Token snapshot。 ### 2.2 公开配置语义 AtomUI 使用统一的 `ThemeConfig` 描述全局主题和局部主题输入: | 配置项 | 设计结论 | |---|---| | `ThemeConfig.Tokens` | 覆盖当前作用域的全局 Token | | `ThemeConfig.Controls[ControlTokenIdentity]` | 覆盖目标 Control 自身 Token 和其消费的全局 Token | | `ThemeConfig.Algorithms` | 定义 Seed 到 Map、Alias 的有序算法链 | | `ThemeConfig.Inherit` | 决定是否继承父作用域有效配置,默认 `true` | | `ControlThemeConfig.Algorithm` | 定义目标 Control 的局部算法策略 | AtomUI 的控件算法必须能表达四种状态: 1. 未指定:继承父配置中同一 Control 的算法策略。 2. 禁用:Control 内覆盖的 Seed Token 不重新派生 Map 和 Alias Token。 3. 使用全局算法:使用当前作用域的全局算法链重新派生该 Control 的有效 Token。 4. 自定义算法:使用 Control 自己声明的有序算法链。 继承链没有提供 Control 算法策略时,未指定最终解析为 AtomUI 默认策略“禁用”。 算法配置的领域模型是 `Unspecified / Disabled / Global / Custom` 四态策略。可空 `bool` 加自定义算法列表 虽然可以间接编码这些状态,但会把一个领域值拆成两个耦合字段,并产生布尔值与自定义列表同时声明等无效 组合。因此配置规范化后必须使用 `ControlAlgorithmMode` 枚举;Custom 的有序算法 identity 作为不可变 payload 保存,并且只允许在 Mode 为 `Custom` 时非空。`bool` 或 `bool?` 不能作为内部真源。 `ThemeConfig`、`ControlThemeConfig` 及其全部集合在构造完成后不可修改。公开构造器或 `ThemeConfigBuilder` 必须防御性复制输入;Builder 只是一次性构造工具,不进入 Provider、Manager、snapshot 或 缓存。运行时绑定通过替换 `ThemeConfigProvider.Config` 更新主题,不支持对已经提交的 Config 内部集合做原位 修改。 ## 3. 总体架构 定义与编译路径: ```text +-------------+ +---------------------+ +-----------------------+ | Theme files | --> | ThemeDocumentReader | --> | ThemeDefinitionBinder | --+ +-------------+ +---------------------+ +-----------------------+ | | +-------------+ +-----------------------+ | | ThemeConfig | --> | ThemeConfigNormalizer | -------------------------------+ +-------------+ +-----------------------+ | v +-------------------+ | ThemeConfigMerger | +---------+---------+ | v +---------------------+ supplies descriptors +-------------------+ | ThemeSchemaRegistry | ------------------------------------------> | ThemeCompiler | +---------------------+ +---------+---------+ | v +-------------------+ | ThemeSnapshotCache | +-------------------+ ThemeSchemaRegistry 还为 Binder、Normalizer 和资源投影提供 descriptor。 ``` 运行时发布路径: ```text +--------------------+ +--------------+ | ThemeSnapshotCache | -----> | ThemeManager | +--------------------+ +------+-------+ | atomic snapshot commit | +---------------+---------------+ | | v v +-------------------+ +-----------------+ | Root ThemeContext | | ThemeScopeGraph | +---------+---------+ +--------+--------+ | | | v | +--------------------+ | | Child ThemeContexts | | +----------+---------+ | | +---------------+---------------+ | v +----------------------------+ | ThemeTokenResourceProvider | +-------------+--------------+ | v +----------------------------+ | AXAML and Control runtime | +----------------------------+ ``` 系统分成六个边界: - 定义层:读取主题文件并生成与运行时注册无关的语法模型。 - Schema 层:提供生成式 Token 元数据、强类型赋值器、资源投影器和 Control 身份。 - 配置层:绑定不可变公开配置,生成不可变规范化配置,并执行 AtomUI 主题配置合并。 - 编译层:纯计算 Token,输出不可变 `ThemeSnapshot`。 - 运行时层:管理根主题、作用域图、事务、并发、缓存和事件。 - 资源层:把当前 snapshot 投影到 Avalonia 资源查询,不拥有第二份 Token 状态。 ## 4. 配置模型 ### 4.1 公开输入 `ThemeConfig` 是面向 C#、AXAML 和绑定的不可变配置输入: ```text ThemeConfig +-- Inherit = true +-- Algorithms: optional ordered list +-- Tokens: global token overrides \-- Controls[ControlTokenIdentity] \-- ControlThemeConfig +-- Algorithm: unspecified / disabled / global / custom \-- Tokens: control-own and global token overrides ``` `ThemeConfig` 使用只读集合和不可变 entry。C# 可以通过一次性 Builder 构造,AXAML 通常把 ViewModel 中的 不可变 Config 绑定到 Provider;需要内联声明时,由构建期配置扩展生成同一不可变值。不存在“修改已加入集合 的 setter 后自动编译”的隐式行为。 `ThemeConfigProvider` 只把以下变化视为新输入: - `Config` 属性被替换。 - Provider attach、detach 或 reparent 导致父 ThemeContext 改变。 Provider 不订阅 Config 内部对象图。Capture 在 UI 线程读取一个深度不可变引用,因此后台 Prepare 不需要再次 复制,也不会观察到中途变化。旧 Config 在属性替换后不再被 Provider、Manager、Compiler、snapshot 或缓存 保留;缓存只保存规范化后的结构化 key。 `Algorithms=null` 表示“未指定”;非空集合表示显式算法链。空集合不是“禁用算法”,属于无效输入,必须 产生 diagnostic 并保留最后一次有效配置。Control 自定义算法链遵守相同规则。 `ThemeConfigProvider` 是轻量局部 ThemeVariant 宿主,只公开一个 `Config` 和继承的内容子节点。旧的 `SharedTokenSetters`、`ControlTokenInfoSetters`、字符串算法集合以及 Token 查询属性不属于新架构。 ### 4.2 规范化配置 `ThemeConfigNormalizer` 对一次不可变输入完成以下工作: - 解析并验证 Token key 和 value。 - 把 Control id 解析为 `ControlTokenIdentity`。 - 保留算法的“未指定”状态。 - 将公开 identity 和 value 绑定为当前 registry revision 下不可变、有确定顺序的值对象。 - 计算稳定的结构 fingerprint。 - 返回 diagnostics,不抛出缺少上下文的反射异常。 编译器只接收 `NormalizedThemeConfig`。Normalizer 不读取 Visual、ResourceHost 或全局服务。 ### 4.3 合并规则 `ThemeConfigMerger` 是全局和局部主题唯一的合并入口: 配置优先级从低到高固定为: ```text ThemeSchemaRegistry / AtomUI library defaults -> selected ThemeDefinition -> builder/application initial config -> runtime ThemeRequest.Config -> parent scope effective config -> local ThemeConfigProvider.Config ``` 根事务先把所选 `ThemeDefinition` 应用到 AtomUI library defaults,再应用 builder/application initial config。 `ThemeRequest.Config` 为 `null` 时保留该根配置;非空时按 `Inherit` 合并:`Inherit=true` 继续继承应用初始 配置,`Inherit=false` 只丢弃较低优先级的应用初始 config,以“AtomUI library defaults + 当前所选 ThemeDefinition”为根基线。选择某个主题后,runtime config 不能通过 `Inherit=false` 意外删除该主题定义。 局部 `ThemeConfigProvider` 的父输入是父 `ThemeContext` 的完整有效配置。局部 `Inherit=false` 的基线是 AtomUI library defaults,不继承父作用域、当前所选主题定义、应用初始配置或 runtime override;随后只应用 当前 Provider 的本地配置。这与 Ant Design ConfigProvider 的局部 `inherit=false` 语义一致。 - `Inherit=true` 时以父作用域的有效配置为基础。 - `Inherit=false` 时使用上述入口对应的基线,不隐式复制父有效配置。 - `Config=null` 等价于不可变空配置 `Inherit=true`,只共享父有效结果。 - 当前 `Tokens` 按 key 覆盖父 `Tokens`。 - `Controls` 先按 `ControlTokenIdentity` 合并,再按 Token key 合并。 - Control 的算法未指定时继承父策略;显式 disabled、global 或 custom 时替换父策略。 - 当前 `Algorithms` 为 `null` 时继承基线算法;显式提供非空列表时整体替换,不和基线算法拼接。 - 主题定义、应用级配置和运行时请求都先转换为规范化配置,再按同一规则合并。 合并器同时计算 `BaseAppearance`:根配置以所选 ThemeDefinition 的已验证 appearance 为基线;局部 `Inherit=true` 以父 snapshot appearance 为基线;局部 `Inherit=false` 以 AtomUI library baseline 的 Light appearance 为基线。全局算法 descriptor 的 appearance effect 按顺序折叠,最后一个非 `Preserve` effect 决定结果。Control 自定义算法只影响该 Control 的 Token 和 Control appearance,不改变所在 ThemeContext 的 Avalonia ThemeVariant。 Control 的 `Tokens` 使用一个集合表达 Control 自身 Token 和其消费的全局 Token。Schema Binder 根据 生成元数据完成分类;同名 Token 同时存在于两类 schema 时,同一个覆盖值同时应用于两类输入。由此,一个 Control 配置项可以同时覆盖 Control 自身 Token 和该 Control 消费的全局 Token。 ## 5. Theme Schema 与注册 `ThemeSchemaRegistry` 在应用主题初始化前一次性构建,内容来自源生成器或显式描述符: ```text ThemeSchemaRegistry +-- GlobalTokenSchema | \-- token name, kind, stage, value type, parser, setter, getter, resource key +-- ControlTokenDescriptor[ControlTokenIdentity] | \-- factory, own token schema, evaluator, resource projector \-- ThemeAlgorithmDescriptor[algorithm id] \-- revision, evaluator, AOT factory, appearance effect ``` 每个 `ThemeAlgorithmDescriptor` 必须声明稳定 id 和显式 revision/version。算法实现、默认参数、依赖 Token 或 输出语义发生变化时必须提升 revision,不能只保留相同 id 让旧缓存继续命中。 算法 descriptor 的求值契约固定为: ```text Evaluate(effectiveSeed, previousMap?) -> nextMap ``` 同一算法链中的 `effectiveSeed` 引用和值保持不变;第一个算法接收 `previousMap=null`,后续算法接收前一个算法 输出的 Map。算法不得修改 Seed 或 previous Map,必须返回新的不可变语义结果或只在本次编译 workspace 中写入 目标 builder。Dark、Compact 在没有 previous Map 时可以内部调用 Default,但这种 fallback 属于算法实现, 不能改变通用链契约。 `ThemeSchemaRegistry.Revision` 根据排序后的完整 descriptor schema 确定性计算,输入至少包含 Control identity、Token name/kind/stage/value type/resource key schema、算法 id/revision 及其外观声明。注册顺序不影响 revision;任何会改变绑定、编译或资源投影结果的 descriptor 变化都必须改变 revision。 Registry 在首个 snapshot 编译前冻结。Manager 构建完成后不能追加 Token、Control、算法或主题资产 descriptor;可选控件包必须在 `UseAtomUI` Builder 阶段完成显式注册。 生成器分配的 Token 和 Control slot 只保证在同一 registry revision 生命周期内稳定。不同 revision 可以重新 分配 slot,公开协议、持久化配置和跨版本缓存不得依赖整数 slot 不变,而应使用稳定 identity 并在绑定后解析 当前 revision 的 slot。 源生成器必须为内置 Token 生成: - 稳定的 `ControlTokenIdentity`,包括 catalog 和 control id。 - Token name 对应的强类型赋值委托查找表。 - Token value 到 Avalonia resource value 的投影。 - Control Token builder 的直接构造委托。 - Control 自身 Token schema 和继承的 Token schema。 - 生成式 Control 自身资源键,以及 Control identity 与共享 Token kind 的内部复合 key factory。 - 每个 ControlTheme 资产的 `ControlThemeAssetDescriptor`,包括资产 URI、唯一 Control identity 和资源 key schema 摘要。 生成器不得为每个 Control 生成 `AlertTokenSharedTokenResource`、`ButtonTokenSharedTokenResource` 之类的公开 MarkupExtension。Control identity 只属于 schema、配置和主题资源作用域边界,不属于每一次共享 Token 的 AXAML 调用名称。 包注册直接传递完整描述符,不得把生成器提供的 identity 丢弃后只注册 `Type`。内置路径删除 `Activator.CreateInstance`、`Type.GetProperties`、`PropertyInfo.GetValue/SetValue` 和枚举反射。 第三方 Control Token 必须使用 AtomUI 源生成器,或者显式提供完整 descriptor。不存在自动扫描程序集的 fallback。 ## 6. 主题文件 ### 6.1 文件结构 主题定义 XML v1 使用版本化 namespace、显式 identity 和 Control 术语: ```xml ``` 规则如下: - namespace `https://atomui.net/schemas/theme/v1` 是格式版本,必须显式声明。 - `Theme.Id`、`Appearance` 和顶层 `Algorithms` 必须显式声明。 - `Appearance` 是从 library Light baseline 折叠 definition 算法 effect 后的最终断言,不是覆盖算法结果的独立 开关。 - 算法使用有序 `Algorithm` 元素,不使用逗号分隔字符串。 - Control 使用 `(Catalog, Id)` 组成稳定身份。 - Control 的 `Algorithm` 省略表示未指定,`Disabled` 表示禁用,`Global` 表示使用全局算法链。 - Control 自定义算法使用子元素 `Algorithms`,并且不能同时声明 `Algorithm` 属性。 - Token 是空元素,必须通过 `Value` 属性提供非空值,不允许正文值或类型分类属性。 完整格式、词法、安全限制、diagnostic 和版本演进规则见 [主题定义 XML v1 规范](theme-definition-xml.md),机器可读标准见 [AtomUI Theme Definition XML Schema v1](schemas/atomui-theme-v1.xsd)。 ### 6.2 两阶段处理 `ThemeDocumentReader` 只负责: - 使用禁止 DTD、外部实体和网络解析的 validating `XmlReader`,并显式启用 XSD identity constraint。 - 通过进程级复用的 v1 XSD 校验 namespace、结构、属性、容量和局部唯一性。 - 保留源码位置并执行 XML 元素总数等 Reader 级资源限制。 - 输出不依赖当前 Control 注册集合的 `ThemeDocument`。 `ThemeDefinitionBinder` 负责: - 使用 `ThemeSchemaRegistry` 解析 Token 和算法 id。 - 将字符串值一次性转换为目标类型。 - 校验未知 Token、未知 Control、算法冲突、Appearance 一致性和跨字段语义。 - 输出不可变、typed `ThemeDefinition` 和结构化 diagnostics。 Reader 不得因为某个可选控件包没有安装而修改或过滤语法树。Catalog 保存完整 diagnostic,并决定某个 定义是否可用于当前 registry。`ThemeDefinitionRevision` 是由 Catalog 生成的结构化值,至少包含稳定 source identity、source revision 和内容摘要:文件来源使用实际字节内容摘要,不能只使用时间戳;包内资源包含包版本、 资源路径和内容摘要;程序化来源必须显式提供 revision。来源字节或程序化定义语义变化时 revision 必须变化。 同一 definition revision 在进程中最多成功读取一次;同一 `(definition revision, registry revision)` 最多成功 绑定一次。更换 registry revision 只重新绑定已有 `ThemeDocument`,不重复解析来源 XML。 ## 7. 编译器 `ThemeCompiler` 是无 UI、无资源宿主、无全局服务访问的纯计算入口: ```text ThemeCompileInput -> merge effective config -> initialize effective Seed Token -> run ordered Evaluate(seed, previousMap) algorithms -> apply Map overrides -> format Alias Token -> apply Alias overrides and AtomUI final normalization -> evaluate each registered Control -> freeze token values and resources -> ThemeSnapshot ``` 编译阶段允许使用可变 builder,因为算法天然需要逐步派生。builder 的生命周期只限于一次编译,不能进入 snapshot,也不能通过公开 API 返回。 全局计算顺序固定为: 1. 从 AtomUI library Seed defaults、ThemeDefinition 和有效配置构造唯一的 effective Seed。 2. 将同一 effective Seed 依次传给算法链;第一个算法的 previous Map 为空,后续算法接收前序 Map。 3. 对最终 Map 应用显式 Map Token override。 4. 由 Map 生成 Alias Token,再应用显式 Alias Token override。 5. 执行 AtomUI 最终语义规范化。`MotionEnabled=false` 时,Motion 服务被禁用且全部标准 duration Token 强制为 `TimeSpan.Zero`;`WaveEnabled=false` 时 Wave 行为不创建动画。该后置条件不依赖单个控件是否正确消费时长。 每个 Control 的计算顺序为: 1. 以全局最终 Alias Token 为基线,叠加该 Control 覆盖的全局 Token。 2. `Disabled` 时只保留直接覆盖,不重新派生;`Global` 或 `Custom` 时,从 Control effective Seed 按相应算法链 重新生成 Map 和 Alias。 3. 在重新派生后应用该 Control 的 Map、Alias override 和 Motion 最终规范化。 4. 使用 Control 的有效全局 Token 计算 Control 自身默认 Token。 5. 应用 Control 自身 Token override。 6. 根据全局 appearance 和 Control 算法 effect 计算 Control appearance;它只提供给该 Control evaluator,不 修改 ThemeContext appearance。 7. 生成共享 Token 差量、Control Token 值和资源投影。 这与 Ant Design 的组件计算保持同一语义:未启用组件算法时只是覆盖;启用时以“全局最终 Token + 组件覆盖” 作为重新派生输入。Control 默认 Token evaluator 必须接收 Control appearance,不能继续只读取全局 `IsDark`。 任何 validation、conversion 或 algorithm 错误都返回失败结果,不返回部分 snapshot。 ## 8. ThemeSnapshot `ThemeSnapshot` 是 Token 编译结果和资源发布单元: ```text ThemeSnapshot +-- ThemeId / DefinitionRevision / ContentFingerprint +-- Algorithms[(id, revision)] / Appearance +-- RegistryRevision +-- EffectiveConfig +-- GlobalTokenValues: dense immutable TokenValueTable +-- GlobalResources +-- PresetColorPalettes \-- Controls[registry control slot] +-- Appearance +-- EffectiveGlobalTokenDelta +-- ControlTokenValues: dense immutable TokenValueTable \-- ControlResources ``` 具体约束: - Token schema 由生成器分配在当前 registry revision 内稳定的 slot;完整 Token 值表使用稠密不可变数组,不使用 object-key 字典保存每个值。 - Control 表按 registry slot 存放引用,`ControlTokenIdentity` 只用于配置边界和诊断。 - 稀疏覆盖、共享 Token 差量和 Avalonia 资源投影使用不可变集合或 `FrozenDictionary`。 - 颜色资源在 snapshot 中已经转换为不可变 Brush。 - palette、集合和其他引用值必须在冻结前深复制为不可变值。 - 不保存 `DesignToken`、`IControlDesignToken`、Control、Visual、Provider、事件或订阅。 - 不提供 `SharedToken`、`ControlToken`、`Clone` 或可变字典查询 API。 - imperative 消费者通过只读 Token resolver 查询 snapshot 值。 - `ContentFingerprint` 是规范化内容的快速 hash,不是无碰撞的唯一身份。snapshot 的语义相等性必须比较对应的 结构化不可变 key,不使用父 snapshot 的递增版本表达语义身份。 - `Appearance` 是 `Light` 或 `Dark` 的已提交结果,是根 Application 和局部 ThemeVariantScope 发布 Avalonia variant 的唯一依据;不能通过算法名称字符串猜测。 ## 9. 缓存 `ThemeSnapshotCache` 使用结构化 value key: - 完整的不可变 `ThemeDefinition` 结构化 key,包括 definition revision 和 typed 内容。 - 完整的不可变 `NormalizedThemeConfig` 结构化 key。 - 有序算法 descriptor `(id, revision)`。 - `ThemeSchemaRegistry` revision。 父主题的影响已经包含在合并后的有效配置中,因此 cache key 不包含父 snapshot 的发布序号。 缓存要求: - fingerprint 只用于定位候选 bucket 或加速拒绝;命中后必须比较结构化不可变 key。有限位 hash 相同但结构不同 的 definition、config、算法链或 schema 不能共享结果。 - 等价内容返回同一个 snapshot 引用。 - 并发等价请求只执行一次编译。 - 失败结果不进入缓存。 - 缓存由 ThemeManager 实例拥有,使用强引用 LRU,并同时受 entry 数量和保守 `EstimatedRetainedBytes` 上限约束;达到任一上限即回收最久未访问 entry,不区分 active/inactive。 - snapshot 的 retained bytes 根据稠密 Token 表、Control 引用表、差量、资源条目、palette 和不可变 Brush 的 确定性保守公式估算,不使用进程 RSS,也不声称等于 CLR 精确对象大小。 - 活动 ThemeContext 自己强持有当前 snapshot。cache eviction 只删除复用入口,不影响已发布 snapshot,因此不 需要 pin、PinCount 或反向 snapshot-key 表。 - Provider 实例不进入缓存,不在不同资源宿主之间共享。 - 不建立无上限的 Brush、字符串、resource key 或 fingerprint intern pool。 single-flight 编译记录与已完成 LRU entry 分离。记录在成功后才进入 LRU;失败、取消或 superseded 结果立即 释放。默认完整 snapshot cache 上限为 32 entry 和 32 MiB estimated retained bytes;Control compilation cache 上限为 2048 entry 和 32 MiB。应用可以调小;扩大默认值必须提供 benchmark、峰值对象数和 Gallery 导航内存证据。 ## 10. ThemeManager 与全局主题 `ThemeManager` 是唯一主题运行时协调者和唯一允许发布 snapshot 的对象。具体类型是 internal Avalonia `Styles` 宿主并实现公开的 `IThemeManager`;同一个实例同时挂载到 Application Styles 和主题服务容器,不再 创建同生命周期的一对一 `ThemeEngine` 或第二份 Theme Token 状态。 `ThemeManager` 只拥有长期运行时状态和编排职责:当前 `ThemeState`、根 `ThemeContext`、`ThemeScopeGraph`、 请求 generation/队列、Application ThemeVariant 发布以及结果事件。每个请求的阶段状态属于独立 `ThemeTransaction`;规范化、合并、编译、缓存和事件调用分别委托给对应对象。合并运行时所有者不等于把这些 职责的实现代码并入 `ThemeManager`。 作为 `Styles` 宿主,Manager 挂载根 Token Provider、Control themes 和语言资源入口;语言切换及资源构建委托给 Language 子系统,不进入 ThemeTransaction,也不能修改 `ThemeState`。Manager 不实现 Token 算法、XML 读取、 schema binding 或资源查询细节。 不存在供多个 Manager 共享的 headless engine,也不存在一个 Manager 切换多个 Styles 宿主的场景。纯计算复用 由 Compiler 和 Cache 提供,局部主题复用由 ThemeContext 和 ThemeScopeGraph 提供,因此额外 Engine 不形成 有效边界,只会引入 facade 转发、事件桥接和双向生命周期协调。 新公开模型由以下对象组成: - `ThemeInfo`:Catalog 中可选择主题的只读元数据。 - `ThemeRequest`:主题 id、`ThemeConfig` 和变更原因。 - `ThemeState`:当前已提交主题的 id、算法、`Appearance`、仅用于观察和快速比较的 fingerprint,以及 transition id。 - `ThemeTransitionResult`:一次请求的 transition id、`Committed / NoOp / Superseded / Failed` 状态、已提交 `ThemeState`、失败 diagnostics/exception 以及独立的 `PublishDiagnostics`。 - `IThemeManager.ApplyThemeAsync(...)`:返回 `Task` 的运行时主题请求入口。 - `IThemeManager.CurrentTheme`:只读 `ThemeState`。 - `IThemeManagerBuilder.WithInitialTheme(...)`:首帧主题 id 和 ThemeConfig 的唯一配置入口。 删除 `Theme` 和 `ITheme`。Catalog 只保存 definition,不为 Default、Dark、Compact 预先创建 Theme 对象。 每个请求都由 definition、配置和算法构成规范化输入,因此同一主题的新 runtime override 必须重新查询缓存 或编译。 AtomUI 主题身份与 Avalonia `ThemeVariant` 解耦: - AtomUI 使用 `ThemeRequest` 表达主题 id 和算法。 - `Application.RequestedThemeVariant` 只根据已提交 snapshot 设置为 Avalonia Light 或 Dark。 - Compact 和自定义主题 id 不编码进 `ThemeVariant` 名称。 - `Application.RequestedThemeVariant` 是 ThemeManager 的发布输出;应用不得把它作为第二个主题写入口。 - 跟随系统主题是一种明确的 Manager 策略,不通过双向绑定或让 Application 长期保持 `ThemeVariant.Default` 实现。 FollowSystem 在 Builder 阶段获得 Light 和 Dark 两个完整、不可变的 root request 模板。Manager 监听平台外观 变化,先根据目标外观 Capture/Prepare 对应 snapshot,再在 Publish 阶段显式设置 `Application.RequestedThemeVariant=Light/Dark`。平台先改变 appearance、AtomUI 后编译 Token 的中间状态不得 暴露。初始系统 appearance 必须在首个 snapshot 编译前解析。 Root ThemeContext 通过 ThemeManager 中的全局 TopLevel style 把稳定的 `ThemeScope.ContextProperty` 注入每个 TopLevel;不能假设 inheritable AvaloniaProperty 会从 `Application` 自动传到独立 TopLevel。由 owner lease 创建的局部 TopLevel 使用 local value 覆盖该根 style 值。 Motion 和 Wave 配置也是 ThemeConfig Token override。任何入口改变这些值时都生成新请求,不直接修改 `ResourceDictionary`。 ## 11. 动态作用域 ### 11.1 稳定 ThemeContext `ThemeContext` 是稳定的作用域身份,包含: - 当前 `ThemeSnapshot` 的原子引用。 - 唯一的 `ThemeTokenResourceProvider`。 - scope identity 和只读 appearance。 - Publish 阶段使用的内部 context changed 通知。 `ThemeContext` 不复制或修改 Token。snapshot 仍是 Token 值唯一真源。 `ThemeScope.ContextProperty` 是 inheritable attached property,在树上传递稳定 context,而不是每次主题变化都 替换 attached snapshot。根值由全局 TopLevel style 注入;局部值由 ThemeConfigProvider 设置。没有局部 Provider 时,控件解析到根 `ThemeContext`。 `ThemeConfigProvider` 继承 Avalonia `ThemeVariantScope`,从而同时拥有局部 AtomUI ThemeContext 和局部 Avalonia variant 边界。它的 Resources 在 attach 期间只挂载一个稳定 Token Provider;更新时不替换 Provider, 只在 CommitCore 替换 ThemeContext snapshot,并在 Publish 设置 `RequestedThemeVariant` 为 snapshot 的 Light 或 Dark appearance。 ### 11.2 ThemeScopeGraph `ThemeManager` 拥有一个活动 `ThemeScopeGraph`: ```text +---------------------------+ | ThemeManager | | owns topology and commits | +-------------+-------------+ | v +---------------------------+ | Root ThemeContext | | snapshot + provider | +-------------+-------------+ | +------+------+ | | v v +-------------+ +-------------+ | Scope A | | Scope B | | context A | | context B | +------+------+ +-------------+ | +---+---+ | | v v +----------+ +----------+ | Scope A1 | | Scope A2 | +----------+ +----------+ ``` 每个 `ThemeScopeNode` 保存: - 单调递增且本次注册唯一的 `RegistrationId`,防止 detach/reattach ABA。 - `ParentRegistrationId` 和子节点集合。 - 每次 Config 替换递增的 `ConfigRevision`。 - 当前不可变规范化配置和 `LastValidConfig`。 - 稳定 ThemeContext、Provider 和 ThemeConfigProvider。 `ThemeScopeGraph.TopologyRevision` 在 attach、detach 或 reparent 时递增。Capture 必须记录 root generation、 TopologyRevision,以及每个受影响节点的 `(RegistrationId, ParentRegistrationId, ConfigRevision)`。Prepare 结果 提交前必须逐项验证;任一值变化都使旧结果成为 `Superseded`,不能提交到新拓扑。 `ThemeConfigProvider` 通过 ThemeManager 的 internal scope registration contract 注册节点;attach 时注册, detach 时注销,父级变化或内容重新挂接时更新图关系。Provider 负责提交当前可观察配置变化,但不通过静态 `ThemeManager.Current` 查找具体实现,不自行监听父 snapshot,不直接调用 compiler,也不自行实现事件队列。 全局主题变化时,ThemeManager 使用新根 snapshot 和各节点最后一次有效配置,按拓扑顺序准备整个作用域图。 局部配置变化时,只准备该节点及其后代。 所有根和局部事务共用一个 Manager-wide scheduler 和 Commit gate。同一 Manager 同时最多有一个 Prepare 管线和一个 CommitCore;新请求按作用域合并,根请求覆盖尚未提交的全部后代准备请求,从而限制编译峰值并 消除根、局部交错提交。 配置输入无效时: - 当前作用域保留已发布 snapshot。 - `LastValidConfig` 不更新。 - 派发失败事件和 diagnostics。 - 后续父主题变化使用 `LastValidConfig`,避免该作用域永久冻结在旧全局主题。 - 首次 attach 尚无已发布局部 snapshot 时,使用父 snapshot 作为安全回退,并把空 `Inherit=true` 配置作为 fallback;无效局部输入不能造成空资源或首帧崩溃。 普通 Popup、Flyout 只要保留 Avalonia 逻辑父级,就自然继承 `ThemeScope.ContextProperty`、资源查询链和 ActualThemeVariant,不创建额外 bridge。 独立 Window、Dialog、Notification host 和其他新 TopLevel 必须通过 owner 创建 `ThemeContextLease`。Lease 在宿主生命周期内: - 在 TopLevel 设置 owner 的 ThemeContext local value 和 snapshot appearance。 - 挂载一个宿主私有 `ThemeContextResourceBridge`,只委托给 owner context,不复制 Token。 - 订阅 owner context Publish 通知并向该 TopLevel 传播最小资源通知。 - owner 改变、窗口关闭或 API dispose 时对称移除 bridge、属性值和订阅。 AtomUI Desktop `Window` 在调用 Avalonia 的 `Show`、`Show(owner)` 或 `ShowDialog(owner)` 之前完成 lease 获取, 因此 `WindowOpenedEvent`、样式应用和首个布局都只能看到完整的 Context、显式 Light/Dark variant 和私有 bridge。 带 owner 的重载从 owner 解析 Context;无 owner 的根 Window 使用 Root ThemeContext。若 Avalonia 打开流程同步失败, 本次新建 lease 必须在异常离开前释放;`OnClosed` 再执行幂等释放,不能依赖外部调用者补清理。 `ThemeTokenResourceProvider` 仍是每个 scope 唯一的规范 Provider;ResourceBridge 是独立资源根的非拥有代理, 不进入 cache,也不形成第二份 Token 状态。无 owner 的静态 API 只能显式租用 Root ThemeContext,并应提供 diagnostic,不能根据调用栈、当前焦点或全局静态变量猜测局部上下文。 ## 12. 事务与发布顺序 一次主题事务分为 Capture、Prepare、CommitCore、Publish、Complete 五个阶段: ```text +-------------+ +-------------+ +-------------+ +-------------+ +-------------+ | Capture |->| Prepare |->| CommitCore |->| Publish |->| Complete | | UI thread | | pure work | | UI thread | | UI thread | | result task | +-------------+ +-------------+ +-------------+ +-------------+ +-------------+ | immutable | | merge/cache | | silent swap | | variants | | committed / | | graph stamp | | compile all | | all contexts| | resources | | no-op / ... | | affected set| | staged data | | state refs | | events | | | +-------------+ +-------------+ +-------------+ +-------------+ +-------------+ ``` Capture 完成不可变 Config 绑定、scope stamp 和 affected set;Prepare 只执行纯合并、single-flight cache 和 完整子树编译。进入 CommitCore 前,Manager 在 UI 线程再次验证 generation、TopologyRevision 和全部 node stamp。 CommitCore 只执行不会调用外部代码的引用和值交换:一次性替换所有受影响 ThemeContext snapshot、 ThemeState 和内部 appearance。它不设置 AvaloniaProperty、不调用 `RaiseResourcesChanged`、不派发 context 或 结果事件。实现必须预先分配 staged 数组并完成所有可失败工作,使 CommitCore 成为无失败、无分配、无通知的 有限赋值序列。 `Application.RequestedThemeVariant`、局部 `ThemeVariantScope.RequestedThemeVariant`、context changed、 ResourceProvider 通知和结果事件都属于不可回滚 Publish。Publish 开始前所有 context 已指向完整新 snapshot, 因此 Avalonia 同步回调也只能读取完整新状态。不能再承诺在 `RequestedThemeVariant` 或资源通知之后静默回滚。 Publish 的每个外部通知边界必须独立捕获、记录并继续后续通知。观察者异常不把已提交事务改写成 Failed;结果 仍为 `Committed`,并携带 publish warning diagnostics。只有 Capture、Prepare 或 CommitCore 前的 stamp/invariant 验证失败可以返回 `Failed` 或 `Superseded`,且此时尚未交换任何已发布引用。 同一作用域的新请求使用 generation 标识。旧的异步准备结果在提交前发现 generation 已过期时直接丢弃。 提交期间发生的重入请求进入队列,同一作用域尚未执行的请求合并为最新请求。 `ApplyThemeAsync` 的请求完成语义固定如下: - `Committed`:新 snapshot 已完整提交,全部 Publish 边界均已按顺序尝试;资源或观察者异常作为 `PublishDiagnostics` 返回,不改变该状态。 - `NoOp`:规范化后的目标状态与当前状态结构相等;不创建 snapshot,不发送资源通知或结果事件。 - `Superseded`:请求的 generation 在 Commit 前被更新请求替代;不发布该请求的状态,也不能伪装成成功。 - `Failed`:Capture、Prepare 或 CommitCore 前验证失败;旧状态从未被修改,`ThemeChangeFailed` 已派发。 主题转换自身的失败通过 `Failed` 结果表达,不以异常代替结果。调用参数违反 API 前置条件仍可以同步抛出;调用 方取消等待时,仅该调用返回取消完成,不把底层 transaction 改写为 `Failed`。 等价并发请求加入同一个 in-flight transaction,共享相同 transition id 和 Prepare/Commit 结果。调用方传入的 `CancellationToken` 只取消该调用方的等待,不得取消已经开始的 Commit,也不得把其他等待者的请求取消。 Prepare 阶段只有在所有等待者都已取消且事务尚未进入 Commit 时,Manager 才可以取消底层工作;新 generation 替代旧 generation 时,仍按 `Superseded` 完成未取消的旧等待者。 结果顺序也属于公开契约:`Committed` 先按顺序尝试 variant、context、资源通知和 `ThemeChanged` 的全部 Publish 边界,再携带可能的 warnings 完成对应 Task;`Failed` 先派发 `ThemeChangeFailed`,然后对应 Task 才完成; `NoOp` 和 `Superseded` 不派发主题结果事件,直接完成结果。 ## 13. 资源解析 根 ThemeContext 的 Provider 在 ThemeManager 初始化时挂载一次。每个局部 ThemeContext 也只拥有一个 Provider。主题切换不追加 `MergedDictionaries`,不创建新的 Provider。 Root Provider 由 ThemeManager 的 Styles 资源拥有;局部 Provider 由对应 ThemeConfigProvider.Resources 拥有;独立 TopLevel 通过自己的 ResourceBridge 委托给 owner context。Provider 和 bridge 都忽略调用方传入的 自定义 AtomUI theme id,只使用已提交 snapshot;Avalonia 的 `ThemeVariant` 参数仍用于同一资源树中的普通 ThemeDictionary 查询。 查询规则: ```text +----------------------+ | Resource key | +----------+-----------+ | +---------------------+---------------------+ | | | v v v +-------------------+ +-------------------+ +-------------------+ | SharedTokenKind.X | | Control shared | | Control own | +---------+---------+ +---------+---------+ +---------+---------+ | | | v v v GlobalResources[X] Control delta[X] ControlResources[X] | miss | v GlobalResources[X] ``` ControlTheme 中消费的全局 Token 必须使用对应 Control 的共享 Token 复合资源键。普通应用内容和没有 Control Token 身份的通用样式继续使用 `SharedTokenKind`。 ### 13.1 AXAML 访问契约 Token 资源的公开 AXAML 语法固定为: ```xml ``` 禁止生成或使用以下把 Control identity 拼进 MarkupExtension 类型名的语法: ```xml ``` `SharedTokenResourceExtension` 根据所在主题资产的 ambient Control token scope 选择 key: - 没有 Control token scope 时,返回普通 `SharedTokenKind`,读取当前 ThemeContext 的全局资源。 - 存在 Control token scope 时,使用 `(ControlTokenIdentity, SharedTokenKind)` 获取缓存后的内部复合 key,先读取 对应 Control 的有效共享 Token 差量,miss 时回退全局资源。 - Control 自身 Token 继续使用生成的 `Resource` 扩展,不经过共享 Token fallback。 Control token scope 在一个主题资产边界只声明一次,不在每个 Setter 上重复。Theme 资源文件使用单一、生成式 `ControlTokenScope.Identity` 把 ResourceDictionary 或 ControlTheme 关联到已注册的 `ControlTokenIdentity`: ```xml ``` `ControlTokenScope` 只是 AXAML 构建阶段的主题资产元数据,不创建 ThemeContext、ResourceProvider 或控件实例 作用域,也不参与运行时 Visual 继承。 直接以 `ControlTheme` 为根时在该 ControlTheme 上声明相同 Identity。一个 ResourceDictionary 中存在多个 Control token family 时,应在各自最近的 ControlTheme 上声明;最近的 scope 优先。Identity 必须是生成器为 该资产声明的静态值,禁止使用两个松散的 Catalog/Id 字符串,也禁止根据 TargetType、Token CLR 类型、文件 路径或运行时反射猜测 identity。 MarkupExtension 在 AXAML 构建时从 ambient parent context 捕获 scope,并生成缓存后的稳定 key。资源查询热路径 仍然只接收 key 和 ThemeVariant,不遍历 Visual、不查找 templated parent,也不进行字符串解析。缺少 scope 本身 是合法的全局共享 Token 用法;AtomUI 自有 ControlTheme 资产必须由静态资源审计确认其声明了正确 scope,避免 Control override 被静默当作全局查询。 ambient 行为必须覆盖 eager AXAML、deferred ControlTemplate、ResourceInclude、ThemeDictionary 和以 ControlTheme 为根的资产。不能仅凭普通 ResourceDictionary 样例推断 deferred 场景正确。生成器或构建 analyzer 为每个资产生成 `ControlThemeAssetDescriptor`,并在构建期执行以下校验: - 资产声明的 Identity 与 Provider 注册的 manifest identity 一致。 - 使用 Control 自身 Token Resource 的资产必须属于相同 Control identity。 - 使用 `SharedTokenResource` 的 AtomUI ControlTheme 必须能在最近资产边界解析到唯一 identity。 - identity 未注册、多个 identity 冲突、deferred parent stack 丢失或资产 URI 重复时构建失败。 运行时注册只加载已经通过 manifest 校验的 descriptor,并再次对照当前 RegistryRevision;它不重新扫描 AXAML 文本。`SharedTokenResourceExtension.ProvideValue` 只在构建阶段解析 ambient identity,并从当前 registry 的 有界预生成 key 表取得稳定 boxed key。 控件实例不创建 Token `ResourceDictionary`,不注册 Control 自己的资源作用域,也不订阅全局主题事件。 Control 配置只修改当前 `ThemeContext` 中指定 `ControlTokenIdentity` 的 Token 投影,不覆盖全局 `SharedToken`,也不由某个控件实例创建私有子作用域。当前 `ThemeContext` 子树中的所有同 identity Control, 包括位于用户 Content 或嵌套控件中的实例,都使用该 Control 配置;其他 Control identity 和子 `ThemeConfigProvider` 的独立 ThemeContext 按各自有效配置解析。 ## 14. Imperative Token 消费 少数控件需要在 C# 中计算 palette、断点或运行时绘制值。它们使用 `ThemeTokenResolver`: - Resolver 必须以明确的 `AvaloniaObject` owner 或 `ThemeContext` 为入口;不能通过 `ThemeManager.Current`、当前焦点、调用栈或 Visual 全局扫描猜测作用域。 - Resolver 从 owner 继承的稳定 `ThemeContext` 原子读取一次 snapshot;同一次布局、绘制或计算必须只使用该 snapshot,不能在多个 Token 查询之间重新读取 context 而混合两个事务的值。 - 全局 Token 使用 `SharedTokenKind` 强类型查询。 - Control Token 使用生成 descriptor 中的 `ControlTokenIdentity` 和强类型 Token kind 查询。 - palette 读取 snapshot 中不可变的 `PresetColorPalettes`。 - Resolver 不缓存第二份 Token 对象图,也不把 snapshot、Provider 或 owner 放入静态缓存。 - 需要动态更新时订阅当前 `ThemeContext` 的内部 Publish 通知,并在 detach、re-template 或 owner 变化时释放 subscription;控件不订阅 `ThemeManager.ThemeChanged`。 - 独立 TopLevel 中的 imperative 查询使用 lease 注入的同一个 owner ThemeContext。无 owner 的静态 API 只能 显式解析 Root ThemeContext,并遵守根主题语义。 优先把固定视觉结果编译为 Control 资源。只有值确实参与控件运行时算法时才使用 resolver。 ## 15. 事件契约 新主题系统只保留结果事件: - `ThemeChanged`:事务已完成 CommitCore,variant、context 和资源通知已经发布后派发;携带 request、scope、 transition id、已提交 `ThemeState` 以及派发到该事件之前累计的 Publish warning diagnostics。 - `ThemeChangeFailed`:Capture、Prepare 或 CommitCore 前的 stamp/invariant 验证失败,且任何已发布引用都尚未 交换时派发;携带 request、scope、transition id、diagnostics 和 exception。 不提供可取消的 Changing 事件,也不保留 Created、AboutToLoad、Loaded、AboutToChange、Unload 等伪生命周期。 事件由统一 `ThemeEventDispatcher` 派发: - 每个订阅者独立调用。 - 一个订阅者异常不阻止后续订阅者。 - Publish 中设置 AvaloniaProperty、context 通知、资源通知或事件订阅者抛出的异常统一转换为 warning diagnostic,并通过 Avalonia Logger 记录完整上下文。 - Publish warning 不触发 `ThemeChangeFailed`,不回滚 snapshot、variant 或已发出的通知,也不把成功提交改写为 主题失败。 - `ThemeChanged` 订阅者自身产生的 warning 无法回填给已经开始执行的同一事件参数;它必须进入最终 `ThemeTransitionResult.PublishDiagnostics`,供请求调用方观察。 - 事件处理期间发起的请求遵守 ThemeManager 重入队列。 全局事件由 `IThemeManager` 暴露;局部结果由对应 `ThemeConfigProvider` 暴露。两者使用相同 event args 和 派发规则。内部 `ThemeContext` changed 通知只用于 ResourceBridge 和 imperative 消费者,不是第二套公开主题 生命周期事件。 ## 16. 启动时序 `Application.UseAtomUI()` 的主题启动顺序固定为: 1. Builder 收集主题包、主题文件来源、算法 descriptor、Control theme asset manifest、不可变初始 root request 模板和语言配置。 2. 构建并冻结 `ThemeSchemaRegistry`;在此时拒绝重复 Control identity、无效 descriptor、资产 identity/URI 冲突以及 manifest 与 registry 不一致。 3. Catalog 读取并绑定主题定义,确定默认主题和 definition revision。 4. 如果启用 FollowSystem,先同步读取初始系统 appearance,再从 Light/Dark 不可变 request 模板中选择首帧 request;不得先按 Default 编译后再补一次 Dark/Light 切换。 5. 同步完成初始 request 的 Capture/Prepare,生成第一个完整根 `ThemeSnapshot`。 6. 使用该 snapshot 创建 Root ThemeContext、稳定 Provider 和唯一 ThemeManager;Manager 的 Styles 同时预装 Control themes、语言资源入口以及向全部 TopLevel 注入 Root ThemeContext 的全局 style。 7. 将同一个已完整初始化的 ThemeManager 实例挂载到 Application Styles,并绑定为 `IThemeManager` 服务。 8. 显式设置与 snapshot appearance 一致的 Avalonia Light/Dark variant,并且只发布一次初始 Token 资源通知。 9. 执行与主题生命周期无关的模块初始化。 不得先挂载空资源层再异步补主题,也不得通过 ThemeLoaded 事件修补 DefaultFont、断点或其他 Token。 `ThemeConfigProvider` 首次 attach 也必须在其内容进入首个可见帧前同步解析父 ThemeContext、准备局部 snapshot、 初始化稳定 Provider,并设置与 snapshot 一致的 `RequestedThemeVariant`。首次 Config 无效时直接使用父 snapshot 和父 appearance 作为安全回退;不能先暴露空 context,再依赖后续异步事务纠正。 ## 17. 线程与生命周期 - `ThemeConfig`、`ControlThemeConfig` 和 `ThemeRequest` 深度不可变。配置输入只在 UI 线程捕获完整引用;Provider 只观察 `Config` 属性替换,不订阅 Config 内部集合或 setter 对象。 - ThemeCompiler 不访问 UI 对象,可以在后台准备运行时变更。 - 初始根主题和 ThemeConfigProvider 首次 attach 同步准备,避免首帧闪烁。 - 运行时更新可以后台编译,但 CommitCore、Publish 和 Complete 排序必须回到 UI 线程。 - Capture 的 generation、TopologyRevision 和 node stamp 只描述当次不可变图快照;Prepare 不得持有可变 ScopeNode 集合。提交前 graph stamp 不一致时必须丢弃 staged result,不能尝试修补后提交。 - Scope attach、detach、reparent 和 Content 替换必须在 ThemeScopeGraph 中对称登记和释放。 - snapshot 不持有作用域、Control、Visual 或事件订阅。 - Provider 生命周期归所属 ThemeContext,不进入全局静态缓存。 - Root ThemeContext 生命周期归 ThemeManager;局部 ThemeContext/Provider 生命周期归已注册 ThemeConfigProvider;Manager dispose 时按拓扑逆序释放局部注册、bridge 和根资源宿主。 - 普通 Popup/Flyout 依靠逻辑父级继承,不创建 lease。独立 TopLevel 的 `ThemeContextLease` 和 `ThemeContextResourceBridge` 由宿主 owner 关系拥有;owner 改变、关闭、异常打开或 dispose 都必须对称替换或 释放 local value、资源挂载和 context subscription。 - lease 只强持有其活动宿主和 owner context,不反向让 owner Visual、已关闭 TopLevel 或旧 snapshot 超过宿主 生命周期存活。 ## 18. 性能与内存模型 ### 18.1 数据布局 源生成器为全局 Token、每个 Control Token 和注册 Control 分配稳定的整数 slot。编译器内部和 snapshot 使用 slot 索引,不在热路径使用 Token name、`PropertyInfo` 或 object-key 字典。 数据布局遵循以下原则: - 全局 Token 值只在 snapshot 中保存一次。 - Control snapshot 只保存 Control 自身 Token 值和相对全局 Token 的有效差量。 - 不为每个 Control 保存一份完整全局 Token 表。 - 未修改的 palette、不可变 Brush、全局 Token 表和 Control snapshot 在父子 snapshot 之间共享引用。 - Control 表可以复制固定长度的引用数组,但不得复制未变化 Control 的值表和资源表。 - `ThemeSnapshot` 不缓存面向兼容 API 的第二份对象图。 目标内存复杂度是: ```text root snapshot = O(global token slots + all control-own slots + control deltas) local scope = O(global changes + changed control values + control reference table) runtime = O(active scopes + bounded cache + one in-flight transaction) ``` 禁止退化为 `O(scope count * control count * global token count)` 的完整 Token 克隆模型。 ### 18.2 增量编译与结构共享 `ThemeConfigMerger` 除有效配置外还输出确定的 change set: - 算法或全局 Token 改变时,重新计算全局表和所有依赖全局值的 Control。 - 只修改某个 Control 配置时,仅重新计算该 Control。 - 只修改局部作用域 Button 时,复用父 snapshot 的全局表、palette、资源表和其他 Control snapshot。 - 配置规范化后与当前有效配置相等时直接 no-op,不创建 snapshot,不通知资源。 - 空配置且 `Inherit=true` 的 ThemeConfigProvider 可以直接共享父 snapshot。 单 Control 局部覆盖的结构共享结果如下: ```text Parent snapshot P +-- Global table G +-- Control[Button] B1 +-- Control[Input] I1 \-- Control[...] N1 Child snapshot C (Button override only) +-- Global table G same reference as P +-- Control[Button] B2 new value +-- Control[Input] I1 same reference as P \-- Control[...] N1 same references as P ``` 编译缓存分两级: - 完整 snapshot cache:复用完全等价的有效主题。 - Control compilation cache:以 global token 结构化 key、Control identity、Control config 结构化 key、 algorithm `(id, revision)` 和 registry revision 为 key,复用跨作用域的 Control 计算结果;fingerprint 只用于 候选定位,命中后仍执行结构相等比较。 两级缓存共用容量策略,失败和取消结果不缓存。 默认容量由 `ThemeCacheOptions` 固定为: - 完整 snapshot cache:最多 32 个 entry,最多 32 MiB `EstimatedRetainedBytes`。 - Control compilation cache:最多 2048 个 entry,最多 32 MiB `EstimatedRetainedBytes`。 每个 entry 在插入前按第 9 节的保守公式计算 retained bytes;共享引用允许重复计入以维持上界保守性,但不能 通过抽样 RSS、GC generation 或固定“每个 Token 一个单位”替代。任一 entry 单独超过对应 bytes 上限时可以返回 给当前事务使用,但不得进入 LRU。 容量可以由应用显式调小;扩大默认值必须附带 benchmark、峰值对象计数和 Gallery 导航内存结果。 ### 18.3 编译分配 - 编译前根据 schema 中已知的 Token 和 Control 数量预分配数组及稀疏字典容量。 - 临时 builder 使用 copy-on-write global token overlay,Control 计算不得深克隆完整 `DesignToken` 对象图。 - 大型临时数组可以由有界 workspace pool 复用,但 pooled buffer 绝不进入 snapshot。 - 取消、异常和 stale generation 路径必须在 `finally` 中归还 workspace。 - 同一 ThemeManager 同时只运行一个根事务准备管线;后续请求合并,避免并发编译造成内存峰值。 - 初始同步编译完成后及时释放 ThemeDocument 中不再需要的 XML 对象和临时字符串。 ### 18.4 资源查询热路径 `ThemeTokenResourceProvider.TryGetResource` 是控件布局和样式更新的热路径,要求: - global、Control shared 和 Control own 命中查询均为零托管分配。 - `SharedTokenResourceExtension` 在 AXAML 构建阶段根据 ambient scope 获取缓存后的稳定 key;资源查询时不创建 key、不拼接字符串,也不解析 Control identity。 - `ControlSharedTokenResourceKey` 直接包含 `ControlTokenIdentity` 或 registry slot,不在每次查询时重新构造身份。 - `SharedTokenKind` 对应的全局资源 key 在 `ThemeSchemaRegistry` 冻结时预装箱并按 slot 保存;Control shared 查询 必须复用该对象,禁止把枚举传给 `object` 参数造成每次查询装箱。 - 查询只执行类型分支、slot 索引和最多一次稀疏差量 fallback。 - miss 不创建 diagnostics、异常或临时集合。 - 一次事务对每个受影响 Provider 最多调用一次 `RaiseResourcesChanged()`。 资源通知使用最小通知前沿: - 根事务只通知 Root Provider;Application/TopLevel 的既有资源传播会使普通逻辑树中的局部 Provider 重新查询, 不再逐个通知所有后代 scope。 - 局部事务只通知 affected subtree 的最高变化节点 Provider;其后代 snapshot 已在 CommitCore 替换,随父通知 重新查询。 - 脱离该逻辑树的活动 ThemeContextResourceBridge 需要各通知一次,因为它们是独立资源根。 - appearance 变化的每个显式 ThemeVariantScope 仍必须设置自己的 RequestedThemeVariant;子 scope 的 local value 会阻断父 variant 继承,不能只更新根。 ### 18.5 生命周期与可回收性 ThemeScopeGraph 在注册有效期间强持有 ScopeNode 和对应 ThemeConfigProvider;Provider detach 时必须通过 注册 token 同步移除节点和全部边。Runtime 不保存已 detach 节点历史。 - 一个活动 ThemeConfigProvider 对应一个 ScopeNode、ThemeContext 和 ResourceProvider。 - ScopeNode 除已 attach 的 ThemeConfigProvider 外不能单独持有 Content、后代 Control 或其他 Visual。 - ThemeConfigProvider 持有 registration disposable;detach、reparent 和异常 attach 路径都必须释放它。 - 一个活动独立 TopLevel 对应一个 `ThemeContextLease` 和一个宿主私有 `ThemeContextResourceBridge`;bridge 只持有 owner ThemeContext 和通知 subscription,不复制 snapshot 或 Token 表。 - owner 变化必须先建立新 lease 的完整 local context/variant/resource bridge,再释放旧 lease;关闭或创建失败 后不得在 owner context 的订阅列表中残留 bridge。 - imperative Token 订阅返回 `IDisposable`,由控件在 detach、re-template 或 owner 变化时释放。 - 非 Visual `AvaloniaObject` 使用 DynamicResource 时必须有 scoped `IResourceHost` 和对称 release 路径。 - 普通 Popup、Flyout 和逻辑树内 overlay 不额外创建 ThemeContext 传播订阅;仅独立资源根使用 lease/bridge。 - static cache 只能持有 schema、descriptor 和不可变无 UI 值,不得持有 ThemeContext、Provider、Control 或 Visual。 - 事件 dispatcher 不保留已完成 transaction、event args 或 invocation list。 旧 snapshot 的可达数量上界必须由“活动 context 数量 + 有界 cache entry + 当前事务 staged snapshot”解释。 无法解释的额外存活 snapshot 视为内存泄漏。 ### 18.6 性能验证 主题系统必须建立可重复的 benchmark 和 allocation 场景: - 冷启动:registry、主题文件绑定、默认主题编译和首次挂载。 - 热启动:相同 definition 和 config 的缓存命中。 - 全局切换:Default、Dark、Compact、Dark + Compact。 - 局部变更:只覆盖一个 Control、只覆盖一个全局 Token、十层嵌套作用域。 - 高频变更:连续 100 次配置更新后的请求合并和 stale result 丢弃。 - 拓扑竞争:Prepare 期间执行 scope detach、reattach、reparent 和 Config 替换,验证旧 graph stamp 从不提交。 - FollowSystem:启动时分别模拟 Light/Dark,并连续切换平台 appearance,验证不存在 `Default` variant 或 snapshot/variant 中间不一致。 - 资源查询:global、Control shared、Control own 的 hit、fallback 和 miss。 - 独立 TopLevel:根 owner、局部 owner、owner 替换和无 owner 静态 API 的资源与 appearance 语义。 - 生命周期:100 次 Provider attach/detach、Popup/Flyout open/close、独立 TopLevel lease/bridge 创建释放和 Gallery 页面导航。 验收指标至少包括: - `TryGetResource` 稳态查询为 0 B/op。 - no-op 请求不创建 snapshot、Provider、Context,不派发资源或主题事件。 - 单 Control 局部覆盖只产生一个新 Control snapshot,其他 Control snapshot 引用保持不变。 - 重复更新后每个作用域仍只有一个 Provider。 - 两级 cache 的 entry 数量和 `EstimatedRetainedBytes` 始终不超过各自配置上限;超大单 entry 不进入 LRU。 - attach/detach 后通过 WeakReference 和强制 GC 验证 ScopeNode、ThemeContext、Provider 及旧 Content 可回收。 - 独立 TopLevel 关闭或 owner 替换后,通过 WeakReference 和订阅计数验证 lease、bridge、旧 owner Visual 和旧 snapshot 可回收。 - Gallery 随机导航稳定后旧 ShowCase、DynamicResourceExpression 和主题作用域对象数量不单调增长。 - RSS 只作为辅助信息;对象数量和可达链才是内存泄漏判据。 ## 19. AOT 约束 - 内置 Token 注册和 schema 全部由源生成器生成。 - 不运行时扫描程序集或 Token 属性。 - 不使用 `Activator.CreateInstance` 创建内置 Control Token。 - 不使用字符串属性路径 Binding 完成主题配置或 Token 查询。 - 自定义算法和第三方 Control Token 必须提供稳定 descriptor id;自定义算法还必须提供显式 revision/version。 - 生成器测试必须验证 identity、继承 Token、强类型 setter、资源投影和输出稳定性。 - 主题系统完成后必须执行真实 Gallery NativeAOT publish,不能只依赖 analyzer。 ## 20. 模块组织 主题源码按稳定职责边界组织,不为少量运行时入口创建目录,也不保留 `Catalog`、`Scope`、`Engine`、 `ControlThemes`、`Styling`、`Transitions` 或 `TokenSystem` 这种过细、语义重叠或已经失真的目录。 ```text Theme/ | +-- IThemeManager.cs 公开主题管理入口 +-- IThemeManagerBuilder.cs 公开注册入口 +-- ThemeManager.cs 唯一运行时协调者、snapshot 发布者和 Styles 宿主 +-- ThemeManagerBuilder.cs 启动组装器 +-- ThemeConfigProvider.cs 局部主题配置宿主 +-- ThemeTransaction.cs Capture/Prepare/CommitCore/Publish/Complete 事务 +-- ThemeState.cs 当前已提交主题状态 +-- ThemeTransitionResult.cs 事务结果和值状态 +-- ThemeChangedEventArgs.cs 成功结果事件 +-- ThemeChangeFailedEventArgs.cs 失败结果事件 +-- ThemeDiagnostic.cs 结构化诊断 +-- ThemeContext.cs 稳定主题上下文 +-- ThemeContextLease.cs 独立 TopLevel 的 owner context 生命周期租约 +-- ThemeContextResourceBridge.cs 独立资源根到 owner context 的稳定资源代理 +-- ThemeScope.cs 唯一可继承的 Context attached property +-- ThemeScopeGraph.cs 局部主题作用域图 +-- ThemeScopeNode.cs 作用域节点 +-- ThemeRequest.cs 主题变更请求 +-- ThemeEventDispatcher.cs 结果事件派发 +-- ThemeExceptions.cs 公开错误类型 +-- ThemeAppearance.cs snapshot 的 Light/Dark 外观 +-- ThemeInfo.cs 可用主题元数据 | +-- Algorithms/ | +-- 算法 descriptor、attribute、接口与内置算法 | +-- Default、Dark、Compact 计算器 | \-- 调色板、颜色映射和算法计算辅助 | +-- Configuration/ | +-- ThemeConfig、ControlThemeConfig | +-- ControlAlgorithmMode | +-- Normalizer、Merger 和 immutable normalized config | \-- 一次性 immutable config builder 与结构化 value key | +-- Definitions/ | +-- 主题源、ThemeInfo、definition revision 和默认主题选择 | +-- XML reader、syntax document 和 diagnostics | \-- Binder、typed ThemeDefinition 和 bind result | +-- Schema/ | +-- ThemeSchemaRegistry | +-- TokenDescriptor、ControlTokenDescriptor | +-- ThemeAlgorithmDescriptor | +-- ControlThemeAssetDescriptor 和 asset manifest contracts | \-- identity、stage、parser 和 resource projector contracts | +-- Compilation/ | +-- CompileInput、Normalizer output consumption | +-- ThemeCompiler、TokenValueTable | +-- ThemeSnapshot、ControlThemeSnapshot | \-- bounded snapshot/control compilation caches | +-- Resources/ | +-- ThemeTokenResourceProvider、ThemeTokenResolver | +-- SharedTokenResource、ControlTokenScope、Control own resource extensions 和内部复合 keys | +-- Control theme provider contracts and attributes | +-- BaseControlTheme and provider aggregation | \-- snapshot lookup、style/template 和 Control theme 加载辅助 | +-- DesignTokens/ | +-- global/control compile-only builders | +-- Token definitions and kind metadata | \-- Token value converters ``` `AtomUI.Generator` 根据每个控件包的 AXAML 资产生成 `ControlThemeAssetDescriptor`、manifest 和稳定 `ControlTokenScope.Identity` 引用,并通过构建 analyzer 校验 deferred template、ResourceInclude、 ThemeDictionary 和 ControlTheme 根场景。运行时 `Theme/Schema` 只消费生成结果,不解析 AXAML 文本,也不通过 反射补全缺失 identity。 `Definitions` 同时拥有主题来源和主题定义,不再用单独的 `Catalog` 目录把一次加载流程拆开。 `ThemeManager`、事务、上下文和作用域图都属于主题子系统的核心运行时入口,直接位于 `Theme` 根目录; 这些类型共享同一个 snapshot 提交边界,但不需要额外的 `ThemeEngine` 长期对象或物理目录表达这一事实。 物理目录与 CLR namespace 必须严格一一对应: | 物理边界 | CLR namespace | |---|---| | `Theme/*.cs` | `AtomUI.Theme` | | `Theme/Algorithms/**` | `AtomUI.Theme.Algorithms` | | `Theme/Compilation/**` | `AtomUI.Theme.Compilation` | | `Theme/Configuration/**` | `AtomUI.Theme.Configuration` | | `Theme/Definitions/**` | `AtomUI.Theme.Definitions` | | `Theme/Resources/**` | `AtomUI.Theme.Resources` | | `Theme/Schema/**` | `AtomUI.Theme.Schema` | | `Theme/DesignTokens/**` | `AtomUI.Theme.DesignTokens` | 职责目录下用于拆分 partial class 或文件数量的组织子目录不继续创建 CLR namespace,例如 `Theme/DesignTokens/Definitions/**` 仍使用 `AtomUI.Theme.DesignTokens`。根目录不保留 `.Scope`、 `.Transitions` 等已经被物理结构删除的 namespace。 `Theme/Algorithms` 与 `AtomUI.Theme.Algorithms` 一一对应:`ThemeAlgorithm`、算法 contract/attribute、内置 calculator、调色板生成、预设色、`ColorMap` 和计算辅助均归该命名空间所有。调色板不是独立运行时子系统, 因此不保留 `AtomUI.Theme.Palette`;算法实现也不属于资源样式层,因此不保留 `AtomUI.Theme.Styling` 兼容命名空间。 AXAML Control theme 的发现、聚合和加载属于主题资源所有权,统一位于 `AtomUI.Theme.Resources`,不参与 Token 编译。`SharedTokenKind`、Token Resource markup extension 和相关生成代码同样输出到该 namespace;不保留 `AtomUI.Theme.Styling`。Token builder、Token kind、Token attribute 和 value converter 统一位于 `AtomUI.Theme.DesignTokens`,不保留 `AtomUI.Theme.Tokens` 或 `AtomUI.Theme.TokenSystem`。源生成器必须使用 相同映射,禁止构建后重新生成旧 namespace。 依赖方向固定为:Definitions/Configuration/Schema 提供输入,Compilation 产生 snapshot,根目录的 ThemeManager 提交 snapshot,Resources 只读取已提交 snapshot。Compilation 不依赖 Avalonia 资源宿主,Resources 不执行编译,ThemeConfigProvider 不拥有编译器。每个程序集生成自己的 `AtomUI.Generated..GeneratedThemeSchema`,程序集标识中的非标识符字符统一 替换为下划线,禁止多个程序集生成相同全限定类型名,也禁止把生成 namespace 嵌套在可能同名的 Control 类型下面。 ## 21. 明确删除的旧设计 新架构实现时删除: - `Theme`、`ITheme` 以及预创建 Theme variant pool。 - `ThemeCoordinator` 以及一对一 `ThemeManager`/`ThemeEngine` facade 分层;事务队列和当前状态只有 Manager 一个 长期所有者。 - `Theme.SharedToken`、`Theme.GetControlToken()`、`ThemeConfigProvider.SharedToken`、 `ThemeConfigProvider.ControlTokens` 和所有 clone compatibility map。 - `SharedTokenSetters`、`ControlTokenInfoSetters`、字符串算法集合、Token 查询属性以及对可变配置对象图的 collection/property changed 订阅。 - snapshot 中的 `DesignToken`、`IControlDesignToken` 和对应 Clone API。 - `IDesignToken`、`IControlDesignToken` 中面向运行时修改和资源构建的接口。 - `ThemeScope.SnapshotProperty` 和基于 snapshot attached value 的父级订阅。 - `TokenFinderUtils` 和沿 StylingParent 查找旧 Provider 的逻辑。 - `ThemeCreated`、`ThemeAboutToLoad`、`ThemeLoaded`、`ThemeLoadFailed`、`ThemeAboutToChange`。 - 对 Theme ResourceDictionary 的 Motion/Wave 直接修改。 - 自定义主题 id 编码的 Avalonia ThemeVariant。 - `ControlTokenRegistration(Type)`、反射 schema、Activator fallback 和对应 AOT suppress。 - `ControlTokenScope.Catalog`、`ControlTokenScope.Id` 两字段身份以及按 TargetType、CLR Token 类型或资产路径推断 Control identity 的逻辑。 - cache pin、父 snapshot Version cache key 和字符串拼接 cache key。 - 旧主题 XML 的 `ControlTokens/ControlToken` 结构。 这些内容不建立 adapter,不标记 obsolete 后继续保留,也不作为迁移阶段的内部真源。 ## 22. 验收标准 ### 22.1 语义 - 全局 Tokens、Controls、Algorithms 和 Inherit 遵守本文定义的配置合并规则。 - 顶层 ThemeConfigProvider 默认继承当前全局主题。 - 嵌套 Provider 正确合并父 Tokens、Control 配置和算法四态。 - 局部 `Inherit=false` 从 AtomUI library defaults 开始,不继承当前主题定义或父作用域配置。 - Control 覆盖不修改全局 SharedToken,也不创建实例私有作用域;同一 ThemeContext 中全部同 identity Control 使用一致的覆盖结果。 - 同一主题的新 runtime override 会产生新有效配置,不复用过期 Theme 对象。 - `Algorithms=null` 继承基线算法,空算法列表被诊断为无效输入。 - `ThemeConfig`、`ControlThemeConfig`、`ThemeRequest` 及其全部集合深度不可变;替换 Provider.Config 是唯一局部 动态配置入口,修改旧输入对象不可能触发事务。 - 所有全局和 Control 算法严格遵守 `Evaluate(sameEffectiveSeed, previousMap?) -> nextMap`;后续算法不能把前一 Map 当作新的 Seed,也不能修改输入。 - 根和每个 `ThemeConfigProvider` 的 snapshot appearance 始终等于其 Avalonia `ActualThemeVariant`;Control custom algorithm 的 appearance 不泄漏到作用域 variant。 - 无 owner 的静态 Dialog、Notification 等 API 始终使用 Root ThemeContext,并产生可诊断的根主题语义,不能 根据当前焦点猜测局部作用域。 ### 22.2 事务 - Dark + Compact 只产生一次完整提交。 - 根主题变化和所有受影响局部作用域在通知前完成引用替换。 - 配置失败、编译失败和 stale result 不发布部分状态。 - `ApplyThemeAsync` 分别返回可区分的 `Committed`、`NoOp`、`Superseded` 和 `Failed` 结果;被替代请求不返回 成功;`Failed` 只发生在任何已发布引用交换之前。 - Capture 后 TopologyRevision、RegistrationId、ParentRegistrationId 或 ConfigRevision 任一变化都会使 staged result 成为 `Superseded`,旧结果绝不提交到新拓扑。 - Publish 开始后不回滚。AvaloniaProperty、资源通知或观察者异常被记录为 publish warning,事务仍返回 `Committed`,最终结果携带完整 `PublishDiagnostics`。 - 等价并发请求共享一次 Prepare/Commit;单个等待者取消不取消其他等待者或已经开始的 Commit。 - `ThemeChanged` 或 `ThemeChangeFailed` 的派发先于对应 transition Task 完成。 - 重入和高频更新不会递归提交,latest request 语义确定。 - 订阅者异常不阻断提交或其他订阅者。 - FollowSystem 在首帧前解析初始系统 appearance,运行期切换只发布完整 Light/Dark snapshot 和对应显式 variant,不出现 `ThemeVariant.Default` 或新旧状态混合帧。 ### 22.3 资源与生命周期 - 根作用域和每个局部作用域始终只有一个 Token ResourceProvider。 - 重复更新不增长 `MergedDictionaries`。 - 控件实例不创建 Control Token 资源字典。 - 所有共享 Token AXAML 引用使用 `{atom:SharedTokenResource ...}`;生产源码、生成输出和 AXAML 资产中不存在 `*TokenSharedTokenResourceExtension` 类型或 `{atom:*TokenSharedTokenResource ...}` 用法。 - global、ambient Control scope、Control delta hit 和 global fallback 均返回正确值;不同 Control identity 的同名 SharedToken 不串值。 - AtomUI ControlTheme 资产都具有可静态验证的 Control token scope;scope identity 未注册或与资产声明不一致时 在主题注册阶段失败。 - Popup、Flyout、Dialog 和窗口覆盖层继承 owner ThemeContext。 - 普通 Popup/Flyout 依靠逻辑树自然继承;独立 TopLevel 通过一个可释放 lease 和宿主私有 ResourceBridge 继承 owner context。owner 替换后资源、imperative Token 和 variant 同时切换。 - detach、reparent、关闭和回收后不保留旧作用域或 Control。 - 独立 TopLevel 关闭、owner 替换和异常创建后不保留旧 bridge、lease、owner Visual 或 context subscription。 - 替换 `ThemeConfigProvider.Config` 后旧配置和旧集合变化不再触发 Capture;detach 后配置订阅全部释放。 ### 22.4 性能、内存与 AOT - 同一 definition revision 只成功读取一次,同一 `(definition revision, registry revision)` 只成功绑定一次; 来源内容变化使 Reader/Binder 缓存失效,descriptor revision 变化只使 Binder/Compiler 缓存失效。 - 等价有效配置只编译一次。 - 人工构造相同 fingerprint、不同结构化配置的碰撞测试不会错误复用 snapshot 或 Control 编译结果。 - 不同 registry revision 允许 slot 重新分配,绑定、缓存和资源查询仍解析到当前 revision 的正确 slot。 - 单 Control 配置变化复用全局表和其他 Control snapshot。 - Token 资源查询热路径保持零分配。 - cache 和旧 snapshot 存活数量满足本文定义的容量上界。 - ThemeConfigProvider、Popup、Flyout 和 Gallery 导航生命周期 WeakReference 测试通过。 - 正常主题切换路径不执行 Token 反射或 Activator。 - Core、Controls、Desktop、DataGrid、ColorPicker、Extras、GalleryBase 和 Gallery 相关测试通过。 - Release 多目标构建和 Gallery NativeAOT publish 通过。 - `git diff --check` 通过,仓库搜索不存在已删除旧 API 的引用。