查看原文
其他

项目必备功能之 JsBridge源码解析

二两五花肉 鸿洋 2019-04-05

本文作者


作者:二两五花肉

链接:

https://kaelinvoker.github.io/Blog/

本文由作者授权发布。


app的快速迭代,离不开h5的支持。而如何解决Js和Native的通信问题,就需要JsBridge来解决了。本文主要是对第三方库JsBridge的源码解析。


项目地址

https://github.com/lzyzsd/JsBridge


1Bridge基本原理


Js通知Native


Js通知Native目前有三种方案。


  1. API注入。通过webview.addJavascriptInterface()的方法实现。

  2. 拦截Js的alert/confirm/prompt/console等事件。由于prompt事件在js中很少使用,所以一般是拦截该事件。这些事件在WebChromeClient都有对应的方法回调(onConsoleMessage,onJsPrompt,onJsAlert,onJsConfirm)

  3. url跳转拦截,对应WebViewClient的shouldOverrideUrlLoading()方法。


第一种方法,由于webview在4.2以下的安全问题,所以有版本兼容问题。后两种方法原理本质上是一样的,都是通过对webview信息冒泡传递的拦截,通过定制协议-拦截协议-解析方法名参数-执行方法-回调。


Native通知Js


webview可以通过loadUrl()的方法直接调用。在4.4以上还可以通过evaluateJavascript()方法获取js方法的返回值。


4.4以前,如果想获取方法的返回值,就需要通过上面的对webview信息冒泡传递拦截的方式来实现。


2JsBridge源码解析


我们项目是采用url跳转拦截的方式实现Native与Js的通信。


JsBridge的接入


JsBridge的接入分为两部分,H5端的接入,客户端的接入。


客户端的接入。


BridgeWebView这个类


//js注入的文件名    
String toLoadJs = "WebViewJavascriptBridge.js";
//白名单的实现接口 由外部自己实现
private IBridgeAccept mAccept;

public void onPageFinished(WebView view, String url) {
    //String toLoadJs = "WebViewJavascriptBridge.js"; 
     if (mAccept != null && mAccept.accept(url) && toLoadJs != null ) {
        BridgeUtil.webViewLoadLocalJs(view, toLoadJs);
    }
}


BridgeUtil的webViewLoadLocalJs方法


public static void webViewLoadLocalJs(WebView view, String path) {
    //如果为空,则去assets目录下读取js文件
    if (null == jsContent) {
        jsContent = assetFile2Str(view.getContext(), path);
    }
    view.loadUrl("javascript:" + jsContent);
}


从上面的代码可以看出,JsBridge实现的JS部分代码也是放在了客户端的assets目录下。在页面加载完成后,由客户端主动注入。并且这里存在域名白名单机制,只有在白名单内才会注入js代码。


H5端的接入


由于客户端注入js是异步的,H5调用方法时,必须要确保js代码注入成功。因此必须要监听注入事件,成功后通知H5。


注入的WebViewJavascriptBridge.js部分代码:


//自定义jsBridge初始化事件WebViewJavascriptBridgeReady,并在最后主动触发事件
//通知h5jsBridge初始化完毕
var doc = document;,
_createQueueReadyIframe(doc);
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('WebViewJavascriptBridgeReady');
readyEvent.bridge = WebViewJavascriptBridge;
doc.dispatchEvent(readyEvent);


在h5调用Bridge方法时,需要监听Bridge初始化事件


