技术文章

Alcinoe 组件库与 Delphi 11.1 Alexandria 兼容性

Alcinoe 是一套面向 Delphi 和 C++Builder 的开源组件库,由 Zeus64 在 GitHub 上维护。它覆盖了不少 VCL 和 FireMonkey RTL 交给第三方去补足的领域,包括 GPU 加速视频播放器、WebRTC 封装、原生 iOS 与 Android 编辑控件、双模式 JSON/BSON 解析器、带连接池的 MongoDB 客户端、ImageMagick 封装,以及一组完全绕开默认渲染管线的 FireMonkey 控件。这个库最早在 Rio 10.3.3 和 Sydney 10.4.2 上建立口碑,之后一路跟进 Embarcadero 的版本演进。到本文撰写时,它已经完全兼容 Delphi 11.1 Alexandria 和 Delphi Athens 12.3

如何把 Alcinoe 加进项目

安装流程首先取决于一个问题:你是否需要 Alcinoe 可视化控件的 design-time 支持。如果不需要,就可以完全跳过 BPL。只要把 {alcinoe_rootdir}\source 加入项目的 library search path 即可。所有非可视组件,包括解析器、数据库客户端和字符串工具,都可以直接从源码编译,不需要额外注册任何东西

如果你确实需要 design-time 支持,步骤会稍长一点。打开 Delphi IDE 里的 Component > Install Packages,定位到与你当前版本匹配的 BPL,比如 {alcinoe_rootdir}\lib\bpl\alcinoe\Win32\alexandria\Alcinoe_alexandria.bpl,完成安装后,仍然要把 {alcinoe_rootdir}\source 加入 search path。BPL 负责注册组件,而真正让编译器在构建你的项目时找到实现代码的,仍然是源码目录

Alcinoe 还附带了针对 Embarcadero RTL 源码的可选补丁。如果你想用它们,进入 {alcinoe_rootdir}\embarcadero\,选择与你版本对应的子目录,然后运行 update.bat。这个脚本要求系统的 PATH 中能找到 GIT,并默认 Embarcadero 安装在常见位置。它会先抓取原始 RTL 源码,再套用补丁。完成后,把补丁后的源码目录加入项目 search path,并确保它排在 Embarcadero 安装树中那份只读副本之前。刚开始使用 Alcinoe 时,这一步完全不是必需的,只有当你碰到这些补丁所针对的 bug 时,它才变得重要

Android 与 D8 desugaring proxy

Alcinoe 的若干组件,比如 WebRTC 和基于 ExoPlayer 的视频播放,都依赖使用 Java 8 语言特性的 Java 库。较旧 Delphi 版本随附的 Android 工具链在做 DEX 转换时使用的是 dx.bat,而它在 API 26 以下无法处理这些字节码。对应的解决办法是 desugaring,而 D8 在直接调用时会自动完成这件事。Alcinoe 在 {alcinoe_rootdir}\tools\D8Proxy\dx.bat 提供了一个代理脚本,把 Delphi 构建系统对 dx 的调用透明转发给 D8,从而让 desugaring 自动发生。你可以用这个代理替换 Android SDK build-tools 目录中的原始 dx.bat,该目录通常类似 C:\SDKs\android\build-tools\30.0.3\。Embarcadero 把这个底层问题记录为 RSP-24155;而较新的 SDK tools 已经直接修复了它,所以最好先确认当前工具链是否仍然需要这个变通方案

FireMonkey 渲染瓶颈与 Alcinoe 的解法

FireMonkey 默认的绘制周期在重滚动界面里很容易成为瓶颈。一个带圆角的 TRectangle 每次重绘大约就要 3 毫秒,因为默认实现会在每一帧都重新计算整条路径。若同屏可见 20 个类似控件,单帧开销就会累积到 60 毫秒左右,最终把实际帧率压到远低于流畅滚动所需的水平

Alcinoe 的应对方式,是为每个控件维护一份驻留在 GPU 内存里的缓冲。第一次绘制时,控件会被渲染进一个存放在 GPU 内存中的 TTexture;后续重绘不再重跑原始绘制算法,而只是直接 blit 这张纹理。对同一个圆角矩形来说,测得的耗时会从大约 3 毫秒降到 0.1 毫秒左右。除了这种缓冲策略,Alcinoe 还会用 Android 和 iOS 的原生绘图 API 取代 OpenGL 的基础形状绘制路径,从而绕开与 Form.Quality 绑定的画质与性能权衡。相关控件包括 TALRectangleTALCircle,以及一组改进过的布局容器,比如 ScrollBoxTabControl

