Android主线程阻塞WebView线程

时间:2021-10-05 20:59:22

I've been working on a problem with doing a synchronous call to JavaScript in a WebView (with a return value) and trying to narrow down the where and why of why it's not working. It seems to be that the WebView thread is blocking while the main thread is waiting for a response from it -- which shouldn't be the case since theWebView runs on a separate thread.

我一直在努力解决在WebView中对JavaScript进行同步调用(带有返回值)的问题,并尝试缩小为什么它不起作用的地方和原因。似乎WebView线程在主线程等待来自它的响应时阻塞 - 由于WebView在单独的线程上运行,因此不应该这样。

I've put together this small sample that demonstrates it (I hope) fairly clearly:

我把这个小样本放在一起,相当清楚地展示了它(我希望):

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

When running, the following results:

运行时,结果如下:

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 is 'main'; 16150 is 'JavaBridge')

(16073是'主要'; 16150是'JavaBridge')

As you can see, the main thread times out waiting for theWebView to call setValue(), which it doesn't until latch.await() has timed out and main thread execution has continued.

正如您所看到的,主线程超时等待WebView调用setValue(),直到latch.await()超时并且主线程执行仍在继续。

Interestingly, trying with an earlier API level:

有趣的是,尝试使用早期的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 is 'main'; 19543 is 'JavaBridge')

(19458是'主'; 19543是'JavaBridge')

Things work correctly in sequence, with getEval() causing the WebView to call setValue(), which then exits latch.await() before it times out (as you'd expect/hope).

事情按顺序正常工作,getEval()导致WebView调用setValue(),然后在超时之前退出latch.await()(如你所料/希望的那样)。

(I've also tried with an even earlier API level, but things crash out due to what may be, as I understand it, an emulator-only bug in 2.3.3 that never got fixed.)

(我也尝试过更早的API级别,但事情已经崩溃了,因为据我所知,2.3.3中一个从未得到修复的模拟器错误。)

So I'm at a bit of a loss. In digging around, this seems like the correct approach to doing things. It certainly seems like the correct approach because it works properly on API level 14. But then it's failing on later versions — and I've tested on 5.1 and 6.0 without success.

所以我有点失落。在挖掘中,这似乎是正确的做事方法。它看起来似乎是正确的方法,因为它在API级别14上正常工作。但是后来的版本失败了 - 我在5.1和6.0上测试没有成功。

1 个解决方案

#1


1  

Look more about migration WebView with Android 4.4. See description on Android Docs I think you need to use another method for funning your JS action.

了解有关使用Android 4.4迁移WebView的更多信息。请参阅Android Docs上的说明我认为您需要使用另一种方法来娱乐您的JS操作。

For example, base on that doc - Running JS Async Asynchronously evaluates JavaScript in the context of the currently displayed page. If non-null, |resultCallback| will be invoked with any result returned from that execution. This method must be called on the UI thread and the callback will be made on the UI thread.

例如,基于该文档 - 运行JS异步异步地在当前显示的页面的上下文中评估JavaScript。如果非null,| resultCallback |将使用该执行返回的任何结果调用。必须在UI线程上调用此方法,并且将在UI线程上进行回调。

#1


1  

Look more about migration WebView with Android 4.4. See description on Android Docs I think you need to use another method for funning your JS action.

了解有关使用Android 4.4迁移WebView的更多信息。请参阅Android Docs上的说明我认为您需要使用另一种方法来娱乐您的JS操作。

For example, base on that doc - Running JS Async Asynchronously evaluates JavaScript in the context of the currently displayed page. If non-null, |resultCallback| will be invoked with any result returned from that execution. This method must be called on the UI thread and the callback will be made on the UI thread.

例如,基于该文档 - 运行JS异步异步地在当前显示的页面的上下文中评估JavaScript。如果非null,| resultCallback |将使用该执行返回的任何结果调用。必须在UI线程上调用此方法,并且将在UI线程上进行回调。