当前位置: 首页 > 工具软件 > Interface.js > 使用案例 >

跨平台实现基础(二)JavascriptInterface原理和Js和Java交互实践

姚善
2023-12-01

作者:Tamic 更多文章关注开发者技术前线

上一篇<JsCore 原理和实践>

前言

谷歌提供的系统组件WebView,用来加载和展现html网页,其采用webkit内核驱动,来实现网页浏览功能。 webkit的原理看上篇。Android从4.4 开始用Chromium动态加载实现网页渲染引擎。

Chromium 渲染引擎实际上是由Browser进程、Render进程和GPU进程组成的。其中,Browser进程负责将网页解析的UI合成 加速展现在页面上,Render 进程 只要负责加载和渲染网页,GPU 进程 负责 Browser 进程 和Render进程发出的GPU消息。

我们的创建WebView时候就会创建启动Chromium ,Chromium中有webCore引擎和JsCore引擎。

WebCore负责对HTML解析,CSS解析,渲染UI,调试信息等部分。

本篇介绍Android和JS交互几种方式,和基础核心类 JavascriptInterface。 JavascriptInterface是提供的一个JS引擎上一个接口,和WebView 一起 Android加载H5的重要组成部分。

1.1 WebView

Android Browser 用来主要用WebView来加载和渲染html网页,来实现网页浏览功能。 webkit的原理看上篇。

主要从网页的 URL 到构建完 DOM 树,接着 从 DOM 树到构建完 WebKit 的绘图上下文,从绘图上下文到生成最终的UI图像。

WebView 拥有load() URL和本地html文件的功能。

// 云端
webView.loadUrl("https://www.baidu.com"); 
// 本地
webView.loadUrl("file:///android_asset/demo.html");

1.2 WebViewClient

WebViewClient主要辅助WebView执行处理各种响应请求事件的,比如:

onLoadResource
onPageStart
onPageFinish
onReceiveError
onReceivedHttpAuthRequest
shouldOverrideUrlLoading

1.2.1 WebViewClient.shouldOverrideUrlLoading()

这个方法的本来是拦截所有WebView的Url跳转的。开发者可以通过这个API 构造一个特殊自定义格式的Url跳转,shouldOverrideUrlLoading拦截Url后判断其格式,然后Native 解析到对应的方法去 能执行本地方法的功能。

public class BirdgeClient extends WebViewClient {

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (isJsBridgeUrl(url)) {
             handle(url)//自定义的本地处理逻辑
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
}

这个类提供还提供了2个特别的函数。供Js和Java交互。

 1. WebChromeClient.onConsoleMessage()
    
 2. WebChromeClient.onJsPrompt()

1.2.2 onConsoleMessage()

Android系统 提供了 Javascript 在调试Native代码里面打印日志的API,同时这个API也成了其中一种Javascript与Native代码交互的方式。

在 Javascript 代码中调用 console.log(‘xxx’)方法()。

console.log('我是tamic')

Native代码会回调 WebChromeClient.consoleMessage()。

public class BridgeClient extends WebChromeClient {

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        super.onConsoleMessage(consoleMessage);
        String msg = consoleMessage.message();
    }
}

consoleMessage.message() 获得的正是Javascript代码console.log(‘tamic’)的内容。

开发中一般通过将consoleMessage 做约束,通过key和Json对象的字符串来做通讯数据格式。

1.2.3 onJsPrompt()

除了onJsPrompt(),还有onJsAlert() 和onJsConfirm()。顾名思义,这三个方法是Javascript给Native代码的回调接口的作用分别是提示信息,提示警告信息和展示确认信息。鉴于,alert和confirm在Javascript的使用率比较高,目前JSBridge框架 用的onJsPrompt()比较多。

JS中要三个API,alert(),confirm()和prompt() 用来个用户提示信息的。

JS中调用

window.prompt(message, value)

WebChromeClient.onJsPrompt()就会收到回调信息。

public classChromeClient extends WebChromeClient {

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 处理JS 的调用逻辑
        result.confirm();
        return true;
    }
}

onJsPrompt()的message的值正是JS的方法window.prompt()的message的值。
上面的代码块中的JsPromptResult是Js和native处理结果的重要部分。

JsPromptResult是对js结果的回调包装类。该类主要只负责处理Js回调结果。
该对象用作底层javascript发起请求的句柄,并为客户端提供指示是否应继续执行此操作的方法。此类的一个实例在在 webcromeclient操作中作为参数传递。
JsPromptResult提供了cancel()confirm() JsPromptResult 拦截ResultReceiver等。

ResultReceiver是结果接收器, 他是在AMS初始化时候创建的。 WebChromeClient 通过自己的quene()做消息循环。 实际是我们的WebViewChromium 实现用创建AwBrowserProcess的来执行将消息取出来。

我们可以借助这些API 巧妙的实现对js的数据的处理。

1.3 JavascriptInterface。

1.3.1 介绍

开发者都知道安卓API 4.4以前谷歌的webview存在安全漏洞,网站可以通过js注入就可以随便拿到客户端的重要信息,甚至轻而易举的调用本地代码进行流氓行为,谷歌后来发现有此漏洞后,增加了防御措施,如果要是js调用本地代码,开发者必须在代码申明JavascriptInterface,

源码如下:

@SuppressWarnings("javadoc")
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface JavascriptInterface {
}

列如在4.2之前我们要使得webView加载js只需如下代码:

mWebView.addJavascriptInterface(new JsBridge(), "JsBridge");  

JsBridge()是JavascriptInterface实现类,JsBridge是我们给的别名,也就是Js能难道的java端的桥对象。

4.2之后调用需要在调用方法加入加入@JavascriptInterface注解,如果代码无此申明,那么也就无法使得js生效,也就是说这样就可以避免恶意网页利用js对android客户端的窃取和攻击。

即使这样,我们很多时候需要在js加载本地代码的时候,要做一些判断和限制,或者有可能也会做些过滤和对用户友好提示!

android中的Js和Java的通讯主要流程

addJavascriptInterface 实际上由 WebViewProvider 来执行实现,WebViewProvider主要负责提供WebView后台程序接口, 每个WebView对象仅绑定到一个实现。

public void addJavascriptInterface(Object obj, String interfaceName);

public void removeJavascriptInterface(String interfaceName)

;

第一步addJavascriptInterface时候 只有Browser 进程才能有权限,否则会抛异常

源码部分:

private void checkThread() {
        
        if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) {
            Throwable throwable = new Throwable(
                    "A WebView method was called on thread '" +
                    Thread.currentThread().getName() + "'. " +
                    "All WebView methods must be called on the same thread. " +
                    "(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() +
                    ", FYI main Looper is " + Looper.getMainLooper() + ")");
            Log.w(LOGTAG, Log.getStackTraceString(throwable));
            StrictMode.onWebViewMethodCalledOnWrongThread(throwable);

            if (sEnforceThreadChecking) {
                throw new RuntimeException(throwable);
            }
        }
    }

第二步
WebViewProvider提供的这两个抽象方法实际上是WebViewFactoryProvider创建出来的具体WebViewChromiumFactoryProvider实现的。

第三步 WebViewChromiumFactoryProvider创建WebViewChromium去实现。

源码:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......
 
    public void init(final Map<String, Object> javaScriptInterfaces,
            final boolean privateBrowsing) {
        ......
 
        // We will defer real initialization until we know which thread to do it on, unless:
        // - we are on the main thread already (common case),
        // - the app is targeting >= JB MR2, in which case checkThread enforces that all usage
        //   comes from a single thread. (Note in JB MR2 this exception was in WebView.java).
        if (mAppTargetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mFactory.startYourEngines(false);
            checkThread();
        } else if (!mFactory.hasStarted()) {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                mFactory.startYourEngines(true);
            }
        }
 
        ......
 
        mRunQueue.addTask(new Runnable() {
                @Override
                public void run() {
                    initForReal();
                    ......
                }
        });
    }
 
    ......
}

第四步。 WebViewChromium 实现用创建AwBrowserProcess的来执行BrowserStartupController的执行Js的函数,这个函数是个JNI方法,


static void SetCommandLineFlags(JNIEnv* env,
                                jclass clazz,
                                jint max_render_process_count,
                                jstring plugin_descriptor) {
  std::string plugin_str =
      (plugin_descriptor == NULL
           ? std::string()
           : base::android::ConvertJavaStringToUTF8(env, plugin_descriptor));
  SetContentCommandLineFlags(max_render_process_count, plugin_str);

1.3.2 使用案例

JavascriptInterface 添加必须要准备一个JavascriptInterface的注解方法,开发者可准备一个简单的JsBridge :

 public class JsBridge {

        @JavascriptInterface
        public void showToast(String toast) {
            Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();

            if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
                log("show toast sucess");
            } else {
                log("show toast error");
            }
        }


        public void log(final String msg) {
            javaCallJS("log", msg);
           /* webView.post(new Runnable() {
                @Override
                public void run() {
                    webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
                }
            });*/
        }

        /**
         * js方法名 參數
         * @param method
         * @param parameter
         */
        public void javaCallJS(String method, String parameter) {
            webView.loadUrl("javascript:" + method + "('" + parameter + "')");
        }
    }

android页面通过webView. addJavascriptInterface中加入绑定桥。

WebSettings webSettings = webView.getSettings();

        webSettings.setJavaScriptEnabled(true);

        webView.addJavascriptInterface(new JsBridge(), "JsBridge");

        webView.loadUrl("url");

Js调Java

在js调Java弹一个toast, Js代码

 <script type="text/javascript">
        function showToast(toast) {
            javascript:jsbrige.showToast(toast);
        }
       
    </script>

接着 可以js可以通过javascript:jsbrige.showToast ()。来调用上面准备好的桥实现。这样就完成了js调用java的toast功能。

Java调JS

我们在Native中通过返回toast是否是成功的案列给Js给一个返回值。接着定义一个通用实现;

 public void javaCallJS(String method, String parameter) {
            webView.loadUrl("javascript:" + method + "('" + parameter + "')");
        }

假如js中有一个Log方法如下:

 function log(msg){
            console.log(msg);
        }

接着通过判断当前线程是否是UI线程来告知js是否执行Toast 成功。

 @JavascriptInterface
        public void showToast(String toast) {
            
            if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
                Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
                log("show toast sucess");
            } else {
                log("show toast error");
            }

        }

总结

本节主要学习了Js和natvie交互三种方式。其实上面介绍的案列就是一个简单的jsBridge的雏形,当然目前企业级的JsBridge 不止这简单,接下来下篇学习webVIew后,再给大家介绍Jsbrige的实现原理。

 类似资料: