Flutter Mobile Development · 课时

后台隔离区与原生内存管理

在主隔离区之外运行 FFI 调用,并管理原生内存分配以避免泄漏。

第 4 / 4 课13 个步骤

后台隔离区与原生内存管理 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why FFI Belongs Off the Main Isolate

Dart's FFI lets you call native C functions directly, but those calls run synchronously on whatever isolate invokes them. If a native function is slow (image decoding, crypto, parsing a large buffer), running it on the main isolate blocks the UI thread and drops frames.

  • The Flutter UI runs on the root (main) isolate; jank appears when it stalls more than ~16ms.
  • Native code has no awareness of Dart's event loop. A 200ms C call freezes the app.
  • The fix: run heavy FFI work on a background isolate so the main isolate stays free to paint.

This lesson covers spawning isolates, sharing native pointers safely, and freeing native memory without leaks.

Isolates vs Threads: The Memory Model

A Dart isolate is an independent worker with its own memory heap and event loop. Unlike OS threads, isolates do not share Dart objects — they communicate by passing messages over ports.

  • Each isolate has its own GC heap; you cannot pass a normal Dart object by reference between them.
  • But native memory is different: a Pointer<T> from dart:ffi is just an integer address into the process's shared native heap.
  • That address is valid in any isolate of the same process — so FFI pointers can be shared across isolates, unlike Dart objects.

This distinction is the key to coordinating FFI work between isolates: you pass the address, not the object.

The Easy Win: Isolate.run

For a one-shot heavy computation, Isolate.run (Dart 2.19+) spawns a short-lived isolate, runs your closure, returns the result, and tears the isolate down automatically. It is the simplest way to move work off the main isolate.

  • The closure must be a top-level or static function reference, or a closure that only captures sendable values.
  • The return value is copied back to the calling isolate.
  • Perfect for a single FFI-heavy operation like hashing a file.
import 'dart:isolate';

int slowFib(int n) {
  if (n < 2) return n;
  return slowFib(n - 1) + slowFib(n - 2);
}

Future<void> main() async {
  // Runs slowFib on a fresh background isolate; UI isolate stays free.
  final result = await Isolate.run(() => slowFib(38));
  print('fib(38) = ' + result.toString());
}

Allocating Native Memory with malloc

The package:ffi library exposes malloc (and calloc) allocators that wrap the C heap. You allocate a typed pointer, write to it, hand it to native code, and you are responsible for freeing it.

  • malloc<Uint8>(count) returns a Pointer<Uint8> to count bytes.
  • The Dart GC does not manage this memory — forgetting malloc.free leaks it permanently.
  • Wrap usage in try/finally so the free runs even if native code throws.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

void main() {
  final buffer = malloc<Uint8>(4);
  try {
    final bytes = buffer.asTypedList(4);
    bytes.setAll(0, [10, 20, 30, 40]);
    print('first byte = ' + buffer.value.toString());
    print('sum = ' + bytes.reduce((a, b) => a + b).toString());
  } finally {
    malloc.free(buffer); // never skip this
  }
}

Passing a Pointer Across an Isolate Boundary

Because a native pointer is an integer address, you can send its address field to a background isolate, then reconstruct the typed pointer there with Pointer.fromAddress.

  • Send ptr.address (a plain int) — it is sendable over a SendPort.
  • On the receiving isolate, rebuild it: Pointer<Uint8>.fromAddress(addr).
  • Ownership rule: decide which isolate frees the memory. A common pattern is the owner allocates, the worker only reads/writes, and the owner frees after the worker signals completion.
import 'dart:ffi';
import 'dart:isolate';
import 'package:ffi/ffi.dart';

int sumOnWorker(int address, int length) {
  final ptr = Pointer<Uint8>.fromAddress(address);
  final list = ptr.asTypedList(length);
  return list.fold<int>(0, (a, b) => a + b);
}

