Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App always crashes on hot restart #970

Closed
TheLastGimbus opened this issue Feb 25, 2024 · 7 comments
Closed

App always crashes on hot restart #970

TheLastGimbus opened this issue Feb 25, 2024 · 7 comments

Comments

@TheLastGimbus
Copy link

While hot-reload works fine, hot-restarting crashes my app every time with JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid global reference: 0x30c6 (deleted reference at index 390) in call to DeleteGlobalRef

I will obviously show my code, but I think it's details are not useful because I suspect I must be "not properly freeing Java references" in dispose()es or something like that - so, I'm opening this issue as a request to add some universal notes about when and why to call .release() 🙏

My main "plugin" file: https://github.com/TheLastGimbus/the_last_bluetooth/blob/60013e4959cb1f0ecd8c2d9f2e1daea81f4a60bd/lib/the_last_bluetooth.dart

My example's main.dart that uses it: https://github.com/TheLastGimbus/the_last_bluetooth/blob/60013e4959cb1f0ecd8c2d9f2e1daea81f4a60bd/example/lib/main.dart

The gigantic stack trace: https://pastebin.com/TRa3kyVu

@TheLastGimbus
Copy link
Author

Later, I discovered that JNI crashes hard whenever closing the flutter engine (??)

I discovered this because my app has a background WorkManager worker that uses same plugin (that I'm now re-writing in jni) in background

If it fires at the same time I use the app, it launches it's own flutterEngine, does some stuff, and then closes it... which crashes the whole app:

I/flutter (14762): ###### INIT UI STUFF ###### // my own log
I/flutter (14762): dynamic_color: Core palette detected.
W/FlutterJNI(14762): FlutterJNI.loadLibrary called more than once
W/FlutterJNI(14762): FlutterJNI.prefetchDefaultFontManager called more than once
W/FlutterJNI(14762): FlutterJNI.init called more than once
...
I/flutter (14762): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): │ #0   callbackDispatcher.<anonymous closure> (package:freebuddy/platform_stuff/android/background/periodic.dart:71:10)
I/flutter (14762): │ #1   Workmanager.executeTask.<anonymous closure> (package:workmanager/src/workmanager.dart:129:28)
I/flutter (14762): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (14762): │ 🐛 Running periodic task freebuddy.routine_update
I/flutter (14762): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): │ #0   routineUpdateCallback (package:freebuddy/platform_stuff/android/background/periodic.dart:35:8)
I/flutter (14762): │ #1   callbackDispatcher.<anonymous closure> (package:freebuddy/platform_stuff/android/background/periodic.dart:75:32)
I/flutter (14762): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (14762): │ ⚠️ creating cubit...
I/flutter (14762): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): │ #0   HeadphonesConnectionCubit._connect (package:freebuddy/headphones/cubit/headphones_connection_cubit.dart:81:16)
I/flutter (14762): │ #1   HeadphonesConnectionCubit._pairedDevicesHandle.<anonymous closure> (package:freebuddy/headphones/cubit/headphones_connection_cubit.dart:128:13)
I/flutter (14762): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (14762): │ ⚠️ Error when connecting socket: 1/3 tries
I/flutter (14762): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
...
I/flutter (14762): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): │ #0   batteryHomeWidgetHearBloc.<anonymous closure> (package:freebuddy/platform_stuff/android/appwidgets/battery_appwidget.dart:26:12)
I/flutter (14762): │ #1   _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
I/flutter (14762): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (14762): │ 🐛 Updating widget from UI listener: Instance of 'LRCBatteryLevels'
I/flutter (14762): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): │ #0   routineUpdateCallback (package:freebuddy/platform_stuff/android/background/periodic.dart:56:10)
I/flutter (14762): │ #1   <asynchronous suspension>
I/flutter (14762): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (14762): │ 🐛 udpating widget from background: Instance of 'LRCBatteryLevels'
I/flutter (14762): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// this is what i think causes the crash
I/flutter (14762): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (14762): │ #0   routineUpdateCallback (package:freebuddy/platform_stuff/android/background/periodic.dart:59:10)
I/flutter (14762): │ #1   <asynchronous suspension>
I/flutter (14762): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (14762): │ ⚠️ Ending background task!
I/flutter (14762): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
F/freebuddy.debug(14762): java_vm_ext.cc:591] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid global reference: 0x3786 (deleted reference at index 444)
F/freebuddy.debug(14762): java_vm_ext.cc:591]     in call to DeleteGlobalRef
F/freebuddy.debug(14762): runtime.cc:691] Runtime aborting...
F/freebuddy.debug(14762): runtime.cc:691] Dumping all threads without mutator lock held
F/freebuddy.debug(14762): runtime.cc:691] All threads:
F/freebuddy.debug(14762): runtime.cc:691] DALVIK THREADS (33):
F/freebuddy.debug(14762): runtime.cc:691] "main" prio=10 tid=1 Native
F/freebuddy.debug(14762): runtime.cc:691]   | group="" sCount=1 ucsCount=0 flags=1 obj=0x72339be0 self=0xb40000706a1657b0
F/freebuddy.debug(14762): runtime.cc:691]   | sysTid=14762 nice=-10 cgrp=system sched=0/0 handle=0x72782d14f8
F/freebuddy.debug(14762): runtime.cc:691]   | state=S schedstat=( 974100948 57648607 850 ) utm=86 stm=10 core=7 HZ=100
F/freebuddy.debug(14762): runtime.cc:691]   | stack=0x7fd97b6000-0x7fd97b8000 stackSize=8188KB
F/freebuddy.debug(14762): runtime.cc:691]   | held mutexes=
F/freebuddy.debug(14762): runtime.cc:691]   native: #00 pc 00055e1c  /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: c74277f481a383c87215b672f6465e24)
F/freebuddy.debug(14762): runtime.cc:691]   native: #01 pc 0005a7c8  /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex+144) (BuildId: c74277f481a383c87215b672f6465e24)
/// etc