//已经初始化了
if (window.WebViewJavascriptBridge) {
     //do your work here
else {
    //监听初始化事件
    document.addEventListener(
        'WebViewJavascriptBridgeReady'function() {
              //do your work here
          }, false);
}


3Native与Js通信的流程


Native调用Js


先看一张我画的流程图



举个例子:


webview.callHandler("functionInJs""哈哈我是java传来的"new CallBackFunction() {

              @Override
              public void onCallBack(String data) {
                  Log.e("MainActivity""reponse data from js " + data);
              }

              @Override
              public void onFailed(String data) {
                  super.onFailed(data);
                  Log.e("MainActivity""onFailed data from js " + data);
              }
  });


看下这个方法做了什么:


//java调用Js
//handlerName是Js提前注册的方法名 data是方法的参数
//callBack 是java的回调对象
public void callHandler(String handlerName, String data,
                            CallBackFunction callBack) 
{
   doSend(handlerName, data, callBack);
}


这里是流程的入口,native调用js提前预注册的方法,这里注意方法名一定要和JS预定义的方法名完全相同,才会调用成功。


private void doSend(String handlerName, String data,
                        CallBackFunction responseCallback) {
        //将要调用的js方法名和参数封装成Message
        Message m = new Message();
        if (!TextUtils.isEmpty(data)) {
            m.setData(data);
        }
        //如果java需要回调,生成唯一的回调id,放到message中
        //并且将对应的java回调保存到 responseCallbacks中,以callbackId为键
        if (responseCallback != null) {
            String callbackStr = String.format(
                    BridgeUtil.CALLBACK_ID_FORMAT,
                    ++uniqueId
                            + (BridgeUtil.UNDERLINE_STR + SystemClock
                            .currentThreadTimeMillis()));
            responseCallbacks.put(callbackStr, responseCallback);
            m.setCallbackId(callbackStr);
        }

        if (!TextUtils.isEmpty(handlerName)) {
            m.setHandlerName(handlerName);
        }

        queueMessage(m);
}


在doSend函数中,主要做的就是将方法名、参数、回调id封装message。


private void queueMessage(Message m) {
  //startupMessage在页面第一次加载完成就会置空,所以这里一定会走到dispatchMessage中
   if (startupMessage != null) {
       startupMessage.add(m);
    } else {
       dispatchMessage(m);
    }
}

 private void dispatchMessage(Message m) {
    //message转成json,并进行转义
    String messageJson = m.toJson();
    messageJson = messageJson.replaceAll("\\\\/""/");
    messageJson = messageJson.replaceAll("(\\\\)([^utrn])""\\\\\\\\$1$2");
    messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")""\\\\\"");
    //将转义好的消息json串传递到js的_handleMessageFromNative方法中
    //并在主线程中调用
    String javascriptCommand = String.format(
            BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
    messageJson = null;
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        WYLogUtils.i(TAG, "dispatchMessage --> " + javascriptCommand);
        this.loadUrl(javascriptCommand);
    }
}


这里的BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA是js的方法名,native可以通过loadUrl()的方式调用


final static String JS_HANDLE_MESSAGE_FROM_JAVA  
    = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";


到这里就把要调用的方法名,参数,是否需要回调的消息就传给了js


再看下js的_handleMessageFromNative做了什么:


function _handleMessageFromNative(messageJSON) {
    //receiveMessageQueue在页面加载完成后就赋值为null了
    //所以最后都会到_dispatchMessageFromNative中
    if (receiveMessageQueue) {
        receiveMessageQueue.push(messageJSON);
    } else {
        _dispatchMessageFromNative(messageJSON);
    }
}

 function _dispatchMessageFromNative(messageJSON) {
    setTimeout(function() {
    //将传递的消息json串转为message对象    
    var message = JSON.parse(messageJSON);
    var responseCallback;
     //java调用js 并没有responseId 所以走else

     //这里是js调用java并且js有回调,这里表明java将js回调需要的数据传过来了
     //此时再进行js 回调处理
      if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                //直接发送 
                ////这里如果有回调id, 说明前面java需要js回传数据
                //构造回调对象
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }
            //从js预定义的方法集合messageHandlers中,匹配传递过来消息中方法名
                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName];
                }
             //匹配成功后,调用该方法,传递数据,并且将回调对象作为参数也传递了
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if(responseCallback){
                        responseCallback({error:404,errorMessage:"JS API not find"})
                    }
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
    }


这里的messageHandlers是从哪来的?看下面的代码


//定义方法集对象
var messageHandlers = {};

//js注册方法时,其实就是以注册的方法名为key, 具体的方法对象为值存储到messageHandler中
function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}


到这里,native已经成功调用js的方法了。但是如果java还需要js回传数据的话,那么在js注册的方法中,需要主动使用上面传递进参数的回调对象调用其方法才可以完成。

如这样。