Future<void> main() async {
  final buf = malloc<Uint8>(5);
  try {
    buf.asTypedList(5).setAll(0, [1, 2, 3, 4, 5]);
    // Owner (main) keeps ownership; worker only reads via the address.
    final total = await Isolate.run(() => sumOnWorker(buf.address, 5));
    print('sum from worker = ' + total.toString());
  } finally {
    malloc.free(buf); // owner frees after worker is done
  }
}

Loading the Native Library Per Isolate

A DynamicLibrary handle and the function pointers you look up from it are not sendable across isolates. Each isolate that calls native code must open its own DynamicLibrary and resolve its own symbols.

  • The underlying .so/.dylib is loaded once into the process; DynamicLibrary.open in another isolate just gets a fresh handle to the same loaded image.
  • Cache the looked-up function in an isolate-local variable; do not try to pass a Dart Function obtained from lookupFunction to another isolate.
  • On Android the library name is like libnative.so; on iOS/macOS prefer DynamicLibrary.process() for statically linked symbols.
import 'dart:ffi';
import 'dart:io' show Platform;

typedef NativeSum = Int32 Function(Int32 a, Int32 b);
typedef DartSum = int Function(int a, int b);

DynamicLibrary openLib() {
  if (Platform.isAndroid) return DynamicLibrary.open('libnative.so');
  if (Platform.isIOS || Platform.isMacOS) return DynamicLibrary.process();
  return DynamicLibrary.open('libnative.so');
}

// Call this INSIDE each isolate that needs native access.
DartSum resolveSum() {
  final lib = openLib();
  return lib.lookupFunction<NativeSum, DartSum>('native_sum');
}

A Long-Lived Worker Isolate with Ports

For repeated FFI calls, spawning a new isolate each time is wasteful. Instead, spawn one persistent worker with Isolate.spawn and stream requests/responses over ReceivePort/SendPort.

  • The worker opens its native library once on startup and reuses the resolved functions.
  • The main isolate sends a request record (e.g. address + length + a reply port); the worker does the FFI call and replies.
  • This amortizes library-open cost and keeps the main isolate responsive.
import 'dart:isolate';

Future<void> main() async {
  final ready = ReceivePort();
  await Isolate.spawn(_worker, ready.sendPort);
  final SendPort toWorker = await ready.first as SendPort;

  final reply = ReceivePort();
  toWorker.send([21, reply.sendPort]);
  final result = await reply.first;
  print('worker doubled => ' + result.toString());
}

void _worker(SendPort initial) {
  final port = ReceivePort();
  initial.send(port.sendPort); // hand back our inbound port
  port.listen((msg) {
    final value = msg[0] as int;
    final SendPort reply = msg[1] as SendPort;
    reply.send(value * 2); // stand-in for a real FFI call
  });
}

Calling Back into Dart from Native Threads

Sometimes native code (running on its own C thread) needs to notify Dart. You cannot call a Dart function directly from an arbitrary native thread — instead use NativeCallable.listener, which marshals the call onto the isolate that created it via its event loop.

  • NativeCallable.isolateLocal — synchronous, only callable from the same isolate's thread.
  • NativeCallable.listener — asynchronous and thread-safe; safe to invoke from any native thread, the callback is queued to the owning isolate.
  • Call close() on the NativeCallable when done, or it keeps the isolate alive and leaks the trampoline.
import 'dart:ffi';

// C signature: void (*)(Int32) the native side stores and calls later.
typedef ProgressNative = Void Function(Int32);

void onProgress(int percent) {
  print('native reported ' + percent.toString() + '%');
}

void setupCallback() {
  final callback = NativeCallable<ProgressNative>.listener(onProgress);
  // Pass callback.nativeFunction to your C registration function.
  // When finished, release it:
  // callback.close();
  print('callback ptr = ' + callback.nativeFunction.address.toString());
}

Auto-Freeing with NativeFinalizer

Manual try/finally works for scoped allocations, but for native resources tied to the lifetime of a Dart object, use a NativeFinalizer. It runs a native free function when the Dart object becomes unreachable and is garbage-collected.

  • Attach the Dart wrapper to the finalizer with a token pointer to free.
  • The finalizer is best-effort — it is not guaranteed to run promptly (or at all on abrupt exit), so it is a safety net, not a substitute for explicit cleanup.
  • Provide an explicit dispose() and detach the finalizer there to avoid a double free.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

