JNI: Unerklärlicher Crash beim Aufruf von CallVoidMethod

AndroidNDK

Mitglied
Moin!

Ich habe ein in C geschriebens Media Player Backend, welches ich nun versuche auf mittels NDK auf Android zu portieren. Da ich noch nie zuvor mit JNI gearbeitet habe, muss ich mir entsprechend auch alles nötige zusammensuchen, wodurch ich nun vor einem für mich und mein Wissen nicht nachvollziehbaren Problem stehe.

Der C Code des Backends funktioniert soweit Problemlos. Derzeit versuche ich also eine Brücke zwischen Android und C zu schaffen. Nun nehmen wir mal folgende Situation: Der Media Player gibt stets irgendwelche Audiodateien wieder, die View soll also anzeigen welcher Song gerade läuft, wie lange der geht, wie lange er schon abgespielt wird, seine Position innerhalb der Playlist, Cover, ...

Nun habe ich mir hierfür eine Callback Methode gebaut, die an diversen Stellen im Player aufgerufen wird, über die diee entsprechenden Informationen an Android weitergegeben werden. Diese sieht bislang wie folgt aus:

Java:
jobject *callbackCurrentTitleObject = NULL;
JNIEnv *callbackCurrentTitleEnv = NULL;
Java:
void currentTitleCallbackint position, char *cover, char *artist, char *title, double start, double end) {
    if(callbackCurrentTitleEnv != NULL && callbackCurrentTitleObject != NULL) {
        jclass cls = (*callbackCurrentTitleEnv )->GetObjectClass(callbackCurrentTitleEnv , callbackCurrentTitleObject);
        jmethodID mid = (*callbackCurrentTitleEnv )->GetMethodID(callbackCurrentTitleEnv , cls, "callbackCurrentTitle", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
        if(mid == 0) {
            return;
        }
        (*callbackCurrentTitleEnv )->CallVoidMethod(
        callbackCurrentTitleEnv ,
        callbackCurrentTitleObject ,
        mid,
        (*callbackCurrentTitleEnv )->NewStringUTF(callbackCurrentTitleEnv , cover),
        (*callbackCurrentTitleEnv )->NewStringUTF(callbackCurrentTitleEnv , artist),
        (*callbackCurrentTitleEnv )->NewStringUTF(callbackCurrentTitleEnv , title)
        );
    }
}
Java:
void Java_com_android_mediaplayer_MPLService_callbackCurrentTitle(JNIEnv *env, jobject javaThis) {
	callbackCurrentTitleEnv = env;
	callbackCurrentTitleObject = javaThis;
}
Nun kann ich lustig in meinem Media Player zwischen den Titeln in der Playlist hin und her wechseln, bekomme brav Cover, Artist und Titel angezeigt, und alles funktioniert wunderbar. Bis auf eine Ausnahme:

Sobald der Player einen Titel zu ende gespielt hat, und den nächsten anfängt, ruft er diesen Callback auf und übergibt die Daten des neuen Stücks. Und in genau dieser Situation, und das ist auch die einzige Situation wo das passiert, crasht die ganze App beim Aufruf von CallVoidMethod, und das OHNE jegliche Fehlermeldung warum. Zumindest spuckt mir der Log nichts aus. Die App hört einfach mit der Wiedergabe auf und ist weg vom Fenster.

Ich hab nun sämtliche Nachforschungen angestellt wieso sie sich so verhält, könnte ja sein dass vom Player keine Daten kommen, oder irgendwo irgendein Müll geliefert wird. Aber nichts, alle Daten kommen sauber rein, und es gibt keinen erkenntlichen Grund für mich wieso mir die App da crasht.

Hat jemand von Euch vlt. einen Tip für mich?

Und dann wäre da noch ein weiters Problem: Wenn ich nun die Callback Methode um int position und die Signatur auf
Code:
(I;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
erweiter bekomme den nächsten Crash mit einer NoSuchMethodError Fatal Exception. Dabei steht I doch für int, oder nicht? Was läuft denn da wieder falsch?

Ich Danke Euch schonmal im voraus! 🙂
 
Hallo, mal kurz zur Sache mit der Signatur und dem int: Du darfst kein Semikolon nach dem I haben, sondern dies einfach hinschreiben. Die Semikolons gibt es nur nach vollen Klassensignaturen wie
Code:
Ljava/lang/String;
.
Ansonsten kann es da noch sein, dass die Reihenfolge der Paramter nicht stimmt.

Zu dem Crash: So etwas passiert eigentlich immer nur, wenn C-seitig ein böser Fehler auftritt, wie NULL-Zugriff, Zugriffsverletzung etc..
Passiert es denn direkt beim CallVoidMethod-Aufruf oder ggfs. schon voher? Falls ja glaube ich derzeit nicht dass die NewStringUTF()-Funktionen hier das Problem sind (deine char*s sind ja wohl nicht NULL...).

Sind deine JNIEnv* und jobject* Objekte aktuell? D.h. sie müssen vom letzten JNI-Aufruf kommen, denn wenn man sie bei Verbindungsaufbau merkt dann können sie zwischenzeitlich neu angelegt worden sein.
 
Ah ok, ohne ; funzt es wunderbar. Danke für den Hinweis. Wegen CallVoidMethod, das war eben auch mein Gedanke dass irgendwo ein Fehler stecken muss, seis ein char* = NULL oder so. Daher habe ich die Übergabe der Daten genaustens überprüft, und mir alle Daten in der Callback-Methode ausgeben lassen. Kein Fehler, es kommt alles an wie es soll.

Es ist der Aufruf. Selbst wenn ich nach der Ermittlung von jmethodID unmittelbar vor dem Aufruf von CallVoidMethod eine Logausgabe mach wird mir diese ausgegeben bevor die App dann durch den besagten Aufruf crasht.

Das Backend ist an einen Service gekoppelt. Sobald dieser gestartet wird, wird der Callback einmalig an das Backend geschickt, wo JNIEnv* und jobject* gespeichert werden, und seit dem steht das auch. Wüsste also nicht, woher die irgendwie neu erstellt werden sollten. Damit kann ich wie schon gesagt ganz normal durch die Titel springen, auch während der Wiedergabe, und alles läuft ohne Probleme. Nur eben in dieser einen Situation, sobald der Player ein Stück zu ende gespielt hat und ein neues beginnt, da crasht das ganze.

Deswegen ist es unerklärlich für mich.

Edit: Das ganze fängt an jetzt lustig zu werden. Habe nun auf die selbe Art und Weise einen zweiten Callback hinzugefügt, der mir die aktuelle Wiedergabeposition übermitteln soll. Signatur: D. Sobald ich auf Play drücke crasht der Player, wieder durch den Aufruf von CallVoidMethod.
 
Zuletzt bearbeitet:
Ah ok, ohne ; funzt es wunderbar.

Es gibt mehr als einen Grund, warum man sich die Header mit [c]javah[/c] generiert: Die Methodensignaturen per Hand zusammenzufrickeln ist fehleranfällig... (q.e.d. 😀)

Das Backend ist an einen Service gekoppelt. Sobald dieser gestartet wird, wird der Callback einmalig an das Backend geschickt, wo JNIEnv* und jobject* gespeichert werden,

Das klingt gefährlich - da muss man zumindest Global References dafür anlegen (siehe Local and Global References ).

Beim JNIEnv hilft nicht mal das: Die MUSS eigentlich immer vom Aufruf aus durchgeschleift werden. Bei Callbacks wird's dann kompliziert: Man muss sich in der "JNI_OnLoad" die JVM speichern, und sich dann im Callback dann mit jvm->GetEnv ein JNIEnv holen (und ggf. noch den Thread dranhängen, mit AttachCurrentThread). Das steht in Additional JNI Features kurz beschrieben, aber schon SEHR kurz, ggf. mal eine asuführlichere Websuche nach den Stichworten machen, um Beispiele zu finden (in jocl.org - Java bindings for OpenCL werden solche Callbacks verwendet, aber die relevanten Teile da rauszufilten wäre vermutlich aufwändiger als ein kleines, dediziertes Beispiel dazu zu suchen)
 
Ich hab mich soviel nicht mit Java auseinandergesetzt. Nur fürs Studium, oder eben für Android. Javah ist mir da eher ein Fremdwort, werde es mir aber mal angucken. 😉

Gefährlich inwiefern? Macht es nen Unterschied ob ich das Backend an einen Service oder eine Activity koppel?

Was die References etc. angeht, habe ich mich mal ein wenig umgeschaut, und nach dem Besuch von mehreren Seiten folgendes gemacht:
Java:
static JavaVM *globalJvm;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved)
{
	JNIEnv *env = NULL;
	if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_6)) {
		mp_log_error("JNI Version not supported...");
		return JNI_ERR;
	}

	globalJvm = jvm;

	return JNI_VERSION_1_6;
}
Java:
void currentTitlecallback(int position, char *cover, char *artist, char *title, double start, double end) {

	int status;
	JNIEnv *env;
	int isAttached = 0;

	if(!callbackCurrentTitleObject) return;

	if ((status = (*globalJvm)->GetEnv(globalJvm, (void**)&env, JNI_VERSION_1_6)) < 0) {
		if ((status = (*globalJvm)->AttachCurrentThread(globalJvm, &env, NULL)) < 0) {
			return;
		}
		isAttached = 1;
	}

	jclass cls = (*env)->GetObjectClass(env, callbackCurrentTitleObject);
	if (!cls) {
		if (isAttached) (*globalJvm)->DetachCurrentThread(globalJvm);
		return;
	}

	jmethodID mid = (*env)->GetMethodID(env, cls, "callbackCurrentTitle", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;DD)V");
	if(mid == 0) {
		mp_log_error("method callbackCurrentTitle not found...");
		return;
	}

	(*env)->CallVoidMethod(
			env,
			callbackCurrentTitleObject,
			mid,
			position,
			(*env)->NewStringUTF(env, cover),
			(*env)->NewStringUTF(env, artist),
			(*env)->NewStringUTF(env, title),
			start,
			end
			);

	if (isAttached) (*globalJvm)->DetachCurrentThread(globalJvm);
}
Java:
void Java_com_android_mediaplayer_MPLService_callbackCurrentTitle(JNIEnv *env, jobject javaThis) {
	callbackCurrentTitleObject = (*env)->NewGlobalRef(env, javaThis);
}
Crashen tut das ganze immernoch, allerdings hab ich jetzt zumindest eine Fehlermeldung:
Java:
03-16 11:59:10.338: E/AndroidRuntime(7027): android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
Ist also ein Thread Problem, meine zweite Vermutung hat sich bestätigt. Die Library die ich zur Wiedergabe meiner Medien benutze nutzt ne StreamProc Methode, aus dieser heraus dieser Callback aufgerufen wird. Hab allerdings k.a. wie diese intern funktioniert da nicht von mir. 😉

