Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion pkgs/ffigen/doc/objc_gc.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ Objective-C uses reference counting to delete objects that are no longer in
use, and Dart uses garbage collection (GC) for this. The Dart wrapper objects
that wrap Objective-C objects automatically increment the reference count
when the wrapper is created, and decrement it when the wrapper is destroyed.

For the most part this is all automatic and you don't need to worry about it.
But there are two main ways that memory can leak during Objective-C interop.

## Reference Cycles

However, when using blocks or protocols, it's possible to create reference
When using blocks or protocols, it's possible to create reference
cycles that will prevent these objects from being cleaned up. If a
block/protocol method closes over a wrapper object that holds a reference
to the block/protocol, this cycle will cause a memory leak. This example
Expand Down Expand Up @@ -52,3 +56,55 @@ void main() {
foo.delegate = createBarDelegate(WeakReference(foo));
}
```

## Autoreleased References

Objective-C has a mechanism where references can be autoreleased,
which means they are placed in an [autorelease pool](
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html).
Autorelease pools form a stack, and autoreleased references are placed in the
top-most pool.
When that pool is destroyed (popped from the stack),
all the references it contains are released.
If the pool is too long-lived, these references are effectively leaked.

Autoreleasing is primarily used for returning results from methods. If you
invoke a method that returns an object reference, it's likely that it was
autoreleased (unless it's an `alloc`, `init`, `new`, or `copy` method).
Additionally, the Objective-C API you're invoking may also create autoreleased
references internally.
So you may be creating these references without knowing it.

In native Objective-C apps, and Flutter apps, autorelease pools are
created and destroyed at event loop boundaries (e.g. every frame).
So this is usually not a problem.
However, until Dart 3.14 (Flutter 3.50), there was
[a bug](https://github.com/dart-lang/sdk/issues/61129)
where Flutter background isolates did not have an
autorelease pool in their event loop.
This also affected *all* isolates in Dart CLI apps.

Even if you have an autorelease pool around the event loop,
autoreleased references won't be cleaned up until the next event loop cycle
(eg at an `async`/`await` boundary, or after a message handler).
If you have a long running block of synchronous code,
autoreleased references may pile up, holding on to memory that should be freed.

The fix to these problems, just like in Objective-C code,
is to use an autorelese pool.
The Dart API for this is [`autoReleasePool`](
https://pub.dev/documentation/objective_c/latest/objective_c/autoReleasePool.html).

<!-- file://./../../objective_c/tool/snippets/autorelease_snippet.dart#autorelease_pool -->
```dart
while (longRunningCondition) {
// When writing ObjC interop code inside a long running loop, it's a good
// idea to use an autorelease pool to clean up autoreleased references.
autoReleasePool(() {
// Interacting with most ObjC APIs autoreleases a lot of internal refs.
final someObjCObject = fooObjCApi.loadNextObject();
someObjCObject.greet('Hello'.toNSString());
barObjCApi.sendObject(someObjCObject);
});
}
```
Loading