Dart FFI 内存管理:用 using + Arena 替代嵌套 try-finally

在使用 Dart FFI 调用 Windows API 时,经常需要手动申请 Native 内存。

例如调用 API 获取物理显示器信息,需要先分配一块内存保存返回值:

ini 复制代码
final countPointer = calloc<ffi.Uint32>();

使用完成后,再手动释放:

arduino 复制代码
try {
  // 调用 Native API
} finally {
  calloc.free(countPointer);
}

当一个方法需要申请多块 Native 内存时,代码很快会变成多个嵌套的 try-finally。

业务代码夹在内存申请和释放之间,代码变长,资源生命周期也不容易看清。

对于这种临时 Native 内存,可以使用 package:ffi 提供的 using 和 Arena。

使用 using + Arena 管理内存

Arena 可以理解成一个 Native 内存作用域。

在 using 中通过 arena 分配的内存,会在作用域结束时统一释放。

例如:

kotlin 复制代码
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';

final result = using((Arena arena) {
  final pointer = arena<ffi.Uint32>();

  // 使用 pointer
  return pointer.value;
});

不需要再为 pointer 编写:

arduino 复制代码
try {
  // ...
} finally {
  calloc.free(pointer);
}

当 using 作用域结束后,Arena 会处理在这个作用域中分配的内存。

多块 Native 内存一起管理

实际使用 FFI 时,一个 API 调用可能需要多块内存。

例如获取物理显示器信息:

arduino 复制代码
final countPointer = calloc<ffi.Uint32>();

try {
  // 获取数量

  final physicalArray = calloc<PhysicalMonitor>(count);

  try {
    // 获取物理显示器

    // 使用 physicalArray
  } finally {
    calloc.free(physicalArray);
  }
} finally {
  calloc.free(countPointer);
}

这里的问题不是释放本身,而是资源管理代码开始占据业务代码的位置。

使用 Arena 后,可以把它改成:

ini 复制代码
return using((Arena arena) {
  final countPointer = arena<ffi.Uint32>();

  final countResult = getNumberOfPhysicalMonitors(
    hMonitor,
    countPointer,
  );

  if (countResult == 0) {
    throw StateError(
      '获取物理监视器数量失败,Win32 错误 ${getLastError()}.',
    );
  }

  final count = countPointer.value;

  if (count == 0) {
    throw StateError(
      '监视器句柄 $hMonitor 没有关联的物理监视器.',
    );
  }

  final physicalArray =
      arena<PhysicalMonitor>(count);

  final getResult = getPhysicalMonitorsFromHMonitor(
    hMonitor,
    count,
    physicalArray,
  );

  if (getResult == 0) {
    throw StateError(
      '获取物理监视器信息失败,Win32 错误 ${getLastError()}.',
    );
  }

  final handles = <int>[];

  for (int index = 0; index < count; index++) {
    final monitor = (physicalArray + index).ref;
    handles.add(monitor.hPhysicalMonitor);
  }

  return handles;
});

这里申请了两块 Native 内存:

ini 复制代码
final countPointer = arena<ffi.Uint32>();

final physicalArray =
    arena<PhysicalMonitor>(count);

但不需要分别释放。

离开 using 作用域后,Arena 会统一处理这些内存。

using 的价值是什么?

原来的代码需要关注:

csharp 复制代码
申请内存
    ↓
try
    ↓
调用 API
    ↓
finally
    ↓
释放内存

申请多块内存后,还需要考虑多个 try-finally 的嵌套关系。

使用 Arena 后,代码变成:

arduino 复制代码
进入 using
    ↓
创建 Arena
    ↓
分配 Native 内存
    ↓
调用 API
    ↓
读取结果
    ↓
离开 using
    ↓
统一释放

资源生命周期从多个 try-finally 收缩成一个作用域。

这也是 Arena 更适合临时 FFI 内存的原因。

Arena 适合临时内存

并不是所有 Native 资源都应该交给 Arena。

例如:

scss 复制代码
using((Arena arena) {
  final pointer = arena<SomeStruct>();

  // 调用 API
});

如果这个指针只在当前方法中使用,使用 Arena 很合适。

但如果需要把 Native 指针保存到一个 Dart 对象中,让它跨越当前方法继续存在,就不能简单地放进当前 Arena。

这时需要重新设计资源的生命周期。

NativeFinalizer 是另一种思路

如果需要把 Native 资源封装成 Dart 对象,可以考虑 NativeFinalizer。

例如:

markdown 复制代码
Dart 对象
    ↓
持有 Native 指针 / 句柄
    ↓
对象生命周期结束
    ↓
NativeFinalizer
    ↓
调用 Native 释放函数

这种方式适合生命周期更长的 Native 资源。

因此,两种方案解决的问题不同:

方式 适合场景
using + Arena 方法内部的临时 Native 内存
NativeFinalizer 与 Dart 对象生命周期绑定的 Native 资源

对于一次 FFI 调用中临时申请的指针,没有必要为了它引入更复杂的对象生命周期管理。

还要注意 Native 句柄和 Native 内存不是一回事

使用 Arena 只能解决 Arena 管理的 Native 内存。

如果 Windows API 返回的是需要显式销毁的系统句柄,仍然需要调用对应的 Windows API。

例如某些 API 返回的资源需要:

markdown 复制代码
Native 内存
    ↓
arena 自动释放

Windows 句柄
    ↓
调用对应 Destroy / Close API

不能因为使用了 Arena,就认为所有 Native 资源都会自动释放。

因此,在使用 FFI 时,需要区分:

  • Dart 对象
  • Native 堆内存
  • Native 指针
  • Windows 系统句柄
  • API 特定的资源释放函数

它们的生命周期并不相同。

相关推荐
白山编程大哥2 小时前
Java 后端如何优雅地把 List 集合返回给前端
java·前端·list
Flynt2 小时前
我把热榜那篇"禁用 axios"逐条实测了一遍:结论大体对,坑在它没讲的地方
前端·javascript·axios
Csvn3 小时前
bundle优化
前端
北冥有鱼4 小时前
发版后动态路由点击报 `Failed to fetch dynamically imported module`」问题上的完整排查与修复过程
前端·vue.js
静默回滚4 小时前
视频压到 25 MB 以内,码率要算、音轨要扣、余量要留
前端
用户37203749260184 小时前
把 10MB 中文语义模型塞进浏览器:STSB 做到教师模型的 93%,但线上 top1 只有 42%
前端
光锥智能4 小时前
海康机器人举行2026移动机器人新品发布会:箱式存拣升级,「明智」系列发布
java·前端·机器人
计算机魔术师4 小时前
Anthropic 上市文件最吓人的一幕,不是亏损 807 亿
前端
alsmile4 小时前
AI友好的可视化图形引擎自我革命:Meta2d.js 2.0 历时一年全新重写,性能与多人协作全面升级
前端·开源·数据可视化
行思理4 小时前
Nginx 跨站请求(CORS)配置完全指南:从原理到生产实践
java·前端·nginx