ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PixiEditor 扩展系统 FlyUI 布局序列化协议深度解析:LayoutSerializationSpec 字节级规范与源码印证

PixiEditor 扩展系统 FlyUI 布局序列化协议深度解析:LayoutSerializationSpec 字节级规范与源码印证 桌面应用图像处理【免费下载链接】PixiEditorPixiEditor is a Universal Editor for all your 2D needs项目地址https://gitcode.com/GitHub_Trending/pi/PixiEditor点击查看免费下载FlyUI 是 PixiEditor 扩展系统中用于在宿主应用内部构建 UI 布局的抽象 API其核心特色是布局数据不直接以对象树形式跨进程/跨运行时传递而是被序列化为一组扁平字节序列byte span由宿主编译期反序列化还原为真实控件。本篇基于仓库中的官方规范文档 LayoutSerializationSpec.md逐字节拆解 FlyUI 布局数据的序列化格式并结合 SDK 端ControlDefinition.Serialize、宿主端LayoutBuilder.Deserialize、ByteMap类型映射、ElementMap控件注册以及官方扩展示例与单元测试给出可验证、可落地的完整实战说明。读完本文你将掌握 FlyUI 布局字节流的精确内存布局、属性编码规则、递归子节点结构、ID 管理与重复 ID 冲突策略并能据此自行实现兼容的序列化/反序列化器。FlyUI 与字节流布局设计动机FlyUI 是面向 PixiEditor 扩展开发者的抽象布局 API参见规范原文 FlyUI is an abstract API used to build layouts inside PixiEditor。扩展可以声明式地描述界面Layout、Row、Column、Text、Button等但最终渲染由 PixiEditor 主程序完成。这种扩展描述、宿主渲染的架构决定了二者之间必须存在一个稳定的数据交换格式扩展进程/运行时SDK 侧只负责把布局对象树序列化为字节序列PixiEditor 主程序宿主侧负责把字节序列反序列化为布局对象再进一步构建为 Avalonia 原生控件树。规范原文明确写道Layout data is passed as byte span, which is then deserialized into a layout object. This spec describes how to serialize and deserialize layout data.也就是说这份规范的核心目标只有一个精确定义布局数据的字节序列格式保证两侧实现互操作。在代码库中两侧实现分别位于序列化侧SDKControlDefinition.cs 中的Serialize/SerializeBytes方法反序列化侧宿主LayoutBuilder.cs 中的Deserialize/DeserializeInternal方法。布局字节序列的顶层结构规范给出一个递归结构布局字节序列由控件头 属性区 子节点区三部分组成子节点再以相同格式递归嵌套。规范原文的字节序列如下4 bytes - unique id of the control, 4 bytes - length of control type string, n bytes - control type string, 4 bytes - length of properties data, n bytes - properties data, - 1 byte - property type, - (if property type is string) 4 bytes - length of string) - x bytes - property value, where x is determined by property type, 4 bytes - number of children, n bytes - children data, where children get serialized recursively.逐字段解读偏移/字段长度含义编码说明unique id4 字节控件的全局唯一 ID小端序int用于宿主侧控件注册表ManagedElements的键control type string长度4 字节控件类型字符串的字节长度小端序int值为 UTF-8 字节数control type stringn 字节控件类型 IDUTF-8 编码字符串对应ElementMap中注册的类型名例如Text、Buttonproperties 数据可变属性区见下文属性区编码children数量4 字节直接子节点个数小端序intchildren数据n 字节子节点序列每个子节点按完全相同的格式递归序列化源码印证反序列化的读取顺序宿主侧LayoutBuilder.DeserializeInternalLayoutBuilder.cs严格按上述顺序依次解析int uniqueId BitConverter.ToInt32(layoutSpan[offset..(offset int32Size)]); offset int32Size; int controlTypeIdLength BitConverter.ToInt32(layoutSpan[offset..(offset int32Size)]); offset int32Size; string controlTypeId Encoding.UTF8.GetString(layoutSpan[offset..(offset controlTypeIdLength)]); offset controlTypeIdLength; int propertiesCount BitConverter.ToInt32(layoutSpan[offset..(offset int32Size)]); offset int32Size; Listobject properties DeserializeProperties(layoutSpan, propertiesCount, ref offset, ElementMap); int childrenCount BitConverter.ToInt32(layoutSpan[offset..(offset int32Size)]); offset int32Size; ListILayoutElementControl children DeserializeChildren(layoutSpan, childrenCount, ref offset, duplicatedIdTactic);需要说明的一点规范中4 bytes - length of properties data字段在参考实现中实际承载的是属性条数propertiesCount而不是属性区字节总长——每个属性自身携带类型字节与定长/带长度的值反序列化器逐条推进offset。从互操作角度实现时应以源码语义属性条数为准。源码印证序列化的写入顺序SDK 侧ControlDefinition.SerializeControlDefinition.cs与规范一一对应byte[] uniqueIdBytes BitConverter.GetBytes(UniqueId); bytes.AddRange(uniqueIdBytes); byte[] idLengthBytes BitConverter.GetBytes(ControlTypeId.Length); bytes.AddRange(idLengthBytes); byte[] idBytes Encoding.UTF8.GetBytes(ControlTypeId); bytes.AddRange(idBytes); bytes.AddRange(BitConverter.GetBytes(Properties.Count)); bytes.AddRange(SerializeProperties()); bytes.AddRange(BitConverter.GetBytes(Children.Count)); SerializeChildren(bytes);其中SerializeChildren对每个子节点递归调用child.Serialize(bytes)实现了规范要求的children get serialized recursively。属性区编码类型字节 值每个属性由1 字节属性类型 ID 值数据组成。规范原文给出- 1 byte - property type, - (if property type is string) 4 bytes - length of string) - x bytes - property value, where x is determined by property type,ByteMap类型字节 ID 映射表类型 ID 由ByteMap统一管理定义于 ByteMap.cs类型字节 IDCLR 类型值字节长度0int4 字节1float4 字节2bool1 字节3double8 字节4long8 字节5short2 字节6byte1 字节7char2 字节8string4 字节长度前缀 UTF-8 字节9byte[]特殊承载良构结构体well-known struct见下文255null0 字节属性值为空GetTypeByteId对不支持的类型会抛出Exception($Unknown unmanaged type: {type})意味着属性值类型被限定为上述非托管基元 字符串集合。字符串的编码细节字符串属性写入时先写 1 字节类型 ID8再写 4 字节 UTF-8 字节数注意是字节数而非字符数随后写入 UTF-8 编码的原始字节。SDK 侧编码见 ControlDefinition.csresult.Add(ByteMap.GetTypeByteId(property.type)); if (property.type typeof(string)) { if (property.value is string str) { int stringLengthBytes Encoding.UTF8.GetByteCount(str); result.AddRange(BitConverter.GetBytes(stringLengthBytes)); } } result.AddRange(property.value switch { int i BitConverter.GetBytes(i), float f BitConverter.GetBytes(f), bool b BitConverter.GetBytes(b), double d BitConverter.GetBytes(d), long l BitConverter.GetBytes(l), short s BitConverter.GetBytes(s), byte b new byte[] { b }, char c BitConverter.GetBytes(c), string s Encoding.UTF8.GetBytes(s), IStructProperty structProperty GetWellKnownStructBytes(structProperty), null [], _ throw new Exception($Unknown unmanaged type: {property.value.GetType()}) });反序列化侧对称处理读 1 字节类型 ID →ByteMap.GetTypeFromByteId反查类型 → 若是string先读 4 字节长度再读 UTF-8 内容其他基元则交给SpanUtility.Read按BitConverter规则读取SpanUtility.cs。255null在DeserializeProperties中被直接以null加入属性列表LayoutBuilder.cs。基元值的读取路径SpanUtilitySpanUtility.Read(Type type, Spanbyte span, ref int offset)是宿主侧读取定长基元的核心工具对int/bool/byte/float/double使用BitConverter其余类型走Marshal.PtrToStructure。所有多字节基元均按BitConverter默认的小端序解释——实现自定义序列化器时务必保持小端序否则跨端序环境将无法互操作。良构结构体属性byte[] / IStructPropertyFlyUI 允许Edges四边边距、TextStyle、Color等复合属性作为属性值传递。这类属性在ByteMap中以byte[]类型 ID9标记实际载荷采用内嵌结构体格式1 byte - 类型字节 (9) 4 bytes - 结构体类型名长度 (UTF-8 字节数) n bytes - 结构体类型名如 Edges、TextStyle 4 bytes - 结构体序列化数据长度 x bytes - 结构体序列化数据由 IStructProperty.Serialize() 产生SDK 侧编码实现在ControlDefinition.GetWellKnownStructBytesControlDefinition.csprivate static Listbyte GetWellKnownStructBytes(IStructProperty structProperty) { Listbyte bytes new Listbyte(BitConverter.GetBytes(structProperty.GetType().Name.Length)); bytes.AddRange(Encoding.UTF8.GetBytes(structProperty.GetType().Name)); byte[] structBytes structProperty.Serialize(); bytes.AddRange(BitConverter.GetBytes(structBytes.Length)); bytes.AddRange(structBytes); return bytes; }宿主侧反序列化LayoutBuilder.cs先读出结构体类型名再到ElementMap.WellKnownStructs字典中反查 CLR 类型通过Activator.CreateInstance实例化并调用IStructProperty.Deserialize(data)map.WellKnownStructs.TryGetValue(wellKnownStructName, out Type? structType); if (structType null) { throw new Exception($Struct type {wellKnownStructName} not found in map); } IStructProperty prop (IStructProperty)Activator.CreateInstance(structType); prop.Deserialize(value);IStructProperty接口IStructProperty.cs要求实现byte[] Serialize()与void Deserialize(byte[] data)两个方法由每个结构体自行定义内部字节布局。以Edges为例Edges.cs其Serialize将Left/Top/Right/Bottom四个double依次写入 32 字节缓冲区Deserialize按偏移 0/8/16/24 读回——即内部布局是四个 8 字节小端序 double。byte[] IStructProperty.Serialize() { byte[] data new byte[32]; BitConverter.GetBytes(Left).CopyTo(data, 0); BitConverter.GetBytes(Top).CopyTo(data, 8); BitConverter.GetBytes(Right).CopyTo(data, 16); BitConverter.GetBytes(Bottom).CopyTo(data, 24); return data; }由此可以总结自定义结构体属性 实现IStructProperty 在ElementMap中注册注册机制见下文。控件类型 ID 与 ElementMap 注册机制布局头部的控件类型字符串并非任意字符串而必须是ElementMap中已注册的控件类型名。ElementMapElementMap.cs维护两张映射ControlMapstring controlTypeId → Type控件类型 ID 到 CLR 类型反序列化时按 ID 实例化控件WellKnownStructsstring name → Type结构体名到实现IStructProperty的类型。宿主启动时通过AddElementsFromAssembly扫描所有PixiEditor*程序集凡实现了ILayoutElementControl的具体类自动登记进ControlMap凡实现IStructProperty的类型登记进WellKnownStructsServiceCollectionHelpers.cs 中以单例注入ElementMap。SDK 侧控件类通过[ControlTypeId(Text)]特性声明其类型 ID如 Text.csControlDefinition构造时据此填充ControlTypeId若类型缺少该特性会抛出ArgumentExceptionControlDefinition.cs。反序列化到控件实例的完整路径在LayoutBuilder.BuildLayoutElementLayoutBuilder.cs用controlId查ElementMap.ControlMap得到目标类型优先调用无参构造器否则取第一个构造函数并用默认值填充参数TryGetDefault回填UniqueId若元素实现IPropertyDeserializable调用DeserializeProperties(properties)还原属性基类 LayoutElement.cs 中默认把属性列表第 0 项作为Cursor其余交给控件自身处理若元素实现IChildHost调用DeserializeChildren(children)挂接子节点以UniqueId为键登记进ManagedElements字典。唯一 IDLayoutElementIdGenerator 与重复 ID 冲突策略每个控件的unique id由LayoutElementIdGeneratorLayoutElementIdGenerator.cs统一生成GetNextId()自增返回下一个 IDSetId(id)允许外部推进保证宿主返回的既有控件 ID 不会与新生成 ID 冲突。SDK 侧每个LayoutElement构造时即获得 IDLayoutElement.cs并同步登记到LayoutElementsStore。由于布局会因状态更新被反复重建同一个控件的 ID 可能出现重复。宿主侧通过DuplicateResolutionTactic枚举处理冲突LayoutBuilder.cs策略行为ThrowException抛DuplicateIdElementException快速失败便于开发期定位问题ReplaceRemoveChildren用新元素替换旧元素若旧元素是IChildHost先递归移除其全部子节点再替换从字节流到原生控件的完整调用链至此可以串起 FlyUI 的完整数据通路SDK 侧构建扩展在StatelessElement.BuildNative()/State.BuildElement()中声明式组装控件树每个控件调用BuildNative()产出ControlDefinitionLayoutElement.cs序列化根节点的ControlDefinition.SerializeBytes()递归输出整棵树的字节序列跨运行时传输通过Interop桥接调用Native.append_element_to_native_multi_child(...)等原生接口传递字节Interop.Ui.cs宿主反序列化LayoutBuilder.Deserialize(span, tactic)按本规范递归还原布局对象登记ManagedElements原生控件构建每个元素实现CreateNativeControl()例如Layout生成 AvaloniaPanel并将子元素BuildNative()的结果逐层加入Layout.cs事件订阅SDK 侧AddEvent会把事件名写入QueuedEvents宿主侧Native.subscribe_to_event(uniqueId, eventName)按控件 ID 绑定事件回调。需要注意FlyUI 存在两套同名 APISDK 面向扩展的PixiEditor.Extensions.Sdk.Api.FlyUI与宿主内部面向 Avalonia 的PixiEditor.Extensions.FlyUI.Elements二者通过ILayoutElementT泛型接口解耦SDK 侧T ControlDefinition宿主侧T Avalonia.Controls.Control字节格式正是连接两者的通用语言。实战验证官方示例与单元测试官方示例Sample7_FlyUIsamples/Sample7_FlyUI 演示了扩展如何用 FlyUI 构建一个弹出窗口public override ControlDefinition BuildNative() { Layout layout new Layout(body: new Container(margin: Edges.All(25), child: new Column( crossAxisAlignment: CrossAxisAlignment.Center, mainAxisAlignment: MainAxisAlignment.SpaceEvenly, children: [ new Center(new Text(..., wrap: TextWrap.Wrap, textStyle: new TextStyle(fontSize: 16))), new Align(alignment: Alignment.CenterRight, child: new Text(...)), new Container(margin: Edges.Symmetric(25, 0), backgroundColor: Color.FromRgba(25, 25, 25, 255), child: new Column(children: [ new Image(/Pizza.png, filterQuality: FilterQuality.None, width: 256, height: 256) ])), new CheckBox(new Text(heloo), onCheckedChanged: args { ... }), new SizeInputField(), new Center(new Button(child: new Text(Close), onClick: _ { Window.Close(); })) ] ) ); return layout.BuildNative(); }这段代码覆盖了本规范讨论的几乎所有序列化要素字符串属性Text的文本、枚举属性TextWrap、FilterQuality、Alignment、良构结构体属性Edges.All(25)、Edges.Symmetric(25, 0)、TextStyle、Color以及多子节点递归结构Column的 children 列表。入口见 FlyUISampleExtension.cs宿主回调通过WindowContentElement挂载。单元测试LayoutBuilder 系列测试tests/PixiEditor.Extensions.Tests/LayoutBuilderTests.cs 验证了事件回传与状态更新链路TestThatButtonClickEventFiresCallback直接对元素RaiseEvent能触发回调TestThatAvaloniaClickEventFiresElementCallback原生 Avalonia 点击事件能映射回 FlyUI 元素回调TestStateChangesDataAndOnlyAppliesDiffPropertiesSetState后只做差异更新且原生控件实例保持不变Assert.Equal(button, contentPresenter.Content)TestStateAddsChildToTree/TestStateRemovesChildFromTree/TestStateReplacesChildInTree验证子节点增删替换在既有原生树上的正确应用。tests/PixiEditor.Extensions.Tests/LayoutBuilderElementsTests.cs 验证了Row/Column/Center布局树构建结果Layout.BuildNative()产出Panel内部依次为对应布局面板与TextBlock子节点且对齐方式Stretch、Center符合预期。实现自定义 FlyUI 序列化器/反序列化器的要点清单基于规范与源码若要在 FlyUI 之外实现一套兼容的字节流处理器需要严格遵守以下约定端序所有多字节整数、浮点数均采用BitConverter默认的小端序控件头4B uniqueId 4B 类型名长度(字节) UTF-8 类型名属性区前置4B 属性条数每条属性 1B 类型字节 值类型字节严格使用ByteMap的 0–9 与 255 映射字符串以 UTF-8 字节数非字符数为长度前缀结构体属性类型字节固定为9载荷为4B 名称长度 UTF-8 名称 4B 数据长度 原始数据结构体内部布局由各IStructProperty自定如Edges为 4 个double小端序平铺子节点4B 子节点数 每个子节点递归使用同一格式控件类型注册类型名必须能在ElementMap.ControlMap中解析到对应 CLR 类型ID 语义uniqueId在宿主ManagedElements中必须唯一冲突时按DuplicateResolutionTactic抛异常或替换并递归移除旧子节点处理。总结LayoutSerializationSpec.md用一段简洁的字节序列图定义了 PixiEditor FlyUI 的跨运行时布局交换格式而仓库源码将其落实为SDK 序列化 — 字节传输 — 宿主反序列化 — 原生控件构建的完整流水线。理解这一格式不仅有助于排查扩展 UI 的序列化问题也为在 FlyUI 之上扩展自定义控件类型、自定义结构体属性乃至实现第三方兼容布局器提供了精确的协议蓝本。赞分享桌面应用图像处理【免费下载链接】PixiEditorPixiEditor is a Universal Editor for all your 2D needs项目地址https://gitcode.com/GitHub_Trending/pi/PixiEditor点击查看免费下载相关推荐NHP 消息头协议规范深度解析OpenNHP 240/304 字节固定头部的字节布局、混淆机制与源码实现NHP 消息头协议规范深度解析OpenNHP 240/304 字节固定头部的字节布局、混淆机制与源码实现 导读 本文围绕 OpenNHP 网络隐身协议的 固定网络安全零信任密码学身份认证网络Advanced Java 分布式系统专题Dubbo 序列化协议深度解析Advanced Java 分布式系统专题Dubbo 序列化协议深度解析 引言序列化协议在分布式系统中的核心地位 在分布式系统架构中序列化Seriali文档教程知识库后端Capn Proto 编码规范深度解析从 64 位字指针布局到流式序列化与安全防护Capn Proto 编码规范深度解析从 64 位字指针布局到流式序列化与安全防护 导读 本文是 Capn Proto 序列化格式的完整编码规范指南基于后端通信上一篇Maestro移动端性能测试终极指南响应时间优化与资源占用监控下一篇Heya测试与预览确保邮件序列质量的完整工作流程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表