//js 注册了一个名为functionInJs的方法 这里的responseCallback就是传递的回调对象
bridge.registerHandler("functionInJs"function(data, responseCallback) {
    document.getElementById("show").innerHTML = ("data from Java: = " + data);
    /因为java不一定需要js回传数据,只有需要的时候,才会传入该对象,所以这里判空
    if (responseCallback) {
         var responseData = {data:'Javascript Says Right back aka!'};
         //使用该回调对象主动调用其方法
          responseCallback(responseData);
     }
});


主动调用该方法后,我看回看上面的代码。


if (message.callbackId) {
     var callbackResponseId = message.callbackId;
    //其实就是调用该方法
     responseCallback = function(responseData) {
        _doSend({
           responseId: callbackResponseId,
           responseData: responseData
           });
      };
}


上面分析过,这里是如果java需要js回传数据,也就是message中有callbackId,会创建回调对象。而这时h5端主动调用该回调的对象的方法,其实就是走到了这里。会把回传的数据和该callbackId封装成message对象,而callbackId这时就成了responseId。这里其实也是为了在java端能够根据id取出最初存入的回调。


function _doSend(message, responseCallback) {
   //这时的responseCallback为null
  if (responseCallback) {
     var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
     responseCallbacks[callbackId] = responseCallback;
     message.callbackId = callbackId;
  }
  //将上步的message放入sendMessageQueue发送消息队列中
  sendMessageQueue.push(message);
  //改变iframe的src从而通知native从h5取消息
  messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
 }


这里的messagingIframe是什么呢?看注入的js代码。


//声明iframe元素
var messagingIframe;
//创建不可见的的ifrmae
function _createQueueReadyIframe(doc) {
    messagingIframe = doc.createElement('iframe');
    messagingIframe.style.display = 'none';
    doc.documentElement.appendChild(messagingIframe);
}


通过改变iframe的src会触发WebViewClient的shouldOverrideUrlLoading()。这里可以看到上面将iframe的src改为yy://__QUEUE_MESSAGE__/,通知Native去取消息。


//通知Native有消息的协议
yy://__QUEUE_MESSAGE__/


下面看看Native做了什么


@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    WYLogUtils.i(TAG, "shouldOverrideUrlLoading --> " + url);
    //yy://return/{function}/returncontent || yy://"
    //这里开始拦截协定的协议
    if (url.startsWith(BridgeUtil.YY_RETURN_DATA) || url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
        //解码
        try {
            url = URLDecoder.decode(url, "UTF-8");
        } catch (IllegalArgumentException e) { //解决未编码同时内容中有%时奔溃的问题
            WYLogUtils.e(TAG, e.getMessage(), e);
            if (url.contains("%%")) {
                try {
                    url = URLDecoder.decode(removeDoublePercent(url), "UTF-8");
                } catch (Exception e1) {
                    WYLogUtils.e(TAG, e1.getMessage(), e1);
                }
            }
        } catch (Exception e) {
            WYLogUtils.e(TAG, e.getMessage(), e);
        }
        //yy://return/{function}/returncontent || yy://
        //这里由于上面是为yy://__QUEUE_MESSAGE__/ 所以会走到flushMessageQueue()中
        if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {// 如果是返回数据
            handlerReturnData(url);
        } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
            flushMessageQueue();
        }
        return true;
    }
    ...
}


这里拦截到预定的协议后,调用了flushMessageQueue()


private void flushMessageQueue() {
     //这里在主线程
     if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        //这里往loadUrl方法中传了一个字符串和一个新的回调对象
        loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA,new CallBackFunction() {
                @Override
                public void onCallBack(String data) {
                ...
            }
}


再看下BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA是什么


//这是个js方法 
final static String JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();";


再看下这里的loadUrl(String jsUrl, CallBackFunction returnCallback)方法做了什么


private void loadUrl(String jsUrl, CallBackFunction returnCallback) {
     //主线程执行了js的_fetchQueue方法
     this.loadUrl(jsUrl);
     responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl),
             returnCallback);
 }
 


看下BridgeUtil.parseFunctionName(jsUrl)是什么


//从字符串中解析出js的方法名
public static String parseFunctionName(String jsUrl) {
        return jsUrl.replace("javascript:WebViewJavascriptBridge.",  "").replaceAll("\\(.*\\);""");
}


所以在loadUrl方法中做了两件事。第一执行_fetchQueue方法,第二以_fetchQueue为key,将上面创建的回调对象为值,存到了responseCallbacks中。