Frage wäre also wie ich das Problem jetzt beseitigt bekomme?
 
Crashen tut das ganze immernoch, allerdings hab ich jetzt zumindest eine Fehlermeldung:
Java:
03-16 11:59:10.338: E/AndroidRuntime(7027): android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
Ist also ein Thread Problem, meine zweite Vermutung hat sich bestätigt. Die Library die ich zur Wiedergabe meiner Medien benutze nutzt ne StreamProc Methode, aus dieser heraus dieser Callback aufgerufen wird. Hab allerdings k.a. wie diese intern funktioniert da nicht von mir. 😉

Frage wäre also wie ich das Problem jetzt beseitigt bekomme?

Du darfst an der UI nur Veränderungen mit dem UI Thread machen, schau dir dazu mal in Handler oder Activity.runOnUiThread an.

Painless Threading | Android Developers

Es sieht wohl so aus als würde deine Medienwiedergabe einen eigenen Thread starten und du rufst aus diesem die Java Callbacks auf.

EDIT: Das gefährlich bezog sich denke ich nicht auf Activity oder Service, sondern darauf das du JNIEnv cached, dies aber nciht erlaubt ist. DU darfst nur das JVM Object und jmethidID Objekte cachen so weit ich weiß.
 
Zuletzt bearbeitet:
Beim Überfliegen sieht der JNI-Code jetzt besser aus - und bekräftigt (wenn auch noch lange nicht bestätigt) wird das ja auch durch das nicht-mehr-crashen 🙂