final NativeFinalizer _finalizer = NativeFinalizer(malloc.nativeFree);

class NativeBuffer implements Finalizable {
  final Pointer<Uint8> ptr;
  bool _disposed = false;

  NativeBuffer(int size) : ptr = malloc<Uint8>(size) {
    _finalizer.attach(this, ptr.cast(), detach: this);
  }

  void dispose() {
    if (_disposed) return;
    _disposed = true;
    _finalizer.detach(this); // prevent double free
    malloc.free(ptr);
  }
}

Ownership Discipline Across Isolates

The most common native leak in multi-isolate FFI code is unclear ownership: two isolates each think the other will free a buffer, or both free it (a crash). Establish a contract.

  • Single owner: exactly one isolate allocates and frees. Others receive a borrowed address and must not free.
  • Hand-off: if ownership transfers, the sender must not touch or free the pointer after sending the address.
  • Lifetime: the owner must keep the memory alive until it has confirmation (a reply message) that all borrowers are done — never free while a worker may still read it.

Document the contract in code comments; race conditions here surface as use-after-free crashes that are hard to reproduce.

Detecting and Diagnosing Native Leaks

Native heap leaks do not show up in Dart's memory view because the Dart GC never tracked them. You need native tooling and disciplined patterns.

  • Use Android Studio's native memory profiler or Xcode Instruments (Allocations / Leaks) to watch the C heap grow.
  • In tests, wrap allocations and assert that every malloc is matched by a free — a counting allocator wrapper helps.
  • Run a tight loop of your FFI operation; if RSS climbs without leveling off, you have a leak.
  • Prefer calloc when you need zeroed memory so stale bytes don't mask bugs.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

int _live = 0;

Pointer<Uint8> track(int n) {
  _live++;
  return malloc<Uint8>(n);
}

void release(Pointer<Uint8> p) {
  _live--;
  malloc.free(p);
}

void main() {
  for (var i = 0; i < 1000; i++) {
    final p = track(64);
    release(p);
  }
  // Must be zero; non-zero means a leak path skipped release().
  print('outstanding allocations = ' + _live.toString());
}

Quick Check: Sharing FFI Memory Between Isolates

You allocate a Pointer<Uint8> on the main isolate and need a background isolate to read it via FFI. What is the correct, safe way to share it?

Recap: Safe Background FFI

You now have a complete mental model for running FFI off the main isolate without leaking native memory:

  • Move heavy native work off the UI isolate with Isolate.run for one-shots or a persistent Isolate.spawn worker for repeated calls.
  • Share native memory by address — send ptr.address, rebuild with Pointer.fromAddress; never send Pointer or DynamicLibrary objects.
  • Open the native library per isolate; each worker resolves its own function pointers once on startup.
  • Free every allocation: try/finally for scoped use, NativeFinalizer as a GC-time safety net, and explicit dispose() with detach.
  • Enforce single-owner ownership across isolates and keep memory alive until borrowers confirm completion.
  • Use NativeCallable.listener for thread-safe callbacks from native threads, and close() them when done.
  • Profile with native tools (Instruments / Android native profiler) since Dart's GC view cannot see C-heap leaks.
免费开始

用 AI 导师学习 Dart — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
22
课程
88

常见问题解答

「后台隔离区与原生内存管理」课时是免费的吗?

是的 — 「后台隔离区与原生内存管理」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。

「后台隔离区与原生内存管理」这节课中我会学到什么?

在主隔离区之外运行 FFI 调用,并管理原生内存分配以避免泄漏。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Flutter Mobile Development 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「后台隔离区与原生内存管理」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Flutter Mobile Development 课中编写并运行代码吗?

能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 dart:ffi 调用 C 库
  2. 类型安全的平台通道与 Pigeon
  3. 为 iOS 和 Android 编写自定义平台插件
  4. 后台隔离区与原生内存管理
← 返回 Flutter Mobile Development