下面看看_fetchQueue方法


function _fetchQueue() {
    //将上面要回传给native的消息sendMessageQueue转为json
    //所有的消息
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    //这里肯定是Android Ios多余 用的不是一个框架
    if(isAndroid()){
     //通知Native过来拿消息。 
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + messageQueueString;
    }else if (isIphone()) {
        return messageQueueString;
//android can't read directly the return data, so we can reload iframe src to communicate with java
    }
}


这里将要回传给native的消息转为json,放到iframe的src中,从而通知native取消息。


//返回数据的协议
yy:///return/_fetchQueue/+json


触发WebViewClient的shouldOverrideUrlLoading(),拦截该url,并调用handlerReturnData(url)


final static String YY_OVERRIDE_SCHEMA = "yy://";    
final static String YY_RETURN_DATA = YY_OVERRIDE_SCHEMA + "return/";/

//shouldOverrideUrlLoading方法中的代码片段
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {// 如果是返回数据
    handlerReturnData(url);
else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
    flushMessageQueue();
}


handlerReturnData方法中处理回传的数据,并执行前面存入回调对象的方法。


private void handlerReturnData(String url) {
    //functionName: _fetchQueue
    //以_fetchQueue为key从responseCallbacks中取出上面存入的回调对象
    String           functionName = BridgeUtil.getFunctionFromReturnUrl(url);
    CallBackFunction f            = responseCallbacks.get(functionName);
    //从回传数据中解析出回传的数据
    String data = BridgeUtil.getDataFromReturnUrl(url);
    if (f != null) {
        //执行回调对象的方法,并传入数据
        f.onCallBack(data);
        //移除回调
        responseCallbacks.remove(functionName);
    }
}


回调对象的方法看看做了什么


loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA,
            new CallBackFunction() {

            @Override
                public void onCallBack(String data) {
                List<Message> list = null;
                try {
                    //将回传的data转成Message集合
                    list = Message.toArrayList(data);
                } catch (Exception e) {
                    WYLogUtils.e(TAG, e.getMessage(), e);
                }
                if (list == null || list.size() == 0) {
                    return;
                }
//由于在js _fetchQueue方法中是将整个消息队列发送过来 遍历集合,这里面有我们分析的那条消息
                for (int i = 0; i < list.size(); i++) {
                    Message m = list.get(i);
                    //分析的那条消息带有responseId
                    String responseId = m.getResponseId();
                    //说明java调用了js 且需要js回传数据
                     if (!TextUtils.isEmpty(responseId)) {
    //而这个responseId其实就是一开始java需要回调时生成的callbackId
    //根据这个callbackId从responseCallbacks取出最开始存入的回调对象 并且执行该回调方法
                        CallBackFunction function = responseCallbacks
                                        .get(responseId)
;
                        String responseData = m.getResponseData();
                        function.onInnerCallBack(responseData);
                        //执行后移除该回调对象
                        responseCallbacks.remove(responseId);
                      }else{
                        .....   
                      } 
                }
            }
    }
);


再看下回调对象这个类


