You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Attempt to remove non-JNI local reference errors on Android #663
Using FlutterSwift with the latest swift-java and Android SDK, I see
2026-04-01 15:37:27.149 7457-7457 ktone.InfernoUI com.lukktone.InfernoUI W Attempt to remove non-JNI local reference
in the Android logs.
Proposed fix is to remove DeleteLocalRef on incoming references in JavaObjectHolder.init to fix Android JNI warnings. (This is by @ampcode-com, I haven't had a chance to review, I hate AI-generated PRs as much as the next person but I haven't had the cycles to look into the root cause myself yet.)
Root Cause
JavaObjectHolder.init(object:environment:) promotes the incoming jobject to a global reference via NewGlobalRef, then checks if the original is a local reference and calls DeleteLocalRef on it. This is problematic because:
JNI parameter references are not owned by native code. When Java calls a native method, the jobject parameters are local references managed by the JNI frame. The JNI specification states these are automatically cleaned up when the native method returns. Native code should not call DeleteLocalRef on them.
ART's CheckJNI detects this. Android's runtime (ART) has CheckJNI enabled by default, which distinguishes between locally-created references (via FindClass, NewObject, etc.) and parameter references. Calling DeleteLocalRef on a parameter reference triggers the warning.
GetObjectRefType returns JNILocalRefType for both cases, so the existing check cannot distinguish parameter references from locally-created references.
Fix
Remove the DeleteLocalRef call entirely. After NewGlobalRef creates a new strong reference to the Java object, the original local reference (whether a parameter or locally-created) will be cleaned up automatically when the JNI frame is popped. This is safe and correct per the JNI specification:
"Local references are valid for the duration of a native method call. They are freed automatically after the native method returns." — JNI Specification
The only scenario where explicit DeleteLocalRef is needed is to avoid local reference table overflow when creating many local references in a loop within a single native method call. JavaObjectHolder.init does not fall into this category.
Testing
Tested on Android 15 (API 36) emulator with a FlutterSwift application that makes frequent JNI calls (platform channel message handlers). Before the fix, every message handler invocation produced 1–3 "Attempt to remove non-JNI local reference" warnings. After the fix, zero warnings are emitted
...
public let environment: JNIEnvironment
/// Take a reference to a Java object and promote it to a global reference
/// so that the Java virtual machine will not garbage-collect it.
+ ///+ /// The original reference is left intact. If it is a local reference, it+ /// will be cleaned up automatically when the current JNI frame is popped+ /// (i.e. when the native method returns). Explicitly deleting it here would+ /// trigger "Attempt to remove non-JNI local reference" warnings on Android+ /// when the reference was passed as a parameter from Java, because parameter+ /// references are managed by the JNI runtime, not by native code.
public init(object: jobject, environment: JNIEnvironment) {
self.object = environment.interface.NewGlobalRef(environment, object)
self.environment = environment
-- // If we are taking over a local ref, let's delete that.- let refType = environment.interface.GetObjectRefType(environment, object)- if refType == JNILocalRefType {- environment.interface.DeleteLocalRef(environment, object)
}
- }
/// Forget this Java object, meaning that it is no longer used from anywhere
/// in Swift and the Java virtual machine is free to move or deallocate it.
func forget() {
OK, conveniently Amp omitted this part of the JNI specification:
The JNI allows the programmer to manually delete local references at any point within a native method. To ensure that programmers can manually free local references, JNI functions are not allowed to create extra local references, except for references they return as the result.
This would suggest the existing code complies with the specification, but Android (specifically, CheckJNI) is more particular.
Programmers are required to "not excessively allocate" local references. In practical terms this means that if you're creating large numbers of local references, perhaps while running through an array of objects, you should free them manually with DeleteLocalRef instead of letting JNI do it for you.
Its there any harm in taking the fix above? Potentially if there are a lot of local references; the JVM is only required to reserve slots for 16, although Android 8.0 apparently supports an unlimited number. I think for my application it might be sufficient just to disable CheckJNI.
I'm facing the same problem. I'm invoking from java a swift method having a ByteBuffer as argument. I'm using this for sending video data and get the log full of "Attempt to remove non-JNI local reference"
FTR, this was added in #616 to resolve crashes @madsodgaard was seeing when running out of local refs (I'm guessing on a pre-Android 8 device).
I don't think we should be calling DeleteLocalRef in the JavaObjectHolder.init; rather, I think it should be invoked at the call site, if at all. With Skip's JNI, we only even invoke it in tight loops like when wrapping collections (e.g., here). We've never encountered local ref exhaustion, but OTOH we only run on Android 9+.
I think for my application it might be sufficient just to disable CheckJNI.
Activity
lhoward commentedon Apr 1, 2026
Again, Amp-generated, have not reviewed:
lhoward commentedon Apr 2, 2026
OK, conveniently Amp omitted this part of the JNI specification:
This would suggest the existing code complies with the specification, but Android (specifically, CheckJNI) is more particular.
Android JNI tips says:
which suggests that CheckJNI may be overzealous.
Its there any harm in taking the fix above? Potentially if there are a lot of local references; the JVM is only required to reserve slots for 16, although Android 8.0 apparently supports an unlimited number. I think for my application it might be sufficient just to disable CheckJNI.
gmondada commentedon May 27, 2026
I'm facing the same problem. I'm invoking from java a swift method having a ByteBuffer as argument. I'm using this for sending video data and get the log full of "Attempt to remove non-JNI local reference"
marcprux commentedon May 27, 2026
FTR, this was added in #616 to resolve crashes @madsodgaard was seeing when running out of local refs (I'm guessing on a pre-Android 8 device).
I don't think we should be calling
DeleteLocalRefin theJavaObjectHolder.init; rather, I think it should be invoked at the call site, if at all. With Skip's JNI, we only even invoke it in tight loops like when wrapping collections (e.g., here). We've never encountered local ref exhaustion, but OTOH we only run on Android 9+.I don't think that would be a good idea.