I see the same crash when i swipe away my app (well, then it's not the problem obviously 😂)

Any useful details: I am using Jni.initDLApi() and jni.Context.fromRef(Jni.getCachedApplicationContext()), if that changes anything

And I, of course, don't .release() anything because I don't know when 🤷🤷

TheLastGimbus added a commit to TheLastGimbus/FreeBuddy that referenced this issue Feb 28, 2024
@HosseinYousefi
Copy link
Member

HosseinYousefi commented Mar 5, 2024

About your original issue: You're doing

  _manager = android.BluetoothManager.fromRef(
      ctx
          .getSystemService(android.Context.BLUETOOTH_SERVICE.toJString())
          .reference,
    );

to simplify, let's write it like:

bar = Bar.fromRef(foo.reference);

Here, both bar and foo point to the same underlying reference (foo.reference) so once they get GC'd (and finalized), the same reference gets deleted twice. Once by bar and once by foo. That's why it crashes. Instead do:

bar = foo.castTo(Bar.type);

This will create a new reference for bar instead of reusing the same reference. And if you don't want to keep foo around, you can do that too:

bar = foo.castTo(Bar.type, releaseOriginal: true);

This way, the same underlying reference is used for bar, but foo is "set as released" – meaning it's got detached from the finalizer and will not delete the reference when GC'd.

So, in your case, do:

_manager = ctx.getSystemService(android.Context.BLUETOOTH_SERVICE.toJString())
  .castTo(android.BluetoothManager.type, releaseOriginal: true);

TheLastGimbus added a commit to TheLastGimbus/the_last_bluetooth that referenced this issue Mar 5, 2024
@TheLastGimbus
Copy link
Author

Oh it works now!!! Both of my issues are gone ✌️ Big thanks again!

@mkustermann
Copy link
Member

to simplify, let's write it like:

bar = Bar.fromRef(foo.reference);

Here, both bar and foo point to the same underlying reference (foo.reference) so once they get GC'd (and finalized), the > same reference gets deleted twice. Once by bar and once by foo. That's why it crashes. Instead do:

@HosseinYousefi Does this issue arise due to us using inheritance over composition? More specifically I see that we have

class JReference implements Finalizable {}
class JObject extends JReference {}
class JArray extends JObject {}
class X extends JObject {}

It would seem more natural to model this as composition and attach finalizers to JReference:

class JReference implements Finalizable {}

class JObject {
  final JReference _reference;
}
class JArray extends JObject {}
class X extends JObject {}

Though since we do allow eager releasing, any re-use of the reference must be made explicit - so developers have to decide (and know) whether reference is shared or not, and thereby be cautions about when to release.

@HosseinYousefi
Copy link
Member

@HosseinYousefi Does this issue arise due to us using inheritance over composition?

Yes, this exact problem will be solved, and I will change this in the next version (also to allow sharing of JObjects). Still I think having easy access to reference can be a footgun. I also opened an issue (#979) about this problem.

@TheLastGimbus
Copy link
Author

TheLastGimbus commented Mar 6, 2024

Looking forward to sending jobjects between Isolates because it will allow me for async read/writes to bluetooth - right now I'm literally doing

while(true) {
  if(available > 0) {
    ...
  }
  await Future.delayed(10ms); // free the main Isolate
}

@HosseinYousefi
Copy link
Member

HosseinYousefi commented Mar 6, 2024

Looking forward to sending jobjects between Isolates because it will allow me for async read/writes to bluetooth - right now I'm literally doing

while(true) {
  if(available > 0) {
    ...
  }
  await Future.delayed(10ms); // free the main Isolate
}

You can send them between isolates. It's just a bit manual right now:

For example, using this helper method:

Future<R> runOnIsolate<T extends JObject, R>(
    T object, R Function(T object) callback) async {
  final type = object.$type;
  final address = object.reference.address;
  return Isolate.run(() {
    final ref = Jni.env.NewGlobalRef(Pointer.fromAddress(address));
    final sentObject = type.fromRef(ref) as T;
    return callback(sentObject);
  });
}

And then you can run something on a different isolate like:

final str = 'hello'.toJString();
await runOnIsolate(str, (object) {
  print(object);
}); // prints hello

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants