| ## Lifecycle of Java objects | |
| All Java classes generated by `package:jnigen` extend `JObject`. In JNI there | |
| are several kinds of references: local, global, and weak global. Local | |
| references are tied to a single thread. To enable seamless transfer of | |
| `JObject`s between isolates and safe usage in asynchronous code, `JObject`s | |
| always use **global** references. It's crucial to remember that there's a limit | |
| of approximately 50,000 global references. | |
| Once all references (in both Java and Dart) to an object are gone, Java's | |
| garbage collector (GC) can reclaim it. | |
| ### Automatic reference management | |
| `JObject`s attach a native finalizer to their global references. Therefore, when | |
| the Dart GC collects them, the underlying Java reference is released. | |
| This approach works well for application code where a large number of references | |
| aren't created repeatedly. However, it's not ideal for packages because the | |
| usage patterns are unpredictable as you will never know how your users will use | |
| your package. This can lead to exceeding the global reference limit and | |
| application crashes. | |
| ### Eagerly releasing references manually (recommended for packages) | |
| Instead of waiting for Dart GC to release the JNI global references, | |
| `.release()` can be called on the `JObject`s. | |
| <!-- file://./../example/api/lifecycle_snippet.dart#release_manual --> | |
| ```dart | |
| // Construct the object. | |
| final hello = 'Hello'.toJString(); | |
| // Use it. | |
| print(hello); | |
| // Eagerly release it! | |
| hello.release(); | |
| ``` | |
| To make releasing easier, we can use | |
| [`Arena`](https://pub.dev/documentation/ffi/latest/ffi/Arena-class.html)s. | |
| First, create an `Arena` via | |
| [`using`](https://pub.dev/documentation/ffi/latest/ffi/using.html). Then | |
| register the object to be released at the end of the callback. | |
| <!-- file://./../example/api/lifecycle_snippet.dart#arena_using --> | |
| ```dart | |
| using((arena) { | |
| final hello = 'Hello'.toJString()..releasedBy(arena); | |
| final world = 'World'.toJString()..releasedBy(arena); | |
| print(hello); | |
| print(world); | |
| }); | |
| // Both `hello` and `world` are now released. | |
| ``` | |
| ### Tips on keeping the number of global references low | |
| - Avoid storing `JObject`s in Dart collections like `List` or `Map`. Use Java | |
| collections such as `JList` or `JMap` instead. | |
| <!-- file://./../example/api/lifecycle_snippet.dart#java_collections --> | |
| ```dart | |
| // GOOD: | |
| final jstrings = JList(JString.type); | |
| using((arena) { | |
| final hello = 'Hello'.toJString()..releasedBy(arena); | |
| final world = 'World'.toJString()..releasedBy(arena); | |
| jstrings.add(hello); // Add to Java collection | |
| jstrings.add(world); // Add to Java collection | |
| }); | |
| print(jstrings.length); // prints 2. | |
| ``` | |
| This approach avoids storing individual references. References are created | |
| only when accessing elements and released afterward. | |
| - Minimize the use of `static` or global `JObject`s. This prevents their | |
| references from being released. | |
| - When an original Java object is no longer needed, set `releaseOriginal` to | |
| `true` during conversion to Dart equivalents or casting. | |
| <!-- file://./../example/api/lifecycle_snippet.dart#release_original --> | |
| ```dart | |
| final foo = Foo(); | |
| final String string = foo.someJString().toDartString(releaseOriginal: true); | |
| final JInteger jint = | |
| foo.someJNumber().as(JInteger.type, releaseOriginal: true); | |
| final int dartInt = jint.toDartInt(releaseOriginal: true); | |
| foo.release(); | |
| // All references are removed. | |
| ``` | |
| ### Lifecycle of interfaces implemented in Dart | |
| Java interfaces can be implemented in Dart using `.implement`. To ensure | |
| accessiblity of the Dart implementations as long as the created Java object | |
| exists, JNIgen transfers the ownership of these Dart objects to Java. When the | |
| Java GC reclaims the implemented object, a message is sent to Dart to remove the | |
| corresponding closures. | |
| ### Cycles between Java and Dart GC's | |
| One could create cycles between Dart and Java GC's when implementing interfaces. | |
| For example consider the following: | |
| <!-- file://./../example/api/lifecycle_snippet.dart#cycle --> | |
| ```dart | |
| final foo = Foo(); | |
| foo.bar = Bar.implement($Bar( | |
| f: () { | |
| return foo; | |
| }, | |
| )); | |
| ``` | |
| In Dart, closure `f` holds a reference to `foo`, preventing its garbage | |
| collection. In Java, `foo` holds a reference to `bar`, preventing its release | |
| and thus preventing the closure's removal. This creates a cycle. | |
| ```mermaid | |
| graph TD; | |
| f-->|holds in Dart|foo; | |
| foo-->|holds in Java|bar; | |
| bar-->|holds in Java|f; | |
| ``` | |
| To prevent cycles, use | |
| [`WeakReference`](https://api.dart.dev/dart-core/WeakReference-class.html)s. | |
| <!-- file://./../example/api/lifecycle_snippet.dart#cycle_breaker --> | |
| ```dart | |
| final weakFoo = WeakReference(foo); | |
| foo.bar = Bar.implement($Bar( | |
| f: () { | |
| final foo = weakFoo.target; | |
| if (foo == null) { | |
| throw StateError('Foo was collected'); | |
| } | |
| return foo; | |
| }, | |
| )); | |
| ``` | |
| The weak reference breaks the cycle, allowing the GCs to collect the objects. | |
| ```mermaid | |
| graph TD; | |
| f-.->|holds weakly in Dart|foo; | |
| foo-->|holds in Java|bar; | |
| bar-->|holds in Java|f; | |
| ``` | |
| > [!WARNING] | |
| > | |
| > Be careful about the closures accidentally overcapturing. To prevent | |
| > overcapturing, implement your logic in a separate function or create a class | |
| > that implements `$Bar`. | |
| > | |
| > <!-- file://./../example/api/lifecycle_snippet.dart#cycle_breaker_class --> | |
| > ```dart | |
| > final class BarImpl with $Bar { | |
| > final WeakReference<Foo> weakFoo; | |
| > | |
| > BarImpl(this.weakFoo); | |
| > | |
| > @override | |
| > Foo f() { | |
| > final foo = weakFoo.target; | |
| > if (foo == null) { | |
| > throw StateError('Foo was collected'); | |
| > } | |
| > return foo; | |
| > } | |
| > } | |
| > | |
| > void cycleBreakerClassMain(Foo foo) { | |
| > final weakFoo = WeakReference(foo); | |
| > foo.bar = Bar.implement(BarImpl(weakFoo)); | |
| > // ... | |
| > } | |
| > ``` |