76 KiB
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 逻辑树和显式资源宿主生命周期替代:
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 的控件算法必须能表达四种状态:
- 未指定:继承父配置中同一 Control 的算法策略。
- 禁用:Control 内覆盖的 Seed Token 不重新派生 Map 和 Alias Token。
- 使用全局算法:使用当前作用域的全局算法链重新派生该 Control 的有效 Token。
- 自定义算法:使用 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. 总体架构
定义与编译路径:
+-------------+ +---------------------+ +-----------------------+
| Theme files | --> | ThemeDocumentReader | --> | ThemeDefinitionBinder | --+
+-------------+ +---------------------+ +-----------------------+ |
|
+-------------+ +-----------------------+ |
| ThemeConfig | --> | ThemeConfigNormalizer | -------------------------------+
+-------------+ +-----------------------+ |
v
+-------------------+
| ThemeConfigMerger |
+---------+---------+
|
v
+---------------------+ supplies descriptors +-------------------+
| ThemeSchemaRegistry | ------------------------------------------> | ThemeCompiler |
+---------------------+ +---------+---------+
|
v
+-------------------+
| ThemeSnapshotCache |
+-------------------+
ThemeSchemaRegistry 还为 Binder、Normalizer 和资源投影提供 descriptor。
运行时发布路径:
+--------------------+ +--------------+
| 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 和绑定的不可变配置输入:
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 是全局和局部主题唯一的合并入口:
配置优先级从低到高固定为:
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 在应用主题初始化前一次性构建,内容来自源生成器或显式描述符:
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 的求值契约固定为:
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 version="1.0" encoding="utf-8"?>
<Theme xmlns="https://atomui.net/schemas/theme/v1"
Id="DaybreakBlue"
Name="Daybreak Blue"
Appearance="Light"
IsDefault="true">
<Algorithms>
<Algorithm Id="Default" />
</Algorithms>
<Tokens>
<Token Name="ColorPrimary" Value="#1677FF" />
</Tokens>
<Controls>
<Control Catalog="AtomUI" Id="Button" Algorithm="Global">
<Tokens>
<Token Name="ColorPrimary" Value="#0958D9" />
<Token Name="ContentFontSize" Value="14" />
</Tokens>
</Control>
</Controls>
</Theme>
规则如下:
- 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 规范,机器可读标准见 AtomUI Theme Definition XML Schema v1。
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、无资源宿主、无全局服务访问的纯计算入口:
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 返回。
全局计算顺序固定为:
- 从 AtomUI library Seed defaults、ThemeDefinition 和有效配置构造唯一的 effective Seed。
- 将同一 effective Seed 依次传给算法链;第一个算法的 previous Map 为空,后续算法接收前序 Map。
- 对最终 Map 应用显式 Map Token override。
- 由 Map 生成 Alias Token,再应用显式 Alias Token override。
- 执行 AtomUI 最终语义规范化。
MotionEnabled=false时,Motion 服务被禁用且全部标准 duration Token 强制为TimeSpan.Zero;WaveEnabled=false时 Wave 行为不创建动画。该后置条件不依赖单个控件是否正确消费时长。
每个 Control 的计算顺序为:
- 以全局最终 Alias Token 为基线,叠加该 Control 覆盖的全局 Token。
Disabled时只保留直接覆盖,不重新派生;Global或Custom时,从 Control effective Seed 按相应算法链 重新生成 Map 和 Alias。- 在重新派生后应用该 Control 的 Map、Alias override 和 Motion 最终规范化。
- 使用 Control 的有效全局 Token 计算 Control 自身默认 Token。
- 应用 Control 自身 Token override。
- 根据全局 appearance 和 Control 算法 effect 计算 Control appearance;它只提供给该 Control evaluator,不 修改 ThemeContext appearance。
- 生成共享 Token 差量、Control Token 值和资源投影。
这与 Ant Design 的组件计算保持同一语义:未启用组件算法时只是覆盖;启用时以“全局最终 Token + 组件覆盖”
作为重新派生输入。Control 默认 Token evaluator 必须接收 Control appearance,不能继续只读取全局
IsDark。
任何 validation、conversion 或 algorithm 错误都返回失败结果,不返回部分 snapshot。
8. ThemeSnapshot
ThemeSnapshot 是 Token 编译结果和资源发布单元:
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)。 ThemeSchemaRegistryrevision。
父主题的影响已经包含在合并后的有效配置中,因此 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<ThemeTransitionResult>的运行时主题请求入口。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:
+---------------------------+
| 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 五个阶段:
+-------------+ +-------------+ +-------------+ +-------------+ +-------------+
| 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 查询。
查询规则:
+----------------------+
| 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 语法固定为:
<!-- 全局共享 Token;在 Control token scope 内自动读取该 Control 的有效共享 Token 差量。 -->
<Setter Property="Background" Value="{atom:SharedTokenResource ColorErrorBg}" />
<!-- Control 自身 Token。 -->
<Setter Property="Padding" Value="{atom:AlertTokenResource DefaultPadding}" />
禁止生成或使用以下把 Control identity 拼进 MarkupExtension 类型名的语法:
<Setter Property="Background" Value="{atom:AlertTokenSharedTokenResource ColorErrorBg}" />
SharedTokenResourceExtension 根据所在主题资产的 ambient Control token scope 选择 key:
- 没有 Control token scope 时,返回普通
SharedTokenKind,读取当前 ThemeContext 的全局资源。 - 存在 Control token scope 时,使用
(ControlTokenIdentity, SharedTokenKind)获取缓存后的内部复合 key,先读取 对应 Control 的有效共享 Token 差量,miss 时回退全局资源。 - Control 自身 Token 继续使用生成的
<ControlTokenName>Resource扩展,不经过共享 Token fallback。
Control token scope 在一个主题资产边界只声明一次,不在每个 Setter 上重复。Theme 资源文件使用单一、生成式
ControlTokenScope.Identity 把 ResourceDictionary 或 ControlTheme 关联到已注册的
ControlTokenIdentity:
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:atom="https://atomui.net"
xmlns:generated="using:AtomUI.Generated.AtomUI_Desktop_Controls"
atom:ControlTokenScope.Identity="{x:Static generated:AlertThemeAsset.Identity}">
<!-- 这里的 SharedTokenResource 自动使用 AtomUI:Alert 的共享 Token 投影。 -->
</ResourceDictionary>
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 必须以明确的
AvaloniaObjectowner 或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() 的主题启动顺序固定为:
- Builder 收集主题包、主题文件来源、算法 descriptor、Control theme asset manifest、不可变初始 root request 模板和语言配置。
- 构建并冻结
ThemeSchemaRegistry;在此时拒绝重复 Control identity、无效 descriptor、资产 identity/URI 冲突以及 manifest 与 registry 不一致。 - Catalog 读取并绑定主题定义,确定默认主题和 definition revision。
- 如果启用 FollowSystem,先同步读取初始系统 appearance,再从 Light/Dark 不可变 request 模板中选择首帧 request;不得先按 Default 编译后再补一次 Dark/Light 切换。
- 同步完成初始 request 的 Capture/Prepare,生成第一个完整根
ThemeSnapshot。 - 使用该 snapshot 创建 Root ThemeContext、稳定 Provider 和唯一 ThemeManager;Manager 的 Styles 同时预装 Control themes、语言资源入口以及向全部 TopLevel 注入 Root ThemeContext 的全局 style。
- 将同一个已完整初始化的 ThemeManager 实例挂载到 Application Styles,并绑定为
IThemeManager服务。 - 显式设置与 snapshot appearance 一致的 Avalonia Light/Dark variant,并且只发布一次初始 Token 资源通知。
- 执行与主题生命周期无关的模块初始化。
不得先挂载空资源层再异步补主题,也不得通过 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 的第二份对象图。
目标内存复杂度是:
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 局部覆盖的结构共享结果如下:
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 时必须有 scopedIResourceHost和对称 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,验证不存在
Defaultvariant 或 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 这种过细、语义重叠或已经失真的目录。
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.<OwnerAssemblyIdentifier>.GeneratedThemeSchema,程序集标识中的非标识符字符统一
替换为下划线,禁止多个程序集生成相同全限定类型名,也禁止把生成 namespace 嵌套在可能同名的 Control
类型下面。
21. 明确删除的旧设计
新架构实现时删除:
Theme、ITheme以及预创建 Theme variant pool。ThemeCoordinator以及一对一ThemeManager/ThemeEnginefacade 分层;事务队列和当前状态只有 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 始终等于其 AvaloniaActualThemeVariant;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 的引用。