public abstract class CallBackFunction {
    //执行回调的方法
    public void onInnerCallBack(String data){
        //将js回传的消息转成json,并解析error属性,默认是200表明调用js方法成功
        //如果解析出404表明调用js方法失败,
        if(data != null && data.length()>0){
            try {
                JSONObject obj = new JSONObject(data);
                int resultCode = obj.optInt("error",200);
                if(resultCode == 404){
                    onFailed(data);
                }else{  
                    onCallBack(data);
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }else{
            onCallBack(data);
        }

    }

    //抽象方法,native注册方法时的回调对象的回调方法
    public abstract void onCallBack(String data);

    public void onFailed(String data){

    }
}


到这里Native调用js分析完毕。从整个流程来看,Native消息通知JS很方便,直接调用就好了。而Js由于不能直接通知给Native,所以相对来说比较绕,得先通过协议拦截的方式通知Native JS有消息要传递,然后Native再主动调用Js的方法,将Js要传递的消息再通过协议拦截的方式传递给Native。


4Js调用Native


还是看张我画的流程图。



js调用Native的流程其实和Native调用Js基本是相似的,所以这里就简单介绍了。


依然是举个例子


window.WebViewJavascriptBridge.callHandler(
    'callNative'
    , {'param''哈哈哈哈'}
    , function(responseData) {
        document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
    }
 );


js调用callHandler方法,将要调用的方法名和参数以及回调对象传入doSend()方法中。


function _doSend(message, responseCallback) {
    //js如果需要回调 生成callbackId与方法名,参数封装成message
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        //将Callback保存到responseCallbacks 以callbackId为key
        responseCallbacks[callbackId] = responseCallback;
        message.callbackId = callbackId;
    }
    //将message放到消息队列中
    sendMessageQueue.push(message);
    //通知Native过来取消息
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}


native拦截该协议,会调用flushMessageQueue,这里和上面Java调用js一样。webview通过loadUrl的方式,调用js的_fetchQueue方法。同时生成回调对象,保存到responseCallbacks中。


f_fetchQueue方法上面也分析过了,会将js的消息队列转为json放到iframe的src中,然后再次通知Native过来取消息。


Native拦截收到消息后,调用handlerReturnData()方法。从responseCallbacks取出上面注册的回调对象,调用其方法,并移除该回调对象。


下面看回调方法里面做了什么。


loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA,
            new CallBackFunction() {

           @Override
         public void onCallBack(String data) {
            //这里和前文一样,取出js传过来的消息
            //将消息转为message集合 遍历集合
            ....
             for (int i = 0; i < list.size(); i++) {
                Message m = list.get(i);        
                //这里和前文一样 由于这次js传过来的消息里并没有responseId 所以直接看else
                String responseId = m.getResponseId();
                if (!TextUtils.isEmpty(responseId)) {
                    ...
                }else{
                    //js 调用了 java
                    //如果有callbackId,说明js要回传数据
                    //创建回调函数
                    CallBackFunction responseFunction = null;
                    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) {
                                //no-op
                            }
                        }
                    }
                    //从消息中取出方法名去匹配native提前的注册的方法池
                    //如果存在就取出方法对象并回调该方法。
                    BridgeHandler handler;
                    if (!TextUtils.isEmpty(m.getHandlerName())) {
                        handler = messageHandlers.get(m.getHandlerName());
                        if (handler == null) {
                            handler = defaultHandler;
                        }
                    } else {
                        handler = defaultHandler;
                    }   
                    handler.handler(specialCharacterReplace(m.getData()), responseFunction);
                }
        }
    });


下面再分析java数据如何回传的。上面已经分析了,如果js需要Java回传数据,是会创建一个回调对象,并传入的要调用的Handler的方法中。java端就可以使用该对象调用其方法来进行回传数据。


看看该方法做了什么。


//将要回传的数据封装成message,原先从js传过来的callbackId变成responseId
Message responseMsg = new Message();
//设置responseId
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);


继而调用queueMessage()方法,最终会调到dispatchMessage。这里就和前文分析的一样,会将传过来的message转成json,再进行转义,随后在主线程调用JS的_handleMessageFromNative方法,并将json传递过去。


_handleMessageFromNative()方法又会调用_dispatchMessageFromNative()方法。


function _dispatchMessageFromNative(messageJSON) {
    setTimeout(function() {
        var message = JSON.parse(messageJSON);
        var responseCallback;
        //java call finished, now need to call js callback function
        //js 调用 java 并且js有回调,这里表明 java将js回调需要的数据传过来了 此时再进行js 回调处理
        if (message.responseId) {
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseId];
        } else {
            ...
        }
    });
}


至此,Js调用Native完毕。


5Bridge框架问题


分析源码后,可以看到Bridge目前存在以下问题


  1. Js调用java偶现调用失败。因为通过iframe的机制并不能保证shouldOverrideUrlLoading每次都会调用。

  2. 部分手机java调用js偶现失败。


推测有两个原因:


  1. webview在调用js方法时,只在主线程调用。如果不在主线程的时候,就不会调用了;

  2. Js注入的时机问题。在OnPageFinished中注入虽然最后都会全局注入成功,但是完成时间有可能太晚。


推荐阅读

2018年终总结(兼个人详历)

我的2018年终总结(进阶之路)

这么多性能优化工具,你都会了么?



扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存