PAYATHON 2026

如何在多个 dart packages 之间共享 dynamic lib code_asset?

技术老齐

结论

由一个包提供 dyLib,其他包引用它的 asset ID,避免多个 build hooks 重复打包同一个动态库。

如果 B 依赖 A,可以由 A 提供 dyLib。如果多个包都要使用这个库,更适合将它抽到独立的 dylib_native 包中,统一构建或下载,并声明 code asset。

如果想在添加前先检查是否重复,支持 ToBuildHooks 的 hooks API 可以接收直接依赖显式传来的 assets。不过,这些 assets 不包含整个应用的资源,无法用于任意包之间的全局去重。BuildInput API

为什么不同的 asset ID 仍会冲突?

需要区分两个名称:

  • asset ID,例如 package:A/dyLib,用于标识 Dart 要访问的 code asset。
  • 动态库文件名,例如 libdyLib.so,用于最终的文件打包和加载。

package:A/dyLib 和 package:B/dyLib 是不同的 asset ID,但两个 hooks 都要求打包名为 dyLib 的动态库,因此仍会触发题目中的文件名冲突。包名提供的命名空间无法消除动态库文件名冲突。

只修改 CodeAsset.name 通常无法解决这个问题。即使两个声明指向内容相同的文件,也不能指望它们自动去重。

解决步骤:由一个包提供动态库

建议采用以下依赖关系,其中 a、b 对应问题中的 A、B:

a ──→ dylib_native
b ──→ dylib_native
b ──→ a

按以下步骤调整:

  1. 将 dyLib 的构建、下载或预编译文件选择逻辑移到 dylib_native/hook/build.dart。
  2. 从 A 和 B 的 hooks 中删除对 dyLib 的重复声明,保留各自其他库的处理逻辑。
  3. 将需要访问 dyLib 的 FFI 绑定指向 package:dylib_native/dyLib,或者直接复用 dylib_native 提供的 Dart 绑定。

Build hook 只能输出属于自身包的 asset ID,因此应由 dylib_native 创建这个资产,A、B 引用它。CodeAsset.id API

提供包的 build hook

在 dylib_native 中添加依赖:

dart pub add hooks code_assets

这两个包应放在 dependencies 中,因为使用者构建应用时也需要运行 hook。Dart hooks 文档

下面以 Linux x64 的预编译库为例,假设包内已有 native/linux_x64/libdyLib.so。其他平台需要补充相应的文件选择或构建逻辑。

// dylib_native/hook/build.dart
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';

Future<void> main(List<String> args) async {
  await build(args, (input, output) async {
    if (!input.config.buildCodeAssets) {
      return;
    }

    final config = input.config.code;
    if (config.targetOS != OS.linux ||
        config.targetArchitecture != Architecture.x64) {
      throw UnsupportedError(
        'This example only provides a Linux x64 library.',
      );
    }

    output.assets.code.add(
      CodeAsset(
        package: input.packageName,
        name: 'dyLib',
        linkMode: DynamicLoadingBundled(),
        file: input.packageRoot.resolve(
          'native/linux_x64/libdyLib.so',
        ),
      ),
    );
  });
}

这里创建的 asset ID 是 package:dylib_native/dyLib。DynamicLoadingBundled() 表示该动态库需要随应用打包,file 指向实际的库文件。code_assets 使用说明

使用包中的 FFI 绑定

假设 dyLib 导出以下 C 函数:

int32_t dylib_add(int32_t a, int32_t b);

A 或 B 可以显式指定同一个 asset ID:

import 'dart:ffi';

@Native<Int32 Function(Int32, Int32)>(
  assetId: 'package:dylib_native/dyLib',
  symbol: 'dylib_add',
)
external int dylibAdd(int a, int b);

assetId 决定从哪个 code asset 解析符号。省略它时,Dart 会使用当前库的 DefaultAsset 配置或库 URI。将资产移到提供包后,也需要调整绑定,避免继续引用原来的 A、B 资产。Native.assetId API

如果 A、B 使用同一组绑定,可以把这些绑定也放进 dylib_native,供两者导入。

多组第三方库难以确定交集时怎么办?

可以按第三方库划分提供包,明确每个库由哪个包负责打包。例如:

a → x_native、y_native
b → a、y_native、z_native

y_native 负责提供共享的 Y 库,A 和 B 都不再自行打包它。各包只需声明自己依赖哪些提供包,无须在每次构建时比较 A、B 的完整库列表。

如果现有项目必须由 B 的 hook 动态判断 A 提供了哪些库,可以让 A 将相应 assets 额外发送给下游 hook:

// 放在 A 的 build 回调中;asset 是 A 已创建的 CodeAsset。
output.assets.code.add(asset);
output.assets.code.add(asset, routing: const ToBuildHooks());

第一次添加使用默认的 ToAppBundle(),负责将资产打包到应用中;第二次使用 ToBuildHooks(),将资产信息传给直接依赖 A 的包。两次添加分别用于不同的路由。BuildOutputCodeAssetBuilder.add API

B 可以通过 input.assets 检查收到的资产,但有以下限制:

  • A 必须显式发送;普通打包输出不会自动成为 B 的输入。
  • 接收范围限于直接依赖,不包含整个依赖图中的所有资产。
  • B 收到资产后,不应再次将它输出到应用包,否则仍可能重复打包。

ToBuildHooks 文档也建议由发送方负责打包,避免多个接收方重复输出同一资产。具体接口以项目所用版本为准。ToBuildHooks API

共享前需要确认的条件

同名文件不一定能共用。共享 dyLib 的前提是 A、B 所需的版本、ABI、目标架构和构建选项兼容。如果两者需要的是不兼容的版本,就需要解决版本约束,或设计能让两个版本并存的原生库构建与加载方案。

如果 A、B 自己的原生库还依赖 dyLib,也要处理原生链接器和运行时加载器的依赖路径。声明 CodeAsset 后,这些原生依赖关系仍需配置。

备注:内容仅供参考。