Isolados em Segundo Plano e Gerenciamento de Memória Nativa
Execute chamadas FFI fora do isolado principal e gerencie alocações nativas para evitar vazamentos.
Isolados em Segundo Plano e Gerenciamento de Memória Nativa é uma aula grátis de Flutter Mobile Development no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Flutter Mobile Development, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Flutter Mobile Development inclui 4 aulas no total.
Partes desta aula ainda não foram traduzidas e aparecem em inglês.
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>fromdart:ffiis 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 aPointer<Uint8>tocountbytes.- The Dart GC does not manage this memory — forgetting
malloc.freeleaks it permanently. - Wrap usage in
try/finallyso 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 plainint) — it is sendable over aSendPort. - 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/.dylibis loaded once into the process;DynamicLibrary.openin 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
Functionobtained fromlookupFunctionto another isolate. - On Android the library name is like
libnative.so; on iOS/macOS preferDynamicLibrary.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 theNativeCallablewhen 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
tokenpointer 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()anddetachthe 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
mallocis matched by afree— a counting allocator wrapper helps. - Run a tight loop of your FFI operation; if RSS climbs without leveling off, you have a leak.
- Prefer
callocwhen 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.runfor one-shots or a persistentIsolate.spawnworker for repeated calls. - Share native memory by address — send
ptr.address, rebuild withPointer.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/finallyfor scoped use,NativeFinalizeras a GC-time safety net, and explicitdispose()withdetach. - Enforce single-owner ownership across isolates and keep memory alive until borrowers confirm completion.
- Use
NativeCallable.listenerfor thread-safe callbacks from native threads, andclose()them when done. - Profile with native tools (Instruments / Android native profiler) since Dart's GC view cannot see C-heap leaks.
Perguntas Frequentes
A aula “Isolados em Segundo Plano e Gerenciamento de Memória Nativa” é grátis?
Sim — o texto completo de “Isolados em Segundo Plano e Gerenciamento de Memória Nativa” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Flutter Mobile Development, atualize para CoddyKit PRO. O curso de Flutter Mobile Development inclui 4 aulas no total.
O que vou aprender em “Isolados em Segundo Plano e Gerenciamento de Memória Nativa”?
Execute chamadas FFI fora do isolado principal e gerencie alocações nativas para evitar vazamentos. Você pratica Flutter Mobile Development com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar Flutter Mobile Development?
Nenhuma experiência prévia é necessária. Flutter Mobile Development no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.
Quanto tempo leva a aula “Isolados em Segundo Plano e Gerenciamento de Memória Nativa”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de Flutter Mobile Development?
Sim. Cada aula de Flutter Mobile Development inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Chamada de Bibliotecas C com dart:ffi
- Canais de Plataforma Seguros quanto aos Tipos com Pigeon
- Escrita de Plugins de Plataforma Personalizados para iOS e Android
- Isolados em Segundo Plano e Gerenciamento de Memória Nativa