Android侧webview与Js通信的方式(1)

Android侧webview与Js通信的方式(1)

JsBridge原理介绍

Android侧JsBridge一般指 JsBridge,该框架对应ios侧的WebViewJavascriptBridge,两者的实现细节各有不同,但是总体原理一致。我们主要看一下其Js与Native通信原理的实现,对于具体的代码细节不做深究。

JsBridge集成

  • Js端

    集成源码中的js文件,WebViewJavascriptBridge.js,注意此处不可以通过注入的方式实现,不要被各种讲解博客误导。

  • Android侧

 dependencies {
   compile 'com.github.lzyzsd:jsbridge:1.0.4'
   }

Js调用Native

步骤

    1. js侧
function _doSend(message, responseCallback) {
          if (responseCallback) {
              //生成唯一callbackid用于标识该次jsbridge通信过程
              var callbackId = 'cb_' + (uniqueId++) + '_' + new  Date().getTime();
              responseCallbacks[callbackId] = responseCallback;
              message.callbackId = callbackId;
           }
           sendMessageQueue.push(message);
           //src:"yy://__QUEUE_MESSAGE__/"
           messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
           }
  • 2.native侧
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
      try {
          url = URLDecoder.decode(url, "UTF-8");
      } catch (UnsupportedEncodingException e) {
          e.printStackTrace();
      }

      if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
          webView.handlerReturnData(url);
          return true;
      } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
          webView.flushMessageQueue();
          return true;
      } else {
          return super.shouldOverrideUrlLoading(view, url);
      }
      }

这里会走第二个if, 调用BridgeWebView的flushMessageQueue()方法

   void flushMessageQueue() {
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new  CallBackFunction() {

                @Override
                public void onCallBack(String data) {
                    ...
                }
            });
        }
    }

在这个flushMessageQueue方法里, 如果当前是主线程就调用一个loadUrl方法

    public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
    
        // jsUrl = "javascript:WebViewJavascriptBridge._fetchQueue();"
        this.loadUrl(jsUrl);
        // 添加至 Map
        String functionName = BridgeUtil.parseFunctionName(jsUrl);
        // functionName = "_fetchQueue"
        responseCallbacks.put(functionName, returnCallback);
    }

在这个方法里, 首先会调用WebViewJavascriptBridge的_fetchQueue()方法, 然后解析方 法名字, 因为这里的方法名字是写死的, 其实就是_fetchQueue, 请记住这个名字, 因为后面会用到.然后将以这个_fetchQueue为key, 回调方法为value, 放到一个map里面.然后我们再去看js那端的方法.

  • 3.js侧
       // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
  function _fetchQueue() {
      var messageQueueString = JSON.stringify(sendMessageQueue);
      console.log('messageQueueString = ' + messageQueueString);
      sendMessageQueue = [];
      // android can't read directly the return data, so we can reload iframe src to communicate with java
      var src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
      messagingIframe.src = src;
  }
  • 4.native侧

    触发shouldOverrideUrlLoading方法,并走第一个if,触发handlerReturnData方法

        void handlerReturnData(String url) {
        // _fetchQueue
        String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
        //取出flushMessageQueue方法中放入responseCallbacks队列中的callback 
        CallBackFunction f = responseCallbacks.get(functionName);
        //取出js侧传来的数据
        String data = BridgeUtil.getDataFromReturnUrl(url);
        if (f != null) {
            //执行callback
            f.onCallBack(data);
            responseCallbacks.remove(functionName);
            return;
         }
        }

