J'ai travaillé sur un problème d'appel synchrone à JavaScript dans un fichier de type WebView
(avec une valeur de retour) et j'essaie de déterminer où et pourquoi cela ne fonctionne pas. Il semble que le WebView
bloque pendant que le thread principal attend une réponse de sa part, ce qui ne devrait pas être le cas puisque le thread WebView
fonctionne sur un fil séparé.
J'ai rassemblé ce petit échantillon qui le démontre (j'espère) assez clairement :
main.xml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:weightSum="1">
<WebView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/webView"/>
</LinearLayout>
MonActivité.java :
package com.example.myapp;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.JavascriptInterface;
import android.webkit.WebViewClient;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class MyActivity extends Activity {
public final static String TAG = "MyActivity";
private WebView webView;
private JSInterface JS;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
webView = (WebView)findViewById(R.id.webView);
JS = new JSInterface();
webView.addJavascriptInterface(JS, JS.getInterfaceName());
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient() {
public void onPageFinished(WebView view, String url) {
Log.d(TAG, JS.getEval("test()"));
}
});
webView.loadData("<script>function test() {JSInterface.log(\"returning Success\"); return 'Success';}</script>Test", "text/html", "UTF-8");
}
private class JSInterface {
private static final String TAG = "JSInterface";
private final String interfaceName = "JSInterface";
private CountDownLatch latch;
private String returnValue;
public JSInterface() {
}
public String getInterfaceName() {
return interfaceName;
}
// JS-side functions can call JSInterface.log() to log to logcat
@JavascriptInterface
public void log(String str) {
// log() gets called from Javascript
Log.i(TAG, str);
}
// JS-side functions will indirectly call setValue() via getEval()'s try block, below
@JavascriptInterface
public void setValue(String value) {
// setValue() receives the value from Javascript
Log.d(TAG, "setValue(): " + value);
returnValue = value;
latch.countDown();
}
// getEval() is for when you need to evaluate JS code and get the return value back
public String getEval(String js) {
Log.d(TAG, "getEval(): " + js);
returnValue = null;
latch = new CountDownLatch(1);
final String code = interfaceName
+ ".setValue(function(){try{return " + js
+ "+\"\";}catch(js_eval_err){return '';}}());";
Log.d(TAG, "getEval(): " + code);
// It doesn't actually matter which one we use; neither works:
if (Build.VERSION.SDK_INT >= 19)
webView.evaluateJavascript(code, null);
else
webView.loadUrl("javascript:" + code);
// The problem is that latch.await() appears to block, not allowing the JavaBridge
// thread to run -- i.e., to call setValue() and therefore latch.countDown() --
// so latch.await() always runs until it times out and getEval() returns ""
try {
// Set a 4 second timeout for the worst/longest possible case
latch.await(4, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Log.e(TAG, "InterruptedException");
}
if (returnValue == null) {
Log.i(TAG, "getEval(): Timed out waiting for response");
returnValue = "";
}
Log.d(TAG, "getEval() = " + returnValue);
return returnValue;
}
// eval() is for when you need to run some JS code and don't care about any return value
public void eval(String js) {
// No return value
Log.d(TAG, "eval(): " + js);
if (Build.VERSION.SDK_INT >= 19)
webView.evaluateJavascript(js, null);
else
webView.loadUrl("javascript:" + js);
}
}
}
Lors de l'exécution, le résultat est le suivant :
Emulator Nexus 5 API 23:
05-25 13:34:46.222 16073-16073/com.example.myapp D/JSInterface: getEval(): test()
05-25 13:34:50.224 16073-16073/com.example.myapp I/JSInterface: getEval(): Timed out waiting for response
05-25 13:34:50.224 16073-16073/com.example.myapp D/JSInterface: getEval() =
05-25 13:34:50.225 16073-16073/com.example.myapp I/Choreographer: Skipped 239 frames! The application may be doing too much work on its main thread.
05-25 13:34:50.235 16073-16150/com.example.myapp I/JSInterface: returning Success
05-25 13:34:50.237 16073-16150/com.example.myapp D/JSInterface: setValue(): Success
(16073 est 'main' ; 16150 est 'JavaBridge')
Comme vous pouvez le voir, le thread principal s'arrête en attendant que le processus de la WebView
d'appeler setValue()
ce qui n'est pas le cas jusqu'à ce que latch.await()
a expiré et l'exécution du fil principal a continué.
Il est intéressant de noter qu'en essayant avec un niveau d'API antérieur :
Emulator Nexus S API 14:
05-25 13:37:15.225 19458-19458/com.example.myapp D/JSInterface: getEval(): test()
05-25 13:37:15.235 19458-19543/com.example.myapp I/JSInterface: returning Success
05-25 13:37:15.235 19458-19543/com.example.myapp D/JSInterface: setValue(): Success
05-25 13:37:15.235 19458-19458/com.example.myapp D/JSInterface: getEval() = Success
05-25 13:37:15.235 19458-19458/com.example.myapp D/MyActivity: Success
(19458 est 'main' ; 19543 est 'JavaBridge')
Les choses fonctionnent correctement en séquence, avec getEval()
causant le WebView
d'appeler setValue()
qui sort ensuite latch.await()
avant qu'elle ne s'arrête (comme vous l'espérez).
(J'ai également essayé avec un niveau d'API encore plus ancien, mais les choses se sont plantées à cause de ce qui pourrait être, d'après ce que j'ai compris, un bug réservé aux émulateurs dans la 2.3.3 qui n'a jamais été corrigé).
Je suis donc un peu perdu. En creusant un peu, il semble que ce soit la bonne façon de faire les choses. Il est certain que semble comme l'approche correcte car elle fonctionne correctement sur le niveau 14 de l'API. Mais elle échoue sur les versions ultérieures - et j'ai testé sur 5.1 et 6.0 sans succès.