Der neue Fehler ... Da wäre es gut, den ganzen Stacktrace zu sehen (Debugging bridge anmachen und mal in der LogCat schauen). Vielleicht

HIER habe ich nochmal kurz runtergescrollt
Java:
throw new AnswerInterruptedException("mjdv hat schon geantwortet!");
😀
 
Java:
03-16 12:34:17.314: E/AndroidRuntime(7507): FATAL EXCEPTION: Thread-10
03-16 12:34:17.314: E/AndroidRuntime(7507): android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.view.ViewRoot.checkThread(ViewRoot.java:3165)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.view.ViewRoot.invalidateChild(ViewRoot.java:690)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.view.ViewRoot.invalidateChildInParent(ViewRoot.java:716)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.view.ViewGroup.invalidateChild(ViewGroup.java:2624)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.view.View.invalidate(View.java:5341)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.widget.TextView.checkForRelayout(TextView.java:5778)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.widget.TextView.setText(TextView.java:2817)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.widget.TextView.setText(TextView.java:2685)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at android.widget.TextView.setText(TextView.java:2660)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at com.android.mediaplayer.MPLActivity.onCurrentTitleChange(MPLActivity.java:67)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at com.android.mediaplayer.MPLService.callbackCurrentTitle(MPLService.java:188)
03-16 12:34:17.314: E/AndroidRuntime(7507): 	at dalvik.system.NativeStart.run(Native Method)
Das der ganze Stacktrace. Kommt im übrigen Automatisch, habe im Java Code kein Try/Catch.
 
Siehe meinen Post oben, es scheint als würdest du setText() aus dem falschen Thread aufrufen. Versuchs einfach mal mit:

Java:
runOnUiThread( new Runnable() {
   public void run() {
      textView.setText(...);
   }
});
 

Zurück
Oben