在看一下这个callback

         void flushMessageQueue() {
         if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

                @Override
                public void onCallBack(String data) {
                    // deserializeMessage 反序列化消息
                    List list = null;
                    try {
                        list = Message.toArrayList(data);
                    } catch (Exception e) {
                        e.printStackTrace();
                        return;
                    }
                    if (list == null || list.size() == 0) {
                        return;
                    }
                    for (int i = 0; i < list.size(); i++) {
                        Message m = list.get(i);
                        String responseId = m.getResponseId();
                        // 是否是response  CallBackFunction
                        if (!TextUtils.isEmpty(responseId)) {
                            CallBackFunction function = responseCallbacks.get(responseId);
                            String responseData = m.getResponseData();
                            function.onCallBack(responseData);
                            responseCallbacks.remove(responseId);
                        } else {
                            CallBackFunction responseFunction = null;
                            // if had callbackId 如果有回调Id
                            final String callbackId = m.getCallbackId();
                            
if (!TextUtils.isEmpty(callbackId)) { responseFunction = new CallBackFunction() { @Override public void onCallBack(String data) { Message responseMsg = new Message(); responseMsg.setResponseId(callbackId); responseMsg.setResponseData(data); queueMessage(responseMsg); } };
} else { responseFunction = new CallBackFunction() { @Override public void onCallBack(String data) { // do nothing } }; } // BridgeHandler执行 BridgeHandler handler; if (!TextUtils.isEmpty(m.getHandlerName())) { handler = messageHandlers.get(m.getHandlerName()); } else { handler = defaultHandler; } if (handler != null){ handler.handler(m.getData(), responseFunction); } } } } }); } }

首先将数据解析成一个Message的list, 这个Message是自定义的类, 里面包含两端协商好格式的信息,最后会执行到queueMessage(responseMsg)中

private void queueMessage(Message m) {
        if (startupMessage != null) {
            startupMessage.add(m);
        } else {
            dispatchMessage(m);
        }
    }

走dispatch方法

/**
     * 分发message 必须在主线程才分发成功
     * @param m Message
     */
    void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string  为json字符串转义特殊字符
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'");
        // javascript:WebViewJavascriptBridge._handleMessageFromNative('{\"responseData\":\"http:\\\/\\\/ww3.sinaimg.cn\\\/mw690\\\/96a29af5jw8fdfu43tnvlj20ro0rotab.jpg\",\"responseId\":\"cb_4_1532856634427\"}');
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        // 必须要找主线程才会将数据传递出去 --- 划重点
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
    }

首先将这个Message转化成json格式的字符串, 去掉一些特殊字符, 然后再主线程调用js方法, 方法是WebViewJavascriptBridge._handleMessageFromNative方法

  • 5.js侧
// 提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
    function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue && receiveMessageQueue.length > 0) {
            receiveMessageQueue.push(messageJSON);
        } else {
            _dispatchMessageFromNative(messageJSON);
        }
    }

_handleMessageFromNative方法会处理native传来数据,本次交互结束

  • 流程图

    avatar
  • 问题

看完JsBridge代码,可能大家都会有疑问,这一次流程中,为什么js侧与native侧为什么要交互两次,第一次其实并没有传任何有效数据过来,是否多余。下面我们着重看下这个问题。

Cordova方案参考

由于业界hybrid方案并不多,一般大厂的方案又较为复杂,而且网上资料基本没有任何对该问题的解释,因此本文参考了云闪付正在使用的hybrid方案cordova的通信逻辑。

Cordova方案相较Jsbridge方案更为重量级,十分复杂,因此本文并不做深入研究,仅针对其实现的Native、JS端通信逻辑进行研究。

ios侧

ios侧一般有两种方式,核心代码如下

if (bridgeMode === jsToNativeModes.WK_WEBVIEW_BINDING) {
        window.webkit.messageHandlers.cordova.postMessage(command);
    } else {
        // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
        // then the queue will be flushed when it returns; no need for a poke.
        // Also, if there is already a command in the queue, then we've already
        // poked the native side, so there is no reason to do so again.
        if (!isInContextOfEvalJs && commandQueue.length == 1) {
            switch (bridgeMode) {
            case jsToNativeModes.XHR_NO_PAYLOAD:
            case jsToNativeModes.XHR_WITH_PAYLOAD:
            case jsToNativeModes.XHR_OPTIONAL_PAYLOAD:
                pokeNativeViaXhr();    // 新建一个XMLHttpRequest,并发送一个HEAD请求,并将commondQueue以json串的形式放在请求头cmds上。
                break;
            default: // iframe-based.
                pokeNativeViaIframe(); // 创建iframe,通过hash值来传递commondQueue 或 execIframe.src = "gap://ready"
            }
        }
    }

可以看出有两种方式,一是新建一个XMLHttpRequest,并发送一个HEAD请求,并将commondQueue以json串的形式放在请求头cmds上。native侧进行拦截;二是创建iframe,通过hash值来传递commondQueue 或 execIframe.src = "gap://ready",与jsbridge一个原理。
ios端通过UIWebViewDelegate(iframe方式)或 NSURLProtocol拦截(xhr方式)方式接收到commondQueue后,执行插件的实际功能。
ios侧处理完后回消息给js侧也有两种方式一是通过UIWebView的stringByEvaluatingJavaScriptFromString方法,二是通过注入方式调用js侧iOSExec.nativeCallback方法。

Android侧

var messages = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson); // 
// If argsJson was received by Java as null, try again with the PROMPT bridge mode.
// This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2.  See CB-2666.
if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && messages === "@Null arguments.") {
  androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
  androidExec(success, fail, service, action, args);
  androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
  return;
} else {
  androidExec.processMessages(messages, true);
}
  1. 如果是JS_OBJECT方式,那么nativeApiProvider.get().exec= 安卓端源码中注解了@JavascriptInterface的 exec方法
  2. 如果是PROMPT方式,那么nativeApiProvider.get().exec 为如下方法:
exec: function(bridgeSecret, service, action, callbackId, argsJson) {
    return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));
  },

