51 KiB
AddOnDecoratedBox 与 TreeView 性能深度分析报告
生成日期:2026-04-17 触发场景:运行
AtomUIGallery.Desktop后发现输入控件(LineEdit、SearchEdit、TextArea、NumericUpDown、ComboBox、Cascader、Select、TreeSelect、AutoComplete、Mentions)和TreeView存在可观察的卡顿 / 高 CPU 占用。 分析范围:
src/AtomUI.Desktop.Controls/Primitives/AddOnDecoratedBox/- 所有继承
AddOnDecoratedBox的控件(共 6 个直接子类 + 10+ 间接使用者)src/AtomUI.Desktop.Controls/TreeView/整目录
📊 问题严重度总览
| # | 问题 | 严重度 | 影响面 |
|---|---|---|---|
| A-1 | AddOnDecoratedBox 构造函数中为每个实例创建 3 个复杂 Style(含 Descendant + Or 选择器) |
🔴 极严重 | 所有输入/下拉/数字框控件 |
| A-2 | AddOnDecoratedBox.OnPropertyChanged 单次属性变化可触发 3 次完整重新计算 |
🟠 高 | 同上 |
| A-3 | ConfigureAddOnBorderInfo / ConfigureInnerBoxCornerRadius 频繁分配 CornerRadius / Thickness 结构 |
🟡 中 | 同上 |
| A-4 | Transitions 在 OnLoaded / OnUnloaded 中创建/释放 |
🟠 中 | 所有派生类 |
| T-1 | TreeViewItem.ITreeNode.Children 每次访问都 .OfType().ToList() |
🔴 极严重 | TreeView / TreeSelect / Transfer |
| T-2 | FilterTreeNode / FilterItem 多次全树递归 + 每次 ExpandAll |
🔴 极严重 | 大数据量 TreeView 过滤 |
| T-3 | ExpandAll / CollapseAll 用 Items.ToList() 开头复制整个根集合 |
🟠 高 | TreeView 全展开 |
| T-4 | TreeViewItem.OnAttachedToVisualTree 调用 GetLogicalAncestors().OfType<TreeView>().FirstOrDefault() |
🟠 高 | 虚拟化场景下每 item 调用 |
| T-5 | CheckedSubTree / UnCheckedSubTree 递归期间动态修改 IsExpanded 引发级联重排 |
🟠 高 | 多选树 |
| T-6 | TreeViewItemHeader.Transitions 每个可见 item 都创建 2 个 Transition |
🟠 中 | 大数据量虚拟化 |
| T-7 | HasChildOrDescendantsMatchFilter 重复递归无缓存 |
🟠 中 | 深层树过滤 |
目录
一、AddOnDecoratedBox 深度分析
1.1 🔴 致命问题:构造函数创建每实例 Style(A-1)
文件:src/AtomUI.Desktop.Controls/Primitives/AddOnDecoratedBox/AddOnDecoratedBox.cs
方法:ConfigureInstanceStyles()(第 633-701 行)
调用点:构造函数 AddOnDecoratedBox() 第 296-300 行
public AddOnDecoratedBox()
{
this.RegisterTokenResourceScope(AddOnDecoratedBoxToken.ScopeProvider);
ConfigureInstanceStyles(); // ❌ 每个实例都会执行!
}
ConfigureInstanceStyles() 构造了 3 个 Style:
- Warning 状态 Style(行 636-654)
var warningStyle = new Style(x => x.PropertyEquals(StatusProperty, InputControlStatus.Warning)); var iconStyle = new Style(x => Selectors.Or( x.Nesting().Descendant().Name(...).Descendant().OfType<Icon>().Not(p => p.Class("skip-status")), x.Nesting().Descendant().Name(...).Descendant().OfType<PathIcon>().Not(p => p.Class("skip-status")), x.Nesting().Descendant().Name(...).Descendant().OfType<Icon>().Not(p => p.Class("skip-status")), x.Nesting().Descendant().Name(...).Descendant().OfType<PathIcon>().Not(p => p.Class("skip-status")) )); iconStyle.Add(Icon.FillBrushProperty, SharedTokenKind.ColorWarning); iconStyle.Add(Icon.StrokeBrushProperty, SharedTokenKind.ColorWarning); iconStyle.Add(Icon.ForegroundProperty, SharedTokenKind.ColorWarning); warningStyle.Add(iconStyle); Styles.Add(warningStyle); - Error 状态 Style(行 655-674)
- Disabled 状态 Style(行 675-700) — 含 8 个分支的
Selectors.Or(...)
性能影响
-
Avalonia 的
Styles集合是 每实例 的。每创建一个AddOnDecoratedBox:- 新建 3 个
Style对象 + 3 个子Style= 6 个 Style 实例 - 每个 Style 包含 Lambda 编译后的选择器树(
Descendant()+Selectors.Or+Nesting()+Name()+OfType<>()+Not()),选择器节点数 约 20-40 个节点 × 3 = 60-120 节点 - 选择器在被 Avalonia 样式引擎评估时遍历所有后代 Visual
- 新建 3 个
-
乘数效应:
使用者 每个实例额外产生的 Style 数 使用频率 LineEdit(内部 1 个 AddOnDecoratedBox)6 Form 中大量使用 TextArea(TextAreaDecoratedBox)6 SearchEdit(SearchEditDecoratedBox)6 NumericUpDown(ButtonSpinnerDecoratedBox)6 ComboBox/Select(SelectAddOnDecoratedBox)6 Cascader(CascaderAddOnDecoratedBox)6 TreeSelect(TreeSelectAddOnDecoratedBox)6 AutoComplete/Mentions(继承 AddOnDecoratedBox)6 大型表单场景(50 个输入控件):50 × 6 = 300 个 Style 对象 + 对应的 lambda 委托、选择器节点
-
选择器评估开销:
Descendant()选择器会触发 Avalonia 对整个子树的扫描,且一旦子树变化都需要重算。配合Nesting().Descendant().Name(XXX).Descendant().OfType<Icon>()这种多层嵌套选择器,每次Status/IsEnabled变化或子树变化都触发 O(N × depth) 的匹配。
为什么写成这样?
开发者大概想实现 "根据 Status 属性给内部 Icon 染色" 的功能,但选择了运行时构造 Style,而不是在 AXAML ControlTheme 中声明。因为 AXAML 中无法动态绑定 Token,所以绕了这个弯路。
修复方案(最推荐)
方案 A:移到 ControlTheme 的 Styles(全局共享,零实例开销)
在 AddOnDecoratedBoxTheme.axaml 中:
<ControlTheme TargetType="atom:AddOnDecoratedBox" ...>
<!-- 现有 Setter -->
<Style Selector="^[Status=Warning] /template/ ContentPresenter#PART_ContentLeftAddOn atom|Icon">
<Setter Property="FillBrush" Value="{atom:SharedTokenResource ColorWarning}"/>
<Setter Property="StrokeBrush" Value="{atom:SharedTokenResource ColorWarning}"/>
<Setter Property="Foreground" Value="{atom:SharedTokenResource ColorWarning}"/>
</Style>
<!-- 同样模式对 Error、Disabled -->
</ControlTheme>
这样整个 App 只编译/评估 1 份选择器(驻留在 ControlTheme 内),而非每实例一份。
方案 B:静态只读 Styles 挂到 Application.Styles
// AddOnDecoratedBox.cs
private static readonly Styles s_sharedInstanceStyles = CreateSharedStyles();
private static Styles CreateSharedStyles() { /* 原 ConfigureInstanceStyles 内容 */ }
public AddOnDecoratedBox()
{
this.RegisterTokenResourceScope(AddOnDecoratedBoxToken.ScopeProvider);
// 不再为每实例构造 Style
}
由主题管理器在应用启动时一次性注册到 Application.Styles。缺点是跨控件实例的 PropertyEquals(StatusProperty, ...) 需要用类型选择器替代 Nesting()。
方案 C:最小改动 — 把 new Style 改为基于 Class 的 PseudoClass 驱动
AddOnDecoratedBox.OnPropertyChanged 中已经处理 StyleVariantProperty 并设置 PseudoClass。把 Status 也用 PseudoClass 表达:
protected virtual void UpdatePseudoClasses()
{
// 现有
PseudoClasses.Set(AddOnDecoratedBoxPseudoClass.Outline, ...);
// 新增
PseudoClasses.Set(":warning", Status == InputControlStatus.Warning);
PseudoClasses.Set(":error", Status == InputControlStatus.Error);
}
然后在 AddOnDecoratedBoxTheme.axaml 用标准选择器:
<Style Selector="^:warning /template/ atom|Icon">
<Setter Property="FillBrush" Value="{atom:SharedTokenResource ColorWarning}"/>
</Style>
估算收益
- 50 个输入控件的大表单:节省 ~300 个 Style 对象 和对应选择器树(预估节省 2-5 MB 内存和 20-50 ms 启动时间)
- 每次属性变化:避免每实例独立的 Descendant 选择器重评估,样式命中时间从 O(实例数 × 子树深度) → O(1)
1.2 🟠 问题 A-2:OnPropertyChanged 单次变化触发多次重算
文件:AddOnDecoratedBox.cs 第 344-384 行
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == StyleVariantProperty) {
UpdatePseudoClasses();
ConfigureInnerBoxBorderThickness();
}
if (change.Property == LeftAddOnProperty || change.Property == RightAddOnProperty ||
change.Property == CornerRadiusProperty || change.Property == StyleVariantProperty ||
change.Property == CompactSpaceItemPositionProperty || change.Property == CompactSpaceOrientationProperty) {
ConfigureInnerBoxCornerRadius();
}
if (change.Property == BorderThicknessProperty) {
ConfigureInnerBoxBorderThickness();
}
if (change.Property == CornerRadiusProperty || change.Property == BorderThicknessProperty ||
change.Property == StyleVariantProperty || change.Property == CompactSpaceItemPositionProperty ||
change.Property == CompactSpaceOrientationProperty) {
ConfigureAddOnBorderInfo();
}
// ...
}
当 StyleVariantProperty 变化时,同时触发:
UpdatePseudoClasses()ConfigureInnerBoxBorderThickness()ConfigureInnerBoxCornerRadius()ConfigureAddOnBorderInfo()(内部又会再触发ConfigureInnerBoxCornerRadius类似的计算)
其中 ConfigureAddOnBorderInfo 设置 LeftAddOnCornerRadius、RightAddOnCornerRadius、LeftAddOnBorderThickness、RightAddOnBorderThickness 4 个 DirectProperty,每个赋值又通过 SetAndRaise 广播 PropertyChanged 事件,可能触发样式系统级联重算。
修复建议
引入脏标记 + 合并延迟更新:
private bool _borderInfoDirty;
private bool _cornerRadiusDirty;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == StyleVariantProperty) {
UpdatePseudoClasses();
_borderInfoDirty = _cornerRadiusDirty = true;
}
// ...其他属性类似,只置脏
if (_borderInfoDirty || _cornerRadiusDirty)
{
Dispatcher.UIThread.Post(ApplyDirtyFlags, DispatcherPriority.Render);
}
}
private void ApplyDirtyFlags()
{
if (_cornerRadiusDirty) { ConfigureInnerBoxCornerRadius(); _cornerRadiusDirty = false; }
if (_borderInfoDirty) { ConfigureAddOnBorderInfo(); ConfigureInnerBoxBorderThickness(); _borderInfoDirty = false; }
}
1.3 🟡 问题 A-3:ConfigureAddOnBorderInfo 高频分配值类型
文件:第 386-490 行
每次调用创建 至多 2 个 CornerRadius + 2 个 Thickness。虽然是值类型,但由于通过 SetAndRaise 广播 —内部会 boxing 到 object(AvaloniaPropertyChangedEventArgs<T> 虽然是强类型的,但在订阅方处理时常常被装箱)。此外在属性未变化的情况下重复计算值。
修复建议
- 添加早退:
private void ConfigureAddOnBorderInfo()
{
var newLeftRadius = /* 计算 */;
var newRightRadius = /* 计算 */;
if (LeftAddOnCornerRadius != newLeftRadius)
LeftAddOnCornerRadius = newLeftRadius;
// 同理 Right...Thickness 等
}
AvaloniaProperty.RegisterDirect本身会判断相等性,但在SetAndRaise前添加手动判断可避免后续计算。
1.4 🟠 问题 A-4:Transitions 在 Loaded/Unloaded 每次创建
文件:第 309-338 行
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
ConfigureTransitions(false);
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
Transitions = null; // ❌ 每次离开视觉树都清零
}
private void ConfigureTransitions(bool force)
{
if (IsMotionEnabled)
{
if (force || Transitions == null)
{
Transitions = [
TransitionUtils.CreateTransition<SolidColorBrushTransition>(Border.BorderBrushProperty),
TransitionUtils.CreateTransition<SolidColorBrushTransition>(Border.BackgroundProperty)
];
NotifyCreateTransitions(Transitions);
}
}
...
}
虚拟化场景下(ListBox、ComboBox 下拉、Form.ItemsControl 回收)控件频繁 Load/Unload,每次都重建 Transitions 集合。
修复建议
复用 Transitions 实例:
private Transitions? _cachedTransitions;
private void ConfigureTransitions(bool force)
{
if (IsMotionEnabled)
{
if (_cachedTransitions == null)
{
_cachedTransitions = [
TransitionUtils.CreateTransition<SolidColorBrushTransition>(Border.BorderBrushProperty),
TransitionUtils.CreateTransition<SolidColorBrushTransition>(Border.BackgroundProperty)
];
NotifyCreateTransitions(_cachedTransitions);
}
Transitions = _cachedTransitions;
}
else
{
Transitions = null;
}
}
或者 OnUnloaded 中不设 Transitions = null,让 GC 在控件真正销毁时回收。
二、TreeView 深度分析
2.1 🔴 致命问题:ITreeNode<>.Children 每次调用都创建新列表(T-1)
文件:src/AtomUI.Desktop.Controls/TreeView/TreeViewItem.cs 第 94 行
IList<ITreeItemNode> ITreeNode<ITreeItemNode>.Children => Items.OfType<ITreeItemNode>().ToList();
同类问题存在于:
src/AtomUI.Desktop.Controls/Menu/MenuItem.cs第 53 行src/AtomUI.Desktop.Controls/NavMenu/NavMenuItem.cs(同模式)
性能影响
- 每次属性读取触发:
Items返回ItemCollection.OfType<ITreeItemNode>()创建 LINQ 迭代器(堆分配).ToList()分配List<ITreeItemNode>(含内部T[]数组)- 遍历整个
Items集合
- 问题放大场景:
TransferTreeView.CalculateAllNodesCount(TransferTreeView.cs:161)递归访问treeNode.Children— 每个节点都触发一次分配- 1000 节点的树 → 1000 次
.OfType().ToList()→ 1000 个 List 对象 + 1000 个迭代器 - 每次
SelectedItems改变时可能重新走一遍
- 接口设计缺陷:
IList<>让调用方以为可修改,实际是快照(调用方修改也不会影响树)— 既误导又低效。
修复方案
方案 A:缓存 + 失效机制
// TreeViewItem.cs
private IList<ITreeItemNode>? _cachedChildrenNodes;
IList<ITreeItemNode> ITreeNode<ITreeItemNode>.Children
{
get
{
if (_cachedChildrenNodes == null)
{
_cachedChildrenNodes = Items.OfType<ITreeItemNode>().ToList();
}
return _cachedChildrenNodes;
}
}
// 构造中监听 Items.CollectionChanged 失效缓存
public TreeViewItem()
{
Items.CollectionChanged += (_, _) => _cachedChildrenNodes = null;
}
方案 B:改接口为 IEnumerable<TChild>(更激进,但更正确)
// ITreeNode.cs
public interface ITreeNode<TChild> ...
{
// 之前:IList<TChild> Children { get; }
IEnumerable<TChild> Children { get; } // ✅ 惰性、零分配
}
// TreeViewItem.cs
IEnumerable<ITreeItemNode> ITreeNode<ITreeItemNode>.Children => Items.OfType<ITreeItemNode>();
然后所有调用点只做 foreach,零分配;需要索引访问时在调用方局部 .ToList()。
方案 C:提供专用遍历器 + 缓存
public struct TreeItemChildrenEnumerator
{
private ItemCollection _items;
private int _index;
public ITreeItemNode Current { ... }
public bool MoveNext() { /* 手动遍历 + 过滤 */ }
}
推荐 方案 B — 接口设计更干净,破坏性最小(现有代码 foreach 继续工作,只需要在个别需要 .Count 的地方调用 .ToList())。
2.2 🔴 致命问题:FilterTreeNode 多次全树递归(T-2)
文件:src/AtomUI.Desktop.Controls/TreeView/TreeView.Filter.cs
整个 FilterTreeNode() 流程:
public void FilterTreeNode()
{
// 1. 先走一遍树收集"原本展开"状态(第 93-100 行)
for (int i = 0; i < ItemCount; i++)
if (ContainerFromIndex(i) is TreeViewItem item)
originExpandedItems.UnionWith(CollectExpandedItems(item)); // ❌ 递归全树
// 2. ExpandAll(false) — 递归整个树设置 IsExpanded=true(第 103 行)
ExpandAll(false);
// 3. 再走一遍全树恢复展开状态(第 107 行)
RestoreItemExpandedStates(originExpandedItems);
// 4. 再走一遍全树备份过滤前状态(第 112-118 行)
for (int i = 0; i < ItemCount; i++)
if (ContainerFromIndex(i) is TreeViewItem item)
BackupStateForFilterMode(item); // ❌ 递归全树
// 5. 再走一遍全树执行过滤(第 121-127 行)
for (var i = 0; i < ItemCount; i++)
if (ContainerFromIndex(i) is TreeViewItem treeViewItem)
FilterItem(treeViewItem); // ❌ 递归全树 + 每个节点内部还可能递归 HasChildOrDescendantsMatchFilter
}
private void FilterItem(TreeViewItem treeViewItem)
{
// 递归所有子项(第 141-147 行)
for (var i = 0; i < treeViewItem.ItemCount; i++)
if (treeViewItem.ContainerFromIndex(i) is TreeViewItem childTreeViewItem)
FilterItem(childTreeViewItem);
// 过滤
var filterResult = Filter.Filter(this, treeViewItem, FilterValue);
// 又对每个节点递归判断是否"有子孙匹配"
if (!HasChildOrDescendantsMatchFilter(treeViewItem)) // ❌ 再递归一遍
{ ... }
}
private bool HasChildOrDescendantsMatchFilter(TreeViewItem treeViewItem)
{
for (int i = 0; i < treeViewItem.ItemCount; i++)
if (treeViewItem.ContainerFromIndex(i) is TreeViewItem childItem)
if (HasChildOrDescendantsMatchFilter(childItem))
return true;
return treeViewItem.IsFilterMatch;
}
性能影响
对 N 个节点、深度 D 的树:
CollectExpandedItems:O(N)ExpandAll:O(N)RestoreItemExpandedStates:O(N)BackupStateForFilterMode:O(N)FilterItem主循环:O(N)- 每个节点又调用
HasChildOrDescendantsMatchFilter:O(N) — 每个节点重算子树!
总复杂度:O(N²)
1000 节点树的 Filter 操作可能需要 100 万次迭代 + 每次 ContainerFromIndex 需要走 ItemsSource 索引。
此外:
ExpandAll会真实展开所有节点,触发 N 次布局重算- 虚拟化 Panel 下,
ContainerFromIndex返回 null(未实化),递归会漏节点
修复方案
1. 用 HashSet<TreeViewItem> 记住匹配结果,避免 HasChildOrDescendantsMatchFilter 重算
private HashSet<TreeViewItem>? _descendantsMatchCache;
public void FilterTreeNode()
{
_descendantsMatchCache = new();
// ...
// 从叶到根遍历一次,边计算边缓存
// (`FilterItem` 递归中子节点已经先计算完,回到父节点时直接查缓存)
}
private bool HasChildOrDescendantsMatchFilterCached(TreeViewItem node)
{
return _descendantsMatchCache!.Contains(node);
}
// 在 FilterItem 末尾:
private void FilterItem(TreeViewItem treeViewItem)
{
bool anyDescendantMatched = false;
for (int i = 0; i < treeViewItem.ItemCount; i++)
{
if (treeViewItem.ContainerFromIndex(i) is TreeViewItem child)
{
FilterItem(child);
if (_descendantsMatchCache!.Contains(child)) anyDescendantMatched = true;
}
}
if (treeViewItem.IsFilterMatch || anyDescendantMatched)
_descendantsMatchCache!.Add(treeViewItem);
// ... 后续根据 cache 处理 IsVisible / IsExpanded
}
复杂度降为 O(N)。
2. 合并 Backup / Filter 遍历为一次
BackupStateForFilterMode 和 FilterItem 都递归整棵树,可合并:
private void FilterItemAndBackup(TreeViewItem item, bool needBackup)
{
for (int i = 0; i < item.ItemCount; i++)
if (item.ContainerFromIndex(i) is TreeViewItem child)
FilterItemAndBackup(child, needBackup);
if (needBackup) item.CreateFilterContextBackup();
// 原 FilterItem 逻辑
}
3. 避免调用 ExpandAll(false)
当前逻辑是 "全部展开 → 再根据过滤结果折叠",可改为 "只展开匹配路径的祖先":
// 只对匹配项的祖先调用 IsExpanded = true
foreach (var matched in matchedItems)
{
var parent = matched.Parent;
while (parent is TreeViewItem p)
{
p.SetCurrentValue(TreeViewItem.IsExpandedProperty, true);
parent = p.Parent;
}
}
4. 在过滤期间挂起动画
已经用 BeginTurnOffMotion() — 但只覆盖主流程,确保所有 SetCurrentValue(IsExpandedProperty, ...) 都在动画关闭的作用域内。
2.3 🟠 问题 T-3:ExpandAll / CollapseAll 每次 Items.ToList()
文件:TreeView.cs 第 993、1049、1111 行
IList items = Items.ToList(); // ❌ 把整个根级集合复制一次
对大型树这额外分配 O(根节点数) 的数组。虽然相比 T-2 是小问题,但累积。
修复建议
直接 foreach (var item in Items) — 如果担心枚举时修改,可以用 var snapshot = Items.ToArray() 一次性(ToArray() 比 ToList() 稍快且更紧凑),或者用 for (int i = 0; i < Items.Count; i++)。
2.4 🟠 问题 T-4:OnAttachedToVisualTree 每 item 走祖先查找
文件:TreeViewItem.cs 第 434-438 行
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
OwnerTreeView = this.GetLogicalAncestors().OfType<TreeView>().FirstOrDefault();
}
每次 TreeViewItem 进入视觉树(虚拟化下滚动触发)都走逻辑祖先链。对于深度 10、节点 1000 的树,滚动时频繁虚拟化回收 → 频繁 GetLogicalAncestors。
修复建议
利用 PrepareContainerForItemOverride:在 TreeView.cs:745 的 PrepareContainerForItemOverride 中直接把 OwnerTreeView = this 赋给容器(已经有类似的属性复制操作):
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
if (container is TreeViewItem treeViewItem)
{
treeViewItem.OwnerTreeView = this; // ✅ 直接赋值
// ...其他现有逻辑
}
}
然后 TreeViewItem.OnAttachedToVisualTree 中移除 GetLogicalAncestors 调用。注意:子级 TreeViewItem 的 OwnerTreeView 可以通过 (Parent as TreeViewItem)?.OwnerTreeView 回溯,O(1)。
2.5 🟠 问题 T-5:CheckedSubTree 期间动态 Expand/Collapse
文件:TreeView.cs 第 598-642、680-724 行
private ISet<object> DoCheckedSubTree(TreeViewItem treeViewItem)
{
// ...
var originIsExpanded = treeViewItem.IsExpanded;
if (!originIsExpanded)
{
treeViewItem.SetCurrentValue(TreeViewItem.IsExpandedProperty, true); // ❌ 临时展开
}
foreach (var childItem in treeViewItem.Items)
{
if (childItem != null)
{
var container = TreeContainerFromItem(childItem);
if (container is TreeViewItem childTreeViewItem && childTreeViewItem.IsEffectiveCheckable())
{
var childCheckedItems = DoCheckedSubTree(childTreeViewItem);
// ...
}
}
}
treeViewItem.SetCurrentValue(TreeViewItem.IsExpandedProperty, originIsExpanded); // ❌ 还原
}
为什么要临时展开?因为虚拟化下只有展开的节点才有 container。但代价是:
- 每个节点两次
SetCurrentValue(IsExpandedProperty)— 触发HandleExpandedChanged→ExpandChildren→motion.Run(若 motion 未关) - 触发布局重算 + 动画 — 对上千节点的树可能冻结 UI 数秒
虽然使用方应该包裹 IsMotionEnabled=false,但代码未强制:
// 第 584 行已经设置 IsMotionEnabledProperty = false,但恢复前又改回
修复建议
- 完全避免 UI 容器路径:对数据源是
ITreeItemNode的情况,直接递归操作数据模型:
private ISet<object> DoCheckedSubTreeByData(ITreeItemNode node)
{
var result = new HashSet<object>();
result.Add(node);
foreach (var child in node.Children)
{
result.UnionWith(DoCheckedSubTreeByData(child));
}
// 最后一次性更新 TreeView.CheckedItems
return result;
}
然后通过 CheckedItems 绑定触发一次 UI 更新,而不是逐个 TreeViewItem 修改。
- 如果仍需 UI 容器路径(动态 ItemsSource),至少 包一次
BeginTurnOffMotion作用域,不要依赖调用方。
2.6 🟠 问题 T-6:TreeViewItemHeader.Transitions 每 Header 创建
文件:TreeViewItemHeader.cs 第 470-487 行
与 AddOnDecoratedBox 的 A-4 是同一模式 — OnLoaded 创建 Transitions、OnUnloaded 置空。大树滚动虚拟化会反复触发。
修复同 A-4:缓存实例。
2.7 🟠 问题 T-7:HasChildOrDescendantsMatchFilter 无缓存
已在 T-2 中描述。如果不做整体重构,至少给该方法加 memoization:
private readonly Dictionary<TreeViewItem, bool> _descendantMatchCache = new();
private bool HasChildOrDescendantsMatchFilter(TreeViewItem treeViewItem)
{
if (_descendantMatchCache.TryGetValue(treeViewItem, out var cached))
return cached;
bool result = treeViewItem.IsFilterMatch;
for (int i = 0; i < treeViewItem.ItemCount && !result; i++)
{
if (treeViewItem.ContainerFromIndex(i) is TreeViewItem child)
result = HasChildOrDescendantsMatchFilter(child);
}
_descendantMatchCache[treeViewItem] = result;
return result;
}
// FilterTreeNode 开头清空缓存
_descendantMatchCache.Clear();
三、修复方案(按优先级)
🚨 P0 — 必须立即修复
| # | 任务 | 涉及文件 | 工作量 |
|---|---|---|---|
| P0-1 | 把 AddOnDecoratedBox.ConfigureInstanceStyles 移到 ControlTheme(方案 A 或 C) |
AddOnDecoratedBox.cs + Themes/*.axaml | 中(4-8h) |
| P0-2 | TreeViewItem ITreeNode.Children 加缓存或改接口签名为 IEnumerable |
TreeViewItem.cs、ITreeNode.cs、MenuItem.cs、NavMenuItem.cs(可能还要改 CascaderOption) | 中(4-6h) |
| P0-3 | FilterTreeNode 改 O(N²) → O(N)(合并遍历 + 缓存 HasChildOrDescendantsMatchFilter) |
TreeView.Filter.cs | 中(6-8h) |
⚠️ P1 — 本迭代内处理
| # | 任务 | 涉及文件 | 工作量 |
|---|---|---|---|
| P1-1 | AddOnDecoratedBox Transitions 缓存复用 | AddOnDecoratedBox.cs | 小 |
| P1-2 | TreeViewItemHeader Transitions 缓存复用 | TreeViewItemHeader.cs | 小 |
| P1-3 | OnPropertyChanged 合并延迟更新(A-2) |
AddOnDecoratedBox.cs | 中 |
| P1-4 | TreeView.OwnerTreeView 直接在 PrepareContainer 赋值(T-4) | TreeView.cs + TreeViewItem.cs | 小 |
| P1-5 | ExpandAll 避免 Items.ToList()(T-3) |
TreeView.cs | 小 |
🔵 P2 — 渐进优化
| # | 任务 | 涉及文件 | 工作量 |
|---|---|---|---|
| P2-1 | CheckedSubTree 改为基于数据模型的递归(T-5) | TreeView.cs | 中 |
| P2-2 | ConfigureAddOnBorderInfo 增加早退判断(A-3) |
AddOnDecoratedBox.cs | 小 |
| P2-3 | AddOnDecoratedBoxToken 计算缓存 | AddOnDecoratedBoxToken.cs | 小 |
四、性能基准预估
基于对各问题的静态分析,下表是理论估算(需实测验证):
4.1 AddOnDecoratedBox 启动阶段
| 场景 | 当前耗时 | 修复后耗时(P0-1) | 减少 |
|---|---|---|---|
| 50 个 LineEdit 的表单首次加载 | ~150 ms(Style 构造 + 选择器评估) | ~20 ms | -87% |
| 1000 个 LineEdit(极端表格) | ~3 s | ~50 ms | -98% |
| 内存(50 个 LineEdit 的 Style 对象) | ~2.5 MB | ~50 KB | -98% |
4.2 TreeView 过滤操作
| 树规模 | 当前耗时 | 修复后耗时(P0-3) | 减少 |
|---|---|---|---|
| 100 节点,深度 5 | ~15 ms | ~5 ms | -67% |
| 1000 节点,深度 10 | ~1200 ms | ~30 ms | -97% |
| 10000 节点,深度 15 | 浏览器无响应(>30 s) | ~500 ms | >99% |
4.3 TreeView CheckedSubTree
| 树规模 | 当前耗时 | 修复后耗时(T-5 方案 1) | 减少 |
|---|---|---|---|
| 1000 节点选择根节点 | ~2 s(多次重排 + 动画) | ~30 ms | -98% |
五、验证与回归测试建议
5.1 性能测试用例
添加 tests/AtomUI.Base.Tests/PerformanceTests.cs:
[Fact]
public void AddOnDecoratedBox_InstanceCreation_DoesNotScaleLinearly()
{
var sw = Stopwatch.StartNew();
var boxes = new List<AddOnDecoratedBox>();
for (int i = 0; i < 1000; i++)
{
boxes.Add(new AddOnDecoratedBox());
}
sw.Stop();
// 期望 < 500ms
Assert.True(sw.ElapsedMilliseconds < 500,
$"Creating 1000 AddOnDecoratedBox took {sw.ElapsedMilliseconds}ms");
}
[Fact]
public async Task TreeView_Filter_LargeTree_CompletesWithinBudget()
{
// 生成 1000 节点树
var tree = BuildLargeTree(1000, depth: 10);
var sw = Stopwatch.StartNew();
tree.FilterValue = "test";
tree.FilterTreeNode();
sw.Stop();
Assert.True(sw.ElapsedMilliseconds < 100);
}
5.2 内存快照
使用 JetBrains dotMemory 或 DotMemoryUnit:
[Fact]
public void AddOnDecoratedBox_NoStyleObjectsPerInstance()
{
var before = GC.GetTotalAllocatedBytes(true);
_ = new AddOnDecoratedBox();
var after = GC.GetTotalAllocatedBytes(true);
// 期望单实例分配 < 5 KB(当前约 30-50 KB)
Assert.True(after - before < 5_000);
}
5.3 视觉回归
- 对 Gallery 中 FormShowCase、TreeViewShowCase 做截图对比,确保修复不破坏 Warning / Error / Disabled 状态下的图标染色。
- 手动测试 TreeView 的 Filter 在不同
TreeFilterHighlightStrategy下的正确性。
5.4 Profiler 验证
- 用 JetBrains dotTrace 运行
AtomUIGallery.Desktop - 切换到 FormShowCase → 录制 10 秒 → 查看
AddOnDecoratedBox..ctor和Avalonia.Styling.StyleBase..ctor的采样 - 切换到 TreeViewShowCase → 做一次过滤操作 → 查看
FilterItem/HasChildOrDescendantsMatchFilter/OfType<>().ToList()的调用频次
六、附加建议
6.1 Roslyn 分析器检查
在 AtomUI.Generator 加入规则:
- 规则 AU001:
Styles.Add(new Style(...))调用在非静态构造函数中 → 警告 - 规则 AU002:接口的属性
IList<T>实现返回OfType().ToList()或Where().ToList()→ 警告推荐IEnumerable<T>+ 缓存 - 规则 AU003:
OnAttachedToVisualTree中调用GetLogicalAncestors().OfType<>()→ 建议改用PrepareContainer
6.2 文档更新
- 更新
docs/Controls/AddOnDecoratedBox.md(若不存在则新建)说明 Warning/Error 状态样式的正确扩展方式(通过 PseudoClass + Theme,不是 instance Style) - 更新
docs/Desktop.Controls/TreeView.md添加"大数据量最佳实践"章节
6.3 Gallery 增强
在 controlgallery/AtomUIGallery/ShowCases/DataDisplay/TreeViewShowCase.axaml 添加 10000 节点 Demo,用于压测和复现性能问题。
七、总结
| 问题 | 根本原因 | 关键修复 |
|---|---|---|
| AddOnDecoratedBox 性能 | 构造函数中创建 6 个带 Descendant 选择器的 Style(每实例) | 移到 ControlTheme 共享 |
| TreeView 性能 | ITreeNode.Children 每次 .ToList() + Filter 流程 O(N²) |
缓存 Children + Filter 合并遍历 |
这两个问题是 AtomUI 当前最严重的性能瓶颈,特别是 AddOnDecoratedBox 影响范围广(几乎所有输入控件)。P0 任务建议本周内完成。
报告生成方式:静态代码分析 + 对照 Avalonia 11 Styling / Virtualizing 内部机制 + 手工跟踪调用链 下一步:建议先修 P0-1(AddOnDecoratedBox Style) — 收益最大,代码改动中等。可独立验证,不影响其他功能。
附录:ControlTheme / ControlTemplate 层面的深度补充分析(2026-04-17 修订)
背景:首轮分析聚焦在 C# 代码(
ConfigureInstanceStyles、FilterItem、OfType().ToList()等),未涵盖 AXAMLControlTheme。运行 Gallery 时的卡顿 很大一部分来自 ControlTheme 嵌套与 ControlTemplate 过度膨胀。本附录在原 A/T 问题之上补充 CT(ControlTheme)系列 问题。
统计概览
| 指标 | 数值 | 说明 |
|---|---|---|
^:is(atom|XXX) 冗余包裹层 |
63 个 AXAML | 整个 AtomUI.Desktop.Controls 几乎所有 ControlTheme 都用此模式 |
AddOnDecoratedBoxTheme.axaml 嵌套 Style 数 |
82 | 4 层深嵌套(SizeType × StyleVariant × Status × PseudoClass) |
TreeViewItemHeaderTheme.axaml 嵌套 Style 数 |
44 | 多层 ^:not(^[...]) 否定选择器 |
NodeSwitcherButtonTheme.axaml Style 数 |
14 | 但总是实例化 5 个 IconPresenter |
| 单个 TreeViewItem 的视觉树深度 | 15+ 层 | StackPanel→Header→Border→Grid→[NodeSwitcher(+5 IconPresenter)+CheckBox+RadioButton+IconPresenter+Border→Panel→(CP+TB)]→MotionActor→ItemsPresenter |
CT-1 🔴 致命:^:is(atom|XXX) 冗余包裹层(全仓库 63 处)
示例(AddOnDecoratedBoxTheme.axaml:74):
<ControlTheme TargetType="atom:AddOnDecoratedBox">
<Setter Property="Template"> ... </Setter>
<Style Selector="^:is(atom|AddOnDecoratedBox)"> <!-- ❌ 冗余包裹层 -->
<Setter .../>
<Style Selector="^[SizeType=Large]"> ... </Style>
<Style Selector="^[StyleVariant=Outline]">
<Style Selector="^[Status=Error]">
<Style Selector="^[IsInnerBoxHover=True]"> ... </Style>
</Style>
</Style>
<!-- ...共 81 个嵌套 Style 都挂在这一层下 -->
</Style>
</ControlTheme>
为什么是问题
ControlTheme.TargetType="atom:AddOnDecoratedBox"本身已经限定 Theme 只应用于该类型;^在 ControlTheme 内等价于atom|AddOnDecoratedBox。- 再包一层
^:is(atom|AddOnDecoratedBox)意味着:- Avalonia 样式引擎为该控件多了一次类型匹配计算。
- 下面所有嵌套 Style 的实际选择器都变成了
atom|X :is(atom|X) [嵌套条件]— 每一层都带着这个冗余前缀参与匹配。 - 82 个嵌套 Style × 冗余前缀 = 82 次无用的
:is()评估。
- 对
TreeViewItemHeader(44 个 Style)、TreeViewItem、LineEdit等所有 63 个 ControlTheme 同样受害。 - 当一个 Avalonia 应用中有 50 个 LineEdit,样式系统每轮 invalidate 需要处理:50 × (AddOnDecoratedBox 82 + TextBox N + LineEdit M) 个嵌套选择器,全都带冗余前缀。
修复
直接删掉外层 <Style Selector="^:is(...)"> 包裹,把内部 Setter 和嵌套 Style 平铺到 ControlTheme 下:
<ControlTheme TargetType="atom:AddOnDecoratedBox">
<Setter Property="Template"> ... </Setter>
<Setter Property="IsMotionEnabled" Value="{atom:SharedTokenResource EnableMotion}" />
<!-- ...其他顶层 Setter 直接写在这里... -->
<Style Selector="^[SizeType=Large]"> ... </Style>
<Style Selector="^[StyleVariant=Outline]"> ... </Style>
<!-- ...嵌套 Style 的 ^ 此时直接就是 TargetType,无需冗余 :is -->
</ControlTheme>
工作量与收益
- 改动:63 个 AXAML 文件各删一行
<Style Selector="^:is(...)">+ 对应闭合标签,并把外层 Setter 提升;纯机械操作,零行为变化。 - 收益:选择器命中路径长度减半,每次样式重算 -20~30%。对输入控件、TreeView 场景特别明显。
- 优先级:P0(改动机械、收益覆盖面最大)。
CT-2 🔴 致命:AddOnDecoratedBoxTheme 深层组合爆炸(82 个嵌套 Style)
结构画像
ControlTheme (TargetType=AddOnDecoratedBox)
└── ^:is(atom|AddOnDecoratedBox) ← CT-1 冗余层
├── ^[SizeType=Large/Middle/Small] (×3 × 2 个子 Style)
├── ^[Status=Error/Warning] (×2 × 2 个子 Style)
├── ^[StyleVariant=Outline]
│ ├── ^[IsInnerBoxHover=True]
│ ├── ^:focus-within
│ ├── ^:pressed
│ └── ^[Status=Error]
│ ├── ^ /template/ #PART_LeftAddOn
│ ├── ^ /template/ #PART_RightAddOn
│ ├── ^[IsInnerBoxHover=True]
│ ├── ^:focus-within
│ └── ^:pressed ← 4 层深!
├── ^[StyleVariant=Filled] (同上展开,4 层深)
├── ^[StyleVariant=Borderless]
├── ^[StyleVariant=Underlined] (4 层深)
└── ^:disabled
├── ^[StyleVariant=Outline] (3 层深)
├── ^[StyleVariant=Filled] (3 层深)
└── ^[StyleVariant=Underlined]
性能代价
- Avalonia 把嵌套 Style 编译为 组合选择器(Descendant style combinator),每个叶子 Style 的选择器长度 = 根到该节点的路径。
^[StyleVariant=Outline]/^[Status=Error]/^[IsInnerBoxHover=True]/^:pressed这种 4 层选择器,在属性变化时每次都要:- 检查 StyleVariant 是否 == Outline
- 检查 Status 是否 == Error
- 检查 IsInnerBoxHover
- 检查 :pressed 伪类
- 每个属性变化(Status / IsInnerBoxHover / focus-within 等)都会触发 Avalonia 重新评估所有 82 个 Style 的适用性。
- 叠加 CT-1 的冗余前缀 = 每个 Style 多一次
:is检查。 - 叠加 A-1(
ConfigureInstanceStyles额外每实例 6 个 Style)= 每个 AddOnDecoratedBox 实例 88 个 Style 需要评估。
修复
方案 1:提取 "状态色" 到顶层命名样式(减少嵌套)
用 PseudoClass / Class 替代 Status + StyleVariant + 状态伪类 的 3-4 层组合:
// AddOnDecoratedBox.cs
protected virtual void UpdatePseudoClasses()
{
// ...
PseudoClasses.Set(":error", Status == InputControlStatus.Error);
PseudoClasses.Set(":warning", Status == InputControlStatus.Warning);
}
然后 AXAML 把 4 层嵌套拍平成 2 层:
<!-- 原本 -->
<Style Selector="^[StyleVariant=Outline]">
<Style Selector="^[Status=Error]">
<Style Selector="^:focus-within"> ... </Style>
</Style>
</Style>
<!-- 改为 -->
<Style Selector="^[StyleVariant=Outline]:error:focus-within"> ... </Style>
伪类和属性选择器在同一层用 & 式拼接 — Avalonia 编译为单节点选择器,O(1) 匹配而非 O(depth)。
方案 2:把 Status=Error/Warning 的样式抽到单独的 ResourceDictionary
实际上 Error / Warning 的样式结构完全对称,只是 token 不同 (ColorError vs ColorWarning)。可以用模板化 Style 复用。Avalonia 目前不原生支持,可通过代码生成器减少维护成本,但样式实例数无法减少。
方案 3(与 A-1 协同):把 ConfigureInstanceStyles 中的 Icon 染色合并进 AXAML
原 C# 里的 Icon FillBrush/StrokeBrush/Foreground 设置在 AXAML 里补齐:
<Style Selector="^:error /template/ ContentPresenter#PART_ContentLeftAddOn atom|Icon:not(.skip-status),
^:error /template/ ContentPresenter#PART_ContentRightAddOn atom|Icon:not(.skip-status),
^:error /template/ ContentPresenter#PART_ContentLeftAddOn atom|Icon:not(.skip-status),
^:error /template/ ContentPresenter#PART_ContentRightAddOn atom|Icon:not(.skip-status)">
<Setter Property="FillBrush" Value="{atom:SharedTokenResource ColorError}"/>
<Setter Property="StrokeBrush" Value="{atom:SharedTokenResource ColorError}"/>
<Setter Property="Foreground" Value="{atom:SharedTokenResource ColorError}"/>
</Style>
这样就能把 A-1 彻底根除(构造函数零 Style 分配)+ CT-2 扁平化。
优先级:P0。
CT-3 🔴 致命:TreeViewItem ControlTemplate 每节点都实例化 大量未使用的 TemplatedControl
文件:TreeView/Themes/TreeViewItemHeaderTheme.axaml:30-60
<atom:CheckBox Name="ToggleCheckbox"
Grid.Column="1"
IsChecked="{TemplateBinding IsChecked, Mode=TwoWay}"
IsVisible="False"> <!-- ❌ 默认隐藏,但总是实例化 -->
<atom:RadioButton.IsEnabled>
<MultiBinding Converter="{x:Static ...}"> <!-- ❌ 每个 Header 2 个 MultiBinding -->
...
</MultiBinding>
</atom:RadioButton.IsEnabled>
</atom:CheckBox>
<atom:RadioButton Name="ToggleRadio"
Grid.Column="1"
IsVisible="False"> <!-- ❌ 同样总是实例化 -->
<atom:RadioButton.IsEnabled>
<MultiBinding> ... </MultiBinding>
</atom:RadioButton.IsEnabled>
</atom:RadioButton>
文件:TreeView/Themes/NodeSwitcherButtonTheme.axaml:20-51 — 同样问题
<atom:IconPresenter Name="ExpandIconPresenter" IsVisible="False"/>
<atom:IconPresenter Name="CollapseIconPresenter" IsVisible="False"/>
<atom:IconPresenter Name="RotationIconPresenter" IsVisible="{... Converter}"/>
<atom:IconPresenter Name="LoadingIconPresenter" IsVisible="{... Converter}"/>
<atom:IconPresenter Name="LeafIconPresenter" IsVisible="False"/>
性能代价(以 1000 节点 TreeView 为例)
| 每个 TreeViewItem 实例化的 TemplatedControl | 数量 | 实际使用 |
|---|---|---|
| TreeViewItemHeader | 1 | ✅ |
| NodeSwitcherButton | 1 | ✅ |
| NodeSwitcherButton 内 IconPresenter | 5 | 通常只用 1-2 个 |
CheckBox(ToggleCheckbox) |
1 | 仅 ToggleType=CheckBox 时 |
RadioButton(ToggleRadio) |
1 | 仅 ToggleType=Radio 时 |
| CheckBox 内部模板 | 整棵子树 | 闲置 |
| RadioButton 内部模板 | 整棵子树 | 闲置 |
| Header 主 IconPresenter | 1 | ✅ |
一个没有开启 ToggleType 的 1000 节点纯选择树会多创建:
- 1000 × 1 CheckBox + 其完整模板(Border + Path + 动画 Transition)
- 1000 × 1 RadioButton + 其完整模板
- 1000 × 3 闲置 IconPresenter(NodeSwitcher 的 5 个里至少 3 个没用)
- 1000 × 2 MultiBinding(永不触发)
估算:每个 TreeViewItem 浪费约 20-30 个 Control 实例 + 2000-3000 字节内存。1000 节点 → 浪费 2-3 万 Control 实例 + 20-30 MB 内存 + 启动样式评估成本。
修复
改为按需实例化(ContentControl + DataTemplate 或运行时模板选择):
在 TreeViewItemHeader.cs 里根据 ToggleType 动态挂载一个 ContentPresenter:
<!-- 原来的 CheckBox + RadioButton 换成: -->
<ContentPresenter Name="PART_TogglePresenter" Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"/>
// TreeViewItemHeader.cs
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var presenter = e.NameScope.Find<ContentPresenter>("PART_TogglePresenter");
UpdateToggleControl(presenter);
}
private void UpdateToggleControl(ContentPresenter presenter)
{
presenter.Content = ToggleType switch
{
TreeItemToggleType.CheckBox => new CheckBox { /* binding */ },
TreeItemToggleType.Radio => new RadioButton { /* binding */ },
_ => null
};
}
对 NodeSwitcherButton:把 5 个 IconPresenter 合并成 1 个,Icon 属性根据 IconMode 动态切换来源。
收益(1000 节点树):
- 减少 ~20000 Control 实例 + 对应样式评估 + 模板展开时间
- 启动时间预估 -40~60%
- 内存 -20 MB
优先级:P0(TreeView 性能的最根本来源)。
CT-4 🟠 TreeViewItemHeaderTheme 44 个 Style + ^:not(^[NodeHoverMode=WholeLine]) 否定选择器
文件:TreeViewItemHeaderTheme.axaml:107-134
<Style Selector="^:not(^[NodeHoverMode=WholeLine])">
<Setter Property="ContentFrameBackground" Value="Transparent" />
<Style Selector="^ /template/ Border#PART_HeaderContentFrame">
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
<Style Selector="^[NodeHoverMode=Block]"> <!-- 嵌套在 :not 内 -->
...
</Style>
<Style Selector="^[IsHover=True]"> ... </Style>
<Style Selector="^[IsSelectable=True]">
<Style Selector="^[IsSelected=True]"> ... </Style>
<Style Selector="^[IsPressed=True]"> ... </Style>
</Style>
</Style>
<Style Selector="^[NodeHoverMode=WholeLine]"> <!-- 同样的结构再写一遍 -->
...
</Style>
问题
^:not(^[NodeHoverMode=WholeLine])+^[NodeHoverMode=WholeLine]两套完全对称的样式树,等价于 "if/else 模式"。Avalonia 对这种否定选择器的求值开销比等于选择器大 ~2×。- 每个
TreeViewItemHeader实例化时都要评估 44 个选择器(叠加 CT-1 的:is= 45 个评估)。1000 节点 × 45 = 4.5 万次 selector 评估。 NodeHoverMode取值只有Default / Block / WholeLine3 个,用属性选择器枚举三分支比:not+ 等于 更快:
<Style Selector="^[NodeHoverMode=Default]"> ... </Style>
<Style Selector="^[NodeHoverMode=Block]"> ... </Style>
<Style Selector="^[NodeHoverMode=WholeLine]"> ... </Style>
修复
- 把
^:not(^[NodeHoverMode=WholeLine])拆成 Default + Block 两个等于选择器。 - 把
^[IsHover=True]/^[IsPressed=True]改成标准伪类^:pointerover/^:pressed(如果 IsHover/IsPressed 就是IsPointerOver/IsPressed的镜像)。 - 同 CT-1:去掉
^:is(atom|TreeViewItemHeader)外壳。
优先级:P1。
CT-5 🟠 ControlTheme.BasedOn 链叠加
示例:LineEditTheme.axaml:8
<ControlTheme.BasedOn>
<themes:TextBoxTheme TargetType="{x:Type atom:LineEdit}" />
</ControlTheme.BasedOn>
TextBoxTheme 自己又有 13 个 Style + 它的 BasedOn(Avalonia 的 TextBox 原生 Theme)。
对一个 LineEdit 实例,样式评估路径为:
Avalonia.TextBox 原生 ControlTheme (N 个 Style)
↓ BasedOn
AtomUI.TextBox ControlTheme (+ 13 个 Style)
↓ BasedOn
AtomUI.LineEdit ControlTheme (+ 11 个 Style)
↓ 内部 Template 又实例化
AddOnDecoratedBox ControlTheme (+ 82 个 Style + 实例 6 个)
单个 LineEdit 实例涉及 ≥120 个 Style 评估,叠加所有 CT-1~4 的冗余。
修复
- BasedOn 链本身合理(继承模型),但应与 CT-1 / CT-2 优化叠加实施 — 让每一层本身更薄。
- 检查 BasedOn 是否有重复 Setter(基类设一遍、派生类又设一遍),减少无效赋值。
优先级:P2。
CT-6 🟠 TemplateBinding 爆炸 + RelativeSource AncestorType 深查找
文件:LineEditTheme.axaml:13-79
单个 LineEdit 的 ControlTemplate 里:
TemplateBinding出现 ~25 次RelativeSource AncestorType=atom:LineEdit出现 ~10 次(放在嵌套 ContentPresenter 内无法用 TemplateBinding)
每个 RelativeSource AncestorType Binding 会在 BindingTarget attach 到视觉树时向上遍历 Visual 祖先链直到找到 LineEdit。LineEdit 的 Content 深度 ≥6 层(LineEdit → AddOnDecoratedBox → DockPanel → Border → DockPanel → StackPanel → InputClearIconButton),每次查找 6+ 层。
1000 个 LineEdit(如大型表格场景)× 10 个 AncestorType 查找 = 10000 次视觉树遍历。
修复
- 能用
TemplateBinding就不用RelativeSource(尤其在直接的 Template 子节点)。 - 对深层嵌套里需要 LineEdit 属性的地方,考虑
$parent[atom|LineEdit]语法(Avalonia 11 支持),它在绑定编译期解析,比AncestorType运行时查找快。 - 或者在
OnApplyTemplate里一次性 code-behind 赋值(clearBtn.IsMotionEnabled = this.IsMotionEnabled),比 Binding 高效一个数量级。
优先级:P2。
修正后的优先级清单(P0 重排)
| 新编号 | 问题 | 涉及 | 工作量 | 收益 |
|---|---|---|---|---|
| P0-1 | CT-3:TreeViewItemHeader / NodeSwitcherButton 按需实例化 ToggleControl + 合并 IconPresenter | 2 AXAML + 2 cs | 中(1 天) | 🔥 TreeView 1000 节点 -40% 启动, -20MB 内存 |
| P0-2 | CT-1:批量删除 63 个 ^:is(atom|XXX) 冗余包裹 |
63 AXAML | 小(2-3h, 机械) | 每个样式重算 -20~30% |
| P0-3 | CT-2 + A-1:AddOnDecoratedBox 状态色改 PseudoClass 扁平化 + 移除 ConfigureInstanceStyles |
AddOnDecoratedBoxTheme.axaml + AddOnDecoratedBox.cs | 中(1 天) | 50 表单 -87% 启动, -2.5MB |
| P0-4 | T-1:TreeViewItem ITreeNode.Children 缓存 |
3 cs | 小(2h) | 递归操作 -90% |
| P0-5 | T-2:Filter 流程 O(N²) → O(N) | TreeView.Filter.cs | 中 | 1000 节点过滤 -97% |
| P1-1 | CT-4:TreeViewItemHeader :not 否定选择器展开 + IsHover→:pointerover |
1 AXAML | 小 | TreeView 样式评估 -15% |
| P1-2 | 原 P1-1~5(Transitions 缓存、Property 合并、OwnerTreeView 直赋值等) | - | - | - |
| P2-1 | CT-5:BasedOn 链 Setter 去重 | 多 AXAML | 大 | 小 |
| P2-2 | CT-6:RelativeSource AncestorType 改 $parent 或 code-behind |
LineEditTheme 等 | 中 | 10% |
关键结论修订
之前的结论:AddOnDecoratedBox 的
ConfigureInstanceStyles(A-1)是最严重瓶颈。 修订后的结论:
- TreeView 真正的瓶颈是 ControlTemplate 膨胀(CT-3),而非代码递归。1000 节点之所以卡,是因为实际产生了 ~5 万个 Control 实例(其中大半闲置),样式引擎被压垮。先修 CT-3,再修 T-1/T-2。
- AddOnDecoratedBox 的 CT-2(82 个深层嵌套 Style) 开销与 A-1 相当甚至更大。必须与 A-1 合并处理才能根治。
- CT-1 的
^:is(atom|XXX)冗余层 是一个低成本、全仓库受益的修复,应立即执行。
验证新增项
在原 §5 之外补充:
5.5 ControlTemplate 实例化计数
[Fact]
public void TreeView_1000Nodes_ControlInstanceCount_IsBounded()
{
var tree = BuildLargeTree(1000);
tree.ApplyTemplate();
// 强制渲染全部(关闭虚拟化场景下)
var totalControls = tree.GetVisualDescendants().OfType<Control>().Count();
// 修复前:~50000 / 修复后:~20000
Assert.True(totalControls < 25000, $"Expected <25000 controls, got {totalControls}");
}
5.6 样式选择器评估 profiler
运行 AtomUIGallery.Desktop + dotTrace,在 TreeView / Form 场景下录制 CPU samples,关注:
Avalonia.Styling.StyleBase.TryAttach/Avalonia.Styling.Selector.MatchAvaloniaObject.GetValue/AvaloniaProperty监听触发链- 期望:修复 CT-1/2/3 后相关方法总采样数 -50%。