Основной поток Android блокирует поток WebView

Я работал над проблемой с синхронным вызовом JavaScript вWebView (с возвращаемым значением) и пытается выяснить, где и почему, почему это не работает. Похоже, чтоWebView поток блокируется, в то время как основной поток ожидает ответа от него - что не должно быть, так какWebView работает в отдельном потоке.

Я собрал этот небольшой образец, который демонстрирует это (я надеюсь) довольно ясно:

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>

MyActivity.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);
        }
    }
}

При запуске получаются следующие результаты:

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 - это «главное»; 16150 - это «JavaBridge»)

Как вы можете видеть, время ожидания основного потока ожиданияWebView звонитьsetValue()что это не доlatch.await() истекло время ожидания, и выполнение основного потока продолжилось.

Интересно, пытаясь с более ранним уровнем API:

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 - «главный»; 19543 - «JavaBridge»)

Вещи работают правильно в последовательности, сgetEval() вызываяWebView звонитьsetValue(), который затем выходитlatch.await() до истечения времени ожидания (как и следовало ожидать / надежда).

(Я также пробовал с более ранним уровнем API, но все вылетает из-за того, что, как я понимаю, может быть ошибка только в эмуляторе в 2.3.3, которая так и не была исправлена.)

Так что я немного растерялся. При копании вокруг это кажется правильным подходом к выполнению задач. Это конечнокажется как правильный подход, потому что он работает должным образом на уровне API 14. Но затем он терпит неудачу на более поздних версиях - и я проверил на 5.1 и 6.0 без успеха.

Ответы на вопрос(1)

Ваш ответ на вопрос