Release builds aborted on the first callback of every test: R8 renamed the IperfCallback implementation and its methods, so the JNI GetMethodID lookups resolved nothing. Adds the keep rules, and makes the native side report a failed lookup instead of aborting the process on the next JNI call. Debug builds do not minify, so every on-device check until now passed while the shipping artifact was broken. Found by running the release APK during release preparation. Verified with the release APK on an XQ-AS72: clean run at 806 Mbits/sec, matching the server's own figure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,13 @@ android {
|
|||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
|
||||||
|
// proguard-rules.pro keeps the names JNI resolves at runtime.
|
||||||
|
// Without it a release build aborts on the first callback.
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# JNI reaches these by name. R8 renames by default, so without these rules a
|
||||||
|
# release build resolves nothing: GetMethodID("onOutput", ...) returns NULL,
|
||||||
|
# raises NoSuchMethodError, and the next JNI call aborts the process with
|
||||||
|
# NoSuchMethodError: no non-static method "Lp0;.onOutput(Ljava/lang/String;)V"
|
||||||
|
# Debug builds do not minify, so this only ever appears in release.
|
||||||
|
|
||||||
|
# The plugin object holds the native method declarations and is looked up from
|
||||||
|
# C by its fully qualified name (Java_com_novoyuuparosk_iperf3gui_IperfRunner_*).
|
||||||
|
-keep class com.novoyuuparosk.iperf3gui.IperfRunner { *; }
|
||||||
|
|
||||||
|
# The callback interface and everything implementing it: iperf_jni.c resolves
|
||||||
|
# onOutput/onError/onComplete by name on the concrete object's class, so the
|
||||||
|
# implementors' method names must survive too, not just the interface's.
|
||||||
|
-keep interface com.novoyuuparosk.iperf3gui.IperfRunner$IperfCallback { *; }
|
||||||
|
-keep class * implements com.novoyuuparosk.iperf3gui.IperfRunner$IperfCallback { *; }
|
||||||
|
|
||||||
|
# Belt and braces for any future native method: keep the name of any class
|
||||||
|
# declaring one, and the native methods themselves.
|
||||||
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
@@ -86,6 +86,21 @@ struct iperf_run_state {
|
|||||||
* iperf_run_client() and cleared after it returns. */
|
* iperf_run_client() and cleared after it returns. */
|
||||||
static struct iperf_run_state *g_current_state = NULL;
|
static struct iperf_run_state *g_current_state = NULL;
|
||||||
|
|
||||||
|
/* Resolve a method ID, clearing the exception a failed lookup leaves behind.
|
||||||
|
*
|
||||||
|
* GetMethodID raises NoSuchMethodError on failure, and any further JNI call
|
||||||
|
* made while an exception is pending aborts the process. Returning NULL and
|
||||||
|
* clearing lets the caller fail on its own terms. */
|
||||||
|
static jmethodID resolve_method(JNIEnv *env, jclass cls,
|
||||||
|
const char *name, const char *sig) {
|
||||||
|
jmethodID mid = (*env)->GetMethodID(env, cls, name, sig);
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
(*env)->ExceptionClear(env);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return mid;
|
||||||
|
}
|
||||||
|
|
||||||
/* Helper: invoke the output callback via CallVoidMethod */
|
/* Helper: invoke the output callback via CallVoidMethod */
|
||||||
static void signal_output(JNIEnv *env, struct iperf_run_state *state,
|
static void signal_output(JNIEnv *env, struct iperf_run_state *state,
|
||||||
const char *msg) {
|
const char *msg) {
|
||||||
@@ -272,13 +287,25 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
|
|
||||||
/* Resolve callback method IDs up front so failures can be reported. */
|
/* Resolve callback method IDs up front so failures can be reported. */
|
||||||
jclass callback_cls = (*env)->GetObjectClass(env, callback);
|
jclass callback_cls = (*env)->GetObjectClass(env, callback);
|
||||||
jmethodID mid_output = (*env)->GetMethodID(
|
jmethodID mid_output = resolve_method(
|
||||||
env, callback_cls, "onOutput", "(Ljava/lang/String;)V");
|
env, callback_cls, "onOutput", "(Ljava/lang/String;)V");
|
||||||
jmethodID mid_error = (*env)->GetMethodID(
|
jmethodID mid_error = resolve_method(
|
||||||
env, callback_cls, "onError", "(Ljava/lang/String;)V");
|
env, callback_cls, "onError", "(Ljava/lang/String;)V");
|
||||||
jmethodID mid_complete = (*env)->GetMethodID(
|
jmethodID mid_complete = resolve_method(
|
||||||
env, callback_cls, "onComplete", "()V");
|
env, callback_cls, "onComplete", "()V");
|
||||||
|
|
||||||
|
/* If the names did not resolve there is no way to report anything: the
|
||||||
|
* reporting path is the thing that is missing. Bail out loudly instead.
|
||||||
|
*
|
||||||
|
* In practice this means R8 renamed the callback in a release build and
|
||||||
|
* android/app/proguard-rules.pro has stopped covering it. */
|
||||||
|
if (!mid_output || !mid_error || !mid_complete) {
|
||||||
|
LOGE("Callback methods did not resolve (onOutput=%p onError=%p "
|
||||||
|
"onComplete=%p) — check the -keep rules for IperfCallback",
|
||||||
|
mid_output, mid_error, mid_complete);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* Report a startup failure directly on the callback object and finish,
|
/* Report a startup failure directly on the callback object and finish,
|
||||||
* so Dart's completer always resolves instead of waiting for a timeout. */
|
* so Dart's completer always resolves instead of waiting for a timeout. */
|
||||||
#define FAIL_EARLY(msg) do { \
|
#define FAIL_EARLY(msg) do { \
|
||||||
|
|||||||
Reference in New Issue
Block a user