Xamarin.Forms切换ControlTemplate原生控件未正确释放问题
这个异常是Xamarin.Forms模板切换逻辑的已知特性叠加第三方Accordion控件的渲染器缓存机制导致的:在同一页面切换ControlTemplate时,旧模板生成的可视化子树不会被立刻销毁,部分第三方自定义控件的渲染器会按控件类型做全局状态缓存,不会区分托管层的不同实例,首次加载时计算出的Accordion Item高度值被缓存后,会被新实例的同类型控件命中,最终出现跨实例的外观串改。你观察到的「两次BLA不是同一实例」是托管层的表现,对应平台的原生控件层实际命中了同一份缓存状态。
切换模板前主动清理旧模板可视化树
不要等GC自动回收资源,在触发ControlTemplate切换的逻辑前,主动清空旧模板的根节点内容,强制触发卸载流程,代码示例:// 切换模板前置操作 var oldTemplateRoot = this.GetTemplateRoot(); if (oldTemplateRoot is Layout oldLayout) { oldLayout.Children.Clear(); } oldTemplateRoot?.OnParentSet(); // 执行模板切换 this.ControlTemplate = targetTemplate;给Accordion控件设置实例唯一标识,破坏缓存命中
绝大多数出现状态串流的第三方Accordion控件,都是用控件类型作为渲染器缓存Key,没有区分实例。只需要在BLA的构造函数中给Accordion赋值运行时生成的唯一标识,就能强制渲染器为每个BLA实例创建独立的原生控件,不复用旧状态:public BLA() { InitializeComponent(); // 每个BLA实例的Accordion都有独立ID innerAccordion.StyleId = $"acc_{Guid.NewGuid():N}"; }重写BLA的卸载生命周期,主动释放原生渲染器
给BLA重写OnParentSet方法,当检测到控件从可视化树移除(父级为null)时,主动重置Accordion的布局状态,Dispose对应的原生渲染器,避免残留:protected override void OnParentSet() { base.OnParentSet(); if (Parent == null) { // 重置Accordion所有Item的高度为自动计算 foreach (AccordionItem item in innerAccordion.Items) { item.HeightRequest = -1; } innerAccordion.ItemsSource = null; // 主动释放原生渲染器 var renderer = Platform.GetRenderer(innerAccordion); renderer?.Dispose(); Platform.SetRenderer(innerAccordion, null); } }关闭全局渲染器缓存(仅针对Xamarin.Forms 5.0以下版本)
如果你使用的是5.0之前的Xamarin.Forms版本,可以在App初始化时加全局开关,关闭默认的渲染器缓存逻辑:// App构造函数中,Forms.Init调用后添加 Forms.SetFlags("DisableRenderersCaching");
注意:不要依赖GC自动回收自定义渲染器资源,只要控件存在未注销的事件订阅、静态缓存引用,GC就不会判定对象为可回收状态,必须手动触发Dispose流程才能彻底释放原生控件。
内容的提问来源于stack exchange,提问作者o_w