TALJsonDocument:把 DOM 和 SAX 合在同一个类型里

TALJsonDocument 是 Alcinoe 提供的 JSON 与 BSON 解析器。它支持两种遍历模式。DOM 模式会建立完整的内存对象树,代价是内存占用与文档大小近似成正比,但好处是你可以随机访问任意节点。SAX 模式则在读取每个 token 时直接触发事件,不保留整棵树,因此非常适合从大文档里筛选少量值。对于相同内容,Delphi 世界里的 DOM 解析器,例如 DBXJSON、SuperObject 以及其他同类实现,通常会比 SAX 模式慢三到五倍,因为每个节点对象的分配和构造本身就带来了显著额外开销

这个类型遵循与 TALXMLDocument 相同的节点导航模式。一个最小可用的 DOM 读取示例如下

MyJsonDoc.LoadFromJSON(AJsonStr, False {dom mode});
MyJsonDoc.ParseOptions := [poAllowComments];

// read scalar values
ShowMessage(MyJsonDoc.ChildNodes['name'].ChildNodes['first'].Text);
ShowMessage(IntToStr(MyJsonDoc.ChildNodes['_id'].Int32));

// iterate an array
for I := 0 to MyJsonDoc.ChildNodes['contribs'].ChildNodes.Count - 1 do
  Writeln(MyJsonDoc.ChildNodes['contribs'].ChildNodes[I].Text);

如果使用 SAX 模式,就在调用 LoadFromJSON 之前,把匿名过程赋给 OnParseText,并将第二个参数设为 True。回调会收到节点路径、名称、值,以及一个用于标识 JSON 类型的 TALJSONNodeSubType,例如 string、integer、float、boolean 等。这个模式不会为节点分配堆对象,因此即便文档规模非常大,也不会因为保留整棵树而迅速吃光内存预算

TALJsonDocument 还原生支持 BSON 的读写;只需在 LoadFromFileSaveToFile 中把 BSON 标志设为 True 即可。另一个变体 TALJsonDocumentU 则会在内部使用 UnicodeString,也就是 UTF-16,而不是 AnsiString,也就是 UTF-8,适合那些整个上下文都基于 Unicode 的场景

MongoDB 客户端与连接池

Alcinoe 的 MongoDB 驱动覆盖了常见查询操作,并原生支持连接池。简单版客户端 TAlMongoDBClient 每次操作只打开并关闭一个连接。带池版本 TAlMongoDBConnectionPoolClient 会维护一组长期存活的连接,并从池中向每个调用线程分配一个连接,在调用完成后再归还。这种模型能避免多个线程在重复建连上互相阻塞,尤其适用于后台工作线程同时访问同一个数据库的场景。对于 capped collection 上的 tailable cursor,TAlMongoDBTailMonitoringThread 会持续监听新文档,并在它们到达时触发回调,这是日志流和变更通知场景里无需轮询的标准做法

其他值得了解的组件

ALVideoPlayer 会把视频渲染到 TTexture 上,而不是一个 overlay window,因此其他 FireMonkey 控件可以叠在它的上面。Android 后端使用 ExoPlayer,所以在 Android 自带 MediaPlayer 之外,还能获得对 DASH、HLS 和 SmoothStreaming 的支持。iOS 后端则使用 AVPlayer,并具备等价的 HLS 能力

TALWebRTC 封装了用于点对点音视频的 WebRTC stack。它不依赖浏览器,也不需要插件,连接的 NAT 穿透会通过底层库处理的标准 ICE、STUN 和 TURN 协商机制完成

TALStringList 放弃了 TStringList 基于 AnsiCompareText 的排序方式,改用与 locale 无关的 ordinal comparison,并配上一个在大列表上最高可快十倍的 quicksort。它的哈希变体 TALHashedStringList 会额外维护内部 hash table,从而以略高的小列表开销换取 O(1) 查找。需要注意的是,TALStringList 是一个 8 位 AnsiString 列表,而不是 Unicode 列表;如果你的服务端代码以 UTF-8 作为工作编码,而且更看重吞吐而不是本地化排序,它会非常合适

在 64 位 Windows 上,曾经让 Alcinoe 许多字符串例程获得速度优势的 FastCode 血统,主要是手写 x86 汇编,并不会自动延续到 Win64。Win64 构建会退回 Pascal 实现,因此在字符串密集型工作负载下会明显更慢。你可以通过 demo\ALStringBenchMark 项目在自己的硬件上测量这一差距,再决定是否要把某个字符串吞吐本来就是瓶颈的场景切换到 64 位构建

完整源码位于 github.com/Zeus64/alcinoe