即通过promt()与native侧的onJsPromt()方法通信。

android侧回调回js侧也有两种方式,一是通过evaluateJavascript,二是通过loadurl。两种方式都是通过注入直接调用js侧androidExec.processMessages(messages, true)方法。

cordova总结

从以上分析可以看出,cordova不管在ios侧还是android侧,都是只通信一次。其中android侧js与native之间的通信使用了webview提供的多种api,接下来我们看一下这些api的特点及优劣。

交互方式总结

Android 端webview与Js通信的方式很多,要了解jsbridge两次通信是否合理,首先要了解下Android通过WebView与JS交互的方式。

  • 总体目录
avatar

Js主动调用Native

Js主动调Native主要有三种方式

  • 通过 WebView的addJavascriptInterface()(@JavascriptInterface)

    该方法通过addJavascriptInterface()将java对象映射到Js对象,js端直接调用即可,十分方便。但是该方法在Android4.2(17)之前有较大的安全漏洞,在Android <=4.1.2 (API 16),WebView使用WebKit浏览器引擎,并未正确限制addJavascriptInterface的使用方法,在应用权限范围内,攻击者可以通过Java反射机制实现任意命令执行。在Android >=4.2 (API 17),WebView使用Chromium浏览器引擎,并且限制了Javascript对Java对象方法的调用权限,只有声明了@JavascriptInterace注解的方法才能被Web页面调用。

    优点:使用简单
    缺点:API17之前有严重的安全漏洞

  • 通过 WebViewClient 的shouldOverrideUrlLoading ()拦截url

    1.js端通过修改iframe属性触发Android侧WebViewClient的回调方法shouldOverrideUrlLoading ()

    2.拦截、解析该 url 的协议

    3.如果检测到是预先约定好的协议,就调用相应方法

    优点:对Api无要求,不存在安全漏洞,较为通用

    缺点:需要js与native侧协商格式,JS获取Android方法的返回值复杂

  • 通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()拦截JS对话框alert()、confirm()、prompt()消息

    Android通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调分别拦截JS对话框 (即上述三个方法),得到他们的消息内容,然后解析即可。对比三个方法我们可以发现只有prompt()可以返回任意类型的值,操作最全面方便、更加灵活;而alert()对话框没有返回值;confirm()对话框只能返回两种状态(确定 / 取消)两个值,因此promt()方法较为合适

    avatar
  • 总结
    对比三种方式如下图

    image

    可以发现,利用WebChromeClient的onJsPrompt()方法拦截js侧的promt(),这种方式最合理

Native主动调用Js

  • 通过WebView的loadUrl(),及我们熟知的js注入

    通过webview的loadUrl()方法, mWebView.loadUrl("javascript:callJS()"),注意javascript为必加的前缀,callJS()为js对应方法名
    特别注意:
    1. JS代码调用一定要在 onPageFinished() 回调之后才能调用,否则不会调用。
    2. loadurl方法在url过长(2000个字符)时会失败,所以不要尝试将一些js文件通过注入的方式直接使用,What is the maximum length of a URL in different browsers?

    优点:对Api无要求,不存在安全漏洞,较为通用

    缺点:对注入代码长度有限制,且该方法执行会使页面刷新,并且无返回值

  • 通过WebView的evaluateJavascript()

    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
            //此处为 js 返回的结果
          }
        });
      }
    

    优点:1. 该方法的执行不会使页面刷新。
    2. 有返回值,效率更高、使用更简洁。

    缺点:1. 要求Android4.4以上
    2. onReceiveValue(String value),value会多一对引号,需要特殊处理

  • 总结

    avatar

你可能感兴趣的:(Android侧webview与Js通信的方式(1))