41 KiB
AtomUI 主题系统架构
本文是 AtomUI 主题系统唯一的长期架构设计文档。它定义最终运行时模型、公开配置语义、主题文件处理、
Token 编译、动态作用域、资源发布、事件契约、AOT 边界和验收标准。阶段性分析、迁移过程和任务进度
不写入本文,统一放在 docs/superpowers/。
本文描述目标架构。实现过程中不得为了保留旧主题系统 API 而偏离这些约束。
1. 设计目标
主题系统必须满足以下不可协商的约束:
- 建立完整的 Seed Token、Map Token、Alias Token、Control Token、算法和嵌套配置模型。
- AtomUI 的公开 API、内部类型、资源键和文档统一使用
Control术语。 ThemeSnapshot是运行时 Token 值的唯一真源。- 全局主题和局部主题使用同一个配置合并器、编译器、缓存和提交协议。
- 主题变更先完成整个受影响作用域子树的编译,再一次性提交,不发布半成品状态。
- 控件级配置只参与目标控件自身的 Token 计算,不形成控件内容树资源作用域。
- 每个主题作用域只拥有一个稳定的 Token 资源 Provider。
- Token 资源查询热路径不分配对象,不执行字符串解析、反射或 LINQ。
- 局部主题只复制发生变化的数据,不能按作用域复制完整全局 Token 和全部 Control Token 值。
- 缓存、作用域图、事件订阅和异步编译任务都必须有确定的容量或释放边界。
- 内置正常路径不使用运行时程序集扫描、
Activator.CreateInstance或 Token 属性反射。 - 第一个可见帧之前完成全局主题准备和挂载。
- 不保留旧主题系统的兼容对象、克隆查询 API、伪生命周期事件或可变资源旁路。
2. ThemeConfig 设计结论
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? 不能作为内部真源。
3. 总体架构
定义与编译路径:
+-------------+ +---------------------+ +-----------------------+
| Theme files | --> | ThemeDocumentReader | --> | ThemeDefinitionBinder | --+
+-------------+ +---------------------+ +-----------------------+ |
|
+-------------+ +-----------------------+ |
| ThemeConfig | --> | ThemeConfigNormalizer | -------------------------------+
+-------------+ +-----------------------+ |
v
+-------------------+
| ThemeConfigMerger |
+---------+---------+
|
v
+---------------------+ supplies descriptors +-------------------+
| ThemeSchemaRegistry | ------------------------------------------> | ThemeCompiler |
+---------------------+ +---------+---------+
|
v
+-------------------+
| ThemeSnapshotCache |
+-------------------+
ThemeSchemaRegistry 还为 Binder、Normalizer 和资源投影提供 descriptor。
运行时发布路径:
+--------------------+ +--------------+
| ThemeSnapshotCache | -----> | ThemeEngine |
+--------------------+ +------+-------+
|
atomic snapshot commit
|
+---------------+---------------+
| |
v v
+-------------------+ +-----------------+
| Root ThemeContext | | ThemeScopeGraph |
+---------+---------+ +--------+--------+
| |
| v
| +--------------------+
| | Child ThemeContexts |
| +----------+---------+
| |
+---------------+---------------+
|
v
+----------------------------+
| ThemeTokenResourceProvider |
+-------------+--------------+
|
v
+----------------------------+
| AXAML and Control runtime |
+----------------------------+
系统分成六个边界:
- 定义层:读取主题文件并生成与运行时注册无关的语法模型。
- Schema 层:提供生成式 Token 元数据、强类型赋值器、资源投影器和 Control 身份。
- 配置层:捕获可变 UI 输入,生成不可变规范化配置,并执行 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 及其集合可以是可观察对象,以支持 AXAML 和运行时绑定。它们只属于输入层,不允许被
ThemeSnapshot、资源 Provider 或控件运行时持有。
ThemeConfigProvider 是轻量 Decorator,只公开一个 Config 和继承的内容子节点。旧的
SharedTokenSetters、ControlTokenInfoSetters、字符串算法集合以及 Token 查询属性不属于新架构。
4.2 规范化配置
ThemeConfigNormalizer 在 UI 线程捕获一次完整输入,完成以下工作:
- 解析并验证 Token key 和 value。
- 把 Control id 解析为
ControlTokenIdentity。 - 保留算法的“未指定”状态。
- 将字典和列表复制为不可变、有确定顺序的值对象。
- 计算稳定的结构 fingerprint。
- 返回 diagnostics,不抛出缺少上下文的反射异常。
编译器只接收 NormalizedThemeConfig。配置对象在捕获后继续变化,不得影响正在执行的编译。
4.3 合并规则
ThemeConfigMerger 是全局和局部主题唯一的合并入口:
Inherit=true时以父作用域的有效配置为基础。Inherit=false时以 AtomUI 默认配置为基础,不继承当前全局主题。- 当前
Tokens按 key 覆盖父Tokens。 Controls先按ControlTokenIdentity合并,再按 Token key 合并。- Control 的算法未指定时继承父策略;显式 disabled、global 或 custom 时替换父策略。
- 当前
Algorithms未指定时继承父算法;显式提供时整体替换,不和父算法拼接。 - 主题定义、应用级配置和运行时请求都先转换为规范化配置,再按同一规则合并。
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]
\-- evaluator, AOT factory, appearance effect
源生成器必须为内置 Token 生成:
- 稳定的
ControlTokenIdentity,包括 catalog 和 control id。 - Token name 对应的强类型赋值委托查找表。
- Token value 到 Avalonia resource value 的投影。
- Control Token builder 的直接构造委托。
- Control 自身 Token schema 和继承的 Token schema。
- 生成式 Control 资源键和共享 Token 复合资源键扩展。
包注册直接传递完整描述符,不得把生成器提供的 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必须显式声明。- 算法使用有序
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。主题文件每个 revision 在进程中最多读取和绑定一次。
7. 编译器
ThemeCompiler 是无 UI、无资源宿主、无全局服务访问的纯计算入口:
ThemeCompileInput
-> merge effective config
-> initialize global seed token builder
-> apply global seed overrides
-> run ordered algorithms
-> apply map and alias overrides
-> evaluate each registered Control
-> freeze token values and resources
-> ThemeSnapshot
编译阶段允许使用可变 builder,因为算法天然需要逐步派生。builder 的生命周期只限于一次编译,不能进入 snapshot,也不能通过公开 API 返回。
每个 Control 的计算顺序为:
- 从全局最终 Token 值创建 Control 有效 Token builder。
- 应用该 Control 覆盖的全局 Seed Token。
- 根据
ControlThemeConfig.Algorithm决定是否重新运行算法。 - 应用 Control 覆盖的 Map 和 Alias Token。
- 计算 Control 自身默认 Token。
- 应用 Control 自身 Token 覆盖。
- 生成共享 Token 差量、Control Token 值和资源投影。
任何 validation、conversion 或 algorithm 错误都返回失败结果,不返回部分 snapshot。
8. ThemeSnapshot
ThemeSnapshot 是 Token 编译结果和资源发布单元:
ThemeSnapshot
+-- ThemeId / ContentFingerprint / Algorithms / IsDark
+-- EffectiveConfig
+-- GlobalTokenValues: dense immutable TokenValueTable
+-- GlobalResources
+-- PresetColorPalettes
\-- Controls[registry control slot]
+-- EffectiveGlobalTokenDelta
+-- ControlTokenValues: dense immutable TokenValueTable
\-- ControlResources
具体约束:
- Token schema 由生成器分配稳定 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 值。
- snapshot 内容身份由
ContentFingerprint表达,不使用父 snapshot 的递增版本表达语义身份。
9. 缓存
ThemeSnapshotCache 使用结构化 value key:
- Theme definition revision。
NormalizedThemeConfigfingerprint。- 有序算法 descriptor id。
ThemeSchemaRegistryrevision。
父主题的影响已经包含在合并后的有效配置中,因此 cache key 不包含父 snapshot 的发布序号。
缓存要求:
- 等价内容返回同一个 snapshot 引用。
- 并发等价请求只执行一次编译。
- 失败结果不进入缓存。
- 缓存采用同时受 entry 数量和 token-slot 权重约束的 LRU;达到任一上限即回收最旧 inactive entry。
- snapshot 权重由全局 Token slot、Control Token slot、差量和资源条目数计算,不依赖不稳定的进程 RSS 估算。
- 作用域本身持有当前 snapshot,因此不需要 pin 机制。
- Provider 实例不进入缓存,不在不同资源宿主之间共享。
- 不建立无上限的 Brush、字符串、resource key 或 fingerprint intern pool。
10. ThemeEngine 与全局主题
ThemeEngine 是唯一允许发布 snapshot 的对象。ThemeManager 是公开 facade 和 Avalonia Styles
宿主,不再拥有第二份 Theme Token 状态。
新公开模型由以下对象组成:
ThemeInfo:Catalog 中可选择主题的只读元数据。ThemeRequest:主题 id、ThemeConfig和变更原因。ThemeState:当前已提交主题的 id、算法、dark 状态、fingerprint 和 transition id。IThemeManager.ApplyThemeAsync(...):运行时主题请求入口。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名称。 - 跟随系统主题是一种明确的运行时策略,不通过双向绑定自定义 ThemeVariant 实现。
Motion 和 Wave 配置也是 ThemeConfig Token override。任何入口改变这些值时都生成新请求,不直接修改
ResourceDictionary。
11. 动态作用域
11.1 稳定 ThemeContext
ThemeContext 是稳定的作用域身份,包含:
- 当前
ThemeSnapshot的原子引用。 - 唯一的
ThemeTokenResourceProvider。 - 内部 context changed 通知。
ThemeContext 不复制或修改 Token。snapshot 仍是 Token 值唯一真源。
ThemeScope.ContextProperty 在树上传递稳定 context,而不是每次主题变化都替换 attached snapshot。
没有局部 Provider 时,控件解析到根 ThemeContext。
11.2 ThemeScopeGraph
ThemeEngine 维护活动作用域图:
+---------------------------+
| ThemeEngine |
| 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 保存父节点、当前规范化配置、最后一次有效配置、ThemeContext、Provider 和子节点。
ThemeConfigProvider attach 时注册节点,detach 时注销节点,父级变化或内容重新挂接时更新图关系。它不自行
监听父 snapshot,不直接调用 compiler,也不自行实现事件队列。
全局主题变化时,Runtime 使用新根 snapshot 和各节点最后一次有效配置,按拓扑顺序准备整个作用域图。 局部配置变化时,只准备该节点及其后代。
配置输入无效时:
- 当前作用域保留已发布 snapshot。
LastValidConfig不更新。- 派发失败事件和 diagnostics。
- 后续父主题变化使用
LastValidConfig,避免该作用域永久冻结在旧全局主题。
Popup、Flyout、Dialog 和其他脱离普通逻辑树的宿主必须显式传播 owner 的 ThemeContext。不得回退到
Application 全局资源来掩盖上下文丢失。
12. 事务与发布顺序
一次主题事务分为 Capture、Prepare、Commit、Notify 四个阶段:
+----------------+ +----------------+ +----------------+ +----------------+
| Capture | -> | Prepare | -> | Commit | -> | Notify |
| UI thread | | pure work | | UI thread | | UI thread |
+----------------+ +----------------+ +----------------+ +----------------+
| immutable | | merge | | swap every | | resources |
| config | | cache | | context | | contexts |
| affected nodes | | compile | | update state | | outcome event |
+----------------+ +----------------+ +----------------+ +----------------+
提交前不得修改 Application、ThemeManager、ThemeContext 或资源 Provider。所有 context 引用完成替换后, 才允许发送第一条资源通知,因此任意资源回调看到的都是完整新状态。
配置规范化或编译失败时不进入 Commit。Commit 中尚未发送通知的状态修改发生异常时必须回滚到旧引用。
同一作用域的新请求使用 generation 标识。旧的异步准备结果在提交前发现 generation 已过期时直接丢弃。 提交期间发生的重入请求进入队列,同一作用域尚未执行的请求合并为最新请求。
13. 资源解析
根 ThemeContext 的 Provider 在 ThemeManager 初始化时挂载一次。每个局部 ThemeContext 也只拥有一个
Provider。主题切换不追加 MergedDictionaries,不创建新的 Provider。
查询规则:
+----------------------+
| 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。
控件实例不创建 Token ResourceDictionary,不注册 Control 自己的资源作用域,也不订阅全局主题事件。
用户 Content 和嵌套控件只能看到当前 ThemeContext,不能看到父控件私有的 Control Token 输入。
14. Imperative Token 消费
少数控件需要在 C# 中计算 palette、断点或运行时绘制值。它们使用 ThemeTokenResolver:
- Resolver 从当前控件继承的
ThemeContext读取一次 snapshot。 - 全局 Token 使用
SharedTokenKind强类型查询。 - Control Token 使用
ControlTokenIdentity和生成的 Token kind 查询。 - palette 读取 snapshot 中不可变的
PresetColorPalettes。 - 需要动态更新时订阅当前
ThemeContext,不订阅ThemeManager.ThemeChanged。
优先把固定视觉结果编译为 Control 资源。只有值确实参与控件运行时算法时才使用 resolver。
15. 事件契约
新主题系统只保留结果事件:
ThemeChanged:事务已经提交且资源引用一致后派发。ThemeChangeFailed:Capture 或 Prepare 失败后派发,携带 request、scope、transition id、diagnostics 和 exception。
不提供可取消的 Changing 事件,也不保留 Created、AboutToLoad、Loaded、AboutToChange、Unload 等伪生命周期。
事件由统一 ThemeEventDispatcher 派发:
- 每个订阅者独立调用。
- 一个订阅者异常不阻止后续订阅者。
- 订阅者异常通过 Avalonia Logger 记录完整上下文。
- 订阅者异常不把成功提交改写为主题失败。
- 事件处理期间发起的请求遵守 Runtime 重入队列。
全局事件由 IThemeManager 暴露;局部结果由对应 ThemeConfigProvider 暴露。两者使用相同 event args 和
派发规则。
16. 启动时序
Application.UseAtomUI() 的主题启动顺序固定为:
- Builder 收集主题包、主题文件来源、算法 descriptor、默认主题配置和语言配置。
- 构建
ThemeSchemaRegistry,在此时拒绝重复 Control identity 和无效 descriptor。 - Catalog 读取并绑定主题定义,确定默认主题。
- 同步准备第一个根
ThemeSnapshot。 - 创建 Root ThemeContext、稳定 Provider、ThemeEngine 和 ThemeManager。
- 将已经包含有效 snapshot 的 ThemeManager 挂载到 Application Styles。
- 设置 Avalonia Light/Dark variant,发布一次资源通知。
- 绑定主题服务并执行与主题生命周期无关的模块初始化。
不得先挂载空资源层再异步补主题,也不得通过 ThemeLoaded 事件修补 DefaultFont、断点或其他 Token。
17. 线程与生命周期
- 配置输入只能在 UI 线程捕获。
- ThemeCompiler 不访问 UI 对象,可以在后台准备运行时变更。
- 初始根主题和 ThemeConfigProvider 首次 attach 同步准备,避免首帧闪烁。
- 运行时更新可以后台编译,但最终 Commit 和资源通知必须在 UI 线程。
- Scope attach、detach、reparent 和 Content 替换必须在 ThemeScopeGraph 中对称登记和释放。
- snapshot 不持有作用域、Control、Visual 或事件订阅。
- Provider 生命周期归所属 ThemeContext,不进入全局静态缓存。
- Popup/Flyout 的 context owner 变化和关闭必须释放传播订阅。
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 fingerprint、Control identity、Control config fingerprint 和 algorithm id 为 key,复用跨作用域的 Control 计算结果。
两级缓存共用容量策略,失败和取消结果不缓存。
默认容量由 ThemeCacheOptions 固定为:
- 完整 snapshot cache:最多 32 个 entry,最多 131072 个 weight unit。
- Control compilation cache:最多 2048 个 entry,最多 131072 个 weight unit。
- 一个 Token slot、资源条目或差量条目计为一个 weight unit。
容量可以由应用显式调小;扩大默认值必须附带 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。 - 同一 ThemeEngine 同时只运行一个根事务准备管线;后续请求合并,避免并发编译造成内存峰值。
- 初始同步编译完成后及时释放 ThemeDocument 中不再需要的 XML 对象和临时字符串。
18.4 资源查询热路径
ThemeTokenResourceProvider.TryGetResource 是控件布局和样式更新的热路径,要求:
- global、Control shared 和 Control own 命中查询均为零托管分配。
- 生成的资源扩展持有缓存后的稳定 key,不在查询时创建 key 或拼接字符串。
ControlSharedTokenResourceKey直接包含ControlTokenIdentity或 registry slot,不在每次查询时重新构造身份。- 查询只执行类型分支、slot 索引和最多一次稀疏差量 fallback。
- miss 不创建 diagnostics、异常或临时集合。
- 一次事务对每个受影响 Provider 最多调用一次
RaiseResourcesChanged()。
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 路径都必须释放它。
- imperative Token 订阅返回
IDisposable,由控件在 detach、re-template 或 owner 变化时释放。 - 非 Visual
AvaloniaObject使用 DynamicResource 时必须有 scopedIResourceHost和对称 release 路径。 - Popup、Flyout 和 overlay 关闭后必须释放 owner ThemeContext 传播订阅。
- 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 丢弃。
- 资源查询:global、Control shared、Control own 的 hit、fallback 和 miss。
- 生命周期:100 次 Provider attach/detach、Popup/Flyout open/close 和 Gallery 页面导航。
验收指标至少包括:
TryGetResource稳态查询为 0 B/op。- no-op 请求不创建 snapshot、Provider、Context,不派发资源或主题事件。
- 单 Control 局部覆盖只产生一个新 Control snapshot,其他 Control snapshot 引用保持不变。
- 重复更新后每个作用域仍只有一个 Provider。
- cache entry 和 token-slot weight 始终不超过配置上限。
- attach/detach 后通过 WeakReference 和强制 GC 验证 ScopeNode、ThemeContext、Provider 及旧 Content 可回收。
- Gallery 随机导航稳定后旧 ShowCase、DynamicResourceExpression 和主题作用域对象数量不单调增长。
- RSS 只作为辅助信息;对象数量和可达链才是内存泄漏判据。
19. AOT 约束
- 内置 Token 注册和 schema 全部由源生成器生成。
- 不运行时扫描程序集或 Token 属性。
- 不使用
Activator.CreateInstance创建内置 Control Token。 - 不使用字符串属性路径 Binding 完成主题配置或 Token 查询。
- 自定义算法和第三方 Control Token 必须提供稳定 descriptor id。
- 生成器测试必须验证 identity、继承 Token、强类型 setter、资源投影和输出稳定性。
- 主题系统完成后必须执行真实 Gallery NativeAOT publish,不能只依赖 analyzer。
20. 模块组织
主题源码按稳定职责边界组织,不为少量运行时入口创建目录,也不保留 Catalog、Scope、Engine、
ControlThemes、Styling、Transitions 或 TokenSystem 这种过细、语义重叠或已经失真的目录。
Theme/
|
+-- IThemeManager.cs 公开主题管理入口
+-- IThemeManagerBuilder.cs 公开注册入口
+-- ThemeManager.cs Avalonia Styles facade
+-- ThemeManagerBuilder.cs 启动组装器
+-- ThemeManagerBuilderExtensions.cs 默认注册扩展
+-- ThemeConfigProvider.cs 局部主题配置宿主
+-- IThemeConfigProvider.cs Provider 公开契约
+-- IThemeAssetPathProvider.cs 主题资源路径契约
+-- ThemeEngine.cs 唯一 snapshot 事务提交者
+-- ThemeTransaction.cs Capture/Prepare/Commit/Notify 事务
+-- ThemeState.cs 当前已提交主题状态
+-- ThemeContext.cs 稳定主题上下文
+-- ThemeScopeGraph.cs 局部主题作用域图
+-- ThemeScopeNode.cs 作用域节点
+-- ThemeRequest.cs 主题变更请求
+-- ThemeEventDispatcher.cs 结果事件派发
+-- ThemeExceptions.cs 公开错误类型
|
+-- Algorithms/
| +-- 算法 descriptor、attribute、接口与内置算法
| +-- Default、Dark、Compact 计算器
| \-- 调色板、颜色映射和算法计算辅助
|
+-- Configuration/
| +-- ThemeConfig、ControlThemeConfig
| +-- ControlAlgorithmMode
| +-- Normalizer、Merger 和 immutable normalized config
| \-- 仅用于捕获配置输入的 setter 类型
|
+-- Definitions/
| +-- 主题源、ThemeInfo、definition revision 和默认主题选择
| +-- XML reader、syntax document 和 diagnostics
| \-- Binder、typed ThemeDefinition 和 bind result
|
+-- Schema/
| +-- ThemeSchemaRegistry
| +-- TokenDescriptor、ControlTokenDescriptor
| +-- ThemeAlgorithmDescriptor
| \-- identity、stage、parser 和 resource projector contracts
|
+-- Compilation/
| +-- CompileInput、Normalizer output consumption
| +-- ThemeCompiler、TokenValueTable
| +-- ThemeSnapshot、ControlThemeSnapshot
| \-- bounded snapshot/control compilation caches
|
+-- Resources/
| +-- ThemeTokenResourceProvider、ThemeTokenResolver
| +-- global/control resource keys and markup extensions
| +-- Control theme provider contracts and attributes
| +-- BaseControlTheme and provider aggregation
| \-- snapshot lookup、style/template 和 Control theme 加载辅助
|
+-- Tokens/
| +-- global/control compile-only builders
| +-- Token definitions and kind metadata
| \-- Token value converters
Definitions 同时拥有主题来源和主题定义,不再用单独的 Catalog 目录把一次加载流程拆开。
ThemeEngine、事务、上下文和作用域图都属于主题子系统的核心运行时入口,直接位于 Theme 根目录;
这些类型必须共享同一个 snapshot 提交边界,但不需要额外的物理目录表达这一事实。
Theme 根目录中的 C# 类型统一使用 AtomUI.Theme 命名空间;职责子目录中的类型使用与目录对应的
AtomUI.Theme.<Area> 命名空间。根目录不保留 .Scope、.Transitions 等已被物理结构删除的命名空间。
Algorithms 同时拥有算法与调色板计算;调色板不是独立运行时子系统。
AXAML Control theme 的发现、聚合和加载属于主题资源所有权,统一位于 Resources,不参与 Token 编译。
依赖方向固定为:Definitions/Configuration/Schema 提供输入,Compilation 产生 snapshot,根目录的
ThemeEngine 提交 snapshot,Resources 只读取已提交 snapshot。Compilation 不依赖 Avalonia 资源宿主,Resources
不执行编译,ThemeConfigProvider 不拥有编译器。每个程序集生成自己的
AtomUI.Generated.<OwnerAssemblyIdentifier>.GeneratedThemeSchema,程序集标识中的非标识符字符统一
替换为下划线,禁止多个程序集生成相同全限定类型名,也禁止把生成 namespace 嵌套在可能同名的 Control
类型下面。
21. 明确删除的旧设计
新架构实现时删除:
Theme、ITheme以及预创建 Theme variant pool。Theme.SharedToken、Theme.GetControlToken()、ThemeConfigProvider.SharedToken、ThemeConfigProvider.ControlTokens和所有 clone compatibility map。- 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。- 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 默认配置开始。- Control 覆盖不泄漏到 Content 或嵌套控件。
- 同一主题的新 runtime override 会产生新有效配置,不复用过期 Theme 对象。
22.2 事务
- Dark + Compact 只产生一次完整提交。
- 根主题变化和所有受影响局部作用域在通知前完成引用替换。
- 配置失败、编译失败和 stale result 不发布部分状态。
- 重入和高频更新不会递归提交,latest request 语义确定。
- 订阅者异常不阻断提交或其他订阅者。
22.3 资源与生命周期
- 根作用域和每个局部作用域始终只有一个 Token ResourceProvider。
- 重复更新不增长
MergedDictionaries。 - 控件实例不创建 Control Token 资源字典。
- Popup、Flyout、Dialog 和窗口覆盖层继承 owner ThemeContext。
- detach、reparent、关闭和回收后不保留旧作用域或 Control。
22.4 性能、内存与 AOT
- 同一主题定义 revision 只读取和绑定一次。
- 等价有效配置只编译一次。
- 单 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 的引用。