当前位置:   article > 正文

JsBridge源码详解(一) JS与Native通讯过程(附详细流程图)_js bridge 时间触发流程图

js bridge 时间触发流程图

JsBridge(github地址)为混合式应用native与h5的通讯提供安全而方便的桥接。

JsBridge是通过url拦截的方式实现的。

本次源码解析分为三篇,本篇为源码分析第一篇,其他两篇请见:
android JS与Native通讯方案汇总
JsBridge源码详解(一) JS与Native通讯过程(附详细流程图)
JsBridge源码详解(二) Native与JS通讯过程(附详细流程图)


由于框架功能分为js与native通讯和native与js通讯两部分,所以分两个流程分别来分析,本篇是源码解析的第一篇,先看js与native通讯部分。

通讯前准备

通讯的实现需要注入一段js代码,js代码的注入在页面加载完毕也就是WebViewClent的onPageFinished方法中

@Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(mWebView, url);
        //加载本地通讯桥接的js文件
        JsBridgeUtil.webViewLoadLocalJs(view, BridgeX5WebView.toLoadJs);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

其中JsBridgeUtil的webViewLoadLocalJs方法负责注入js片段,其代码如下

public static void webViewLoadLocalJs(WebView view, String path) {
        String jsContent = assetFile2Str(view.getContext(), path);
        view.loadUrl("javascript:" + jsContent);
    }
  • 1
  • 2
  • 3
  • 4

负责通讯的js片段以文件形式放在asserts目录下,assetFile2Str将文件以流的方式读取出来转换成字符串格式,然后通过loadUrl就加载了通讯的js片段,js的具体内容下面再一一分析。

至此,通讯的准备工作就完成了,下面来看看具体的通讯实现。

通讯过程

js调用native有个前提条件,就是上面讲到的js片段必须注入完毕,所以这里有部分逻辑是对该部分准备工作的监听,负责通讯的js通过发送事件的方式告诉调用方js:

 var readyEvent = doc.createEvent('Events');
    readyEvent.initEvent('WebViewJavascriptBridgeReady');
    readyEvent.bridge = WebViewJavascriptBridge;
    doc.dispatchEvent(readyEvent);
  • 1
  • 2
  • 3
  • 4

调用方js可以通过如下方式来监听该事件

function connectWebViewJavascriptBridge(callback) {
            if (window.WebViewJavascriptBridge) {
                // 准备完毕,可以通讯了
            } else {
                document.addEventListener(
                    'WebViewJavascriptBridgeReady'
                    , function() {
                        // 准备完毕,可以通讯了
                    },
                    false
                );
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

监听到桥js加载完毕(步骤1),接下来就可以通讯了,调用方js通过调用桥js的callHandler函数来发起通讯(步骤2),来看一个获取H5所运行的设备的信息例子:

function getDeviceInfo(){
             window.WebViewJavascriptBridge.callHandler(
                'NativeHandler'
                , {'funcName'  : 'getDeviceInfo'}
                , function(responseData) {
                    document.getElementById("show").innerHTML = "getDeviceInfo involved,responseData from native is " + responseData
                }
            );
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

各个参数的作用后面会讲到,先来看一下callHandler的实现

function callHandler(handlerName, data, responseCallback) {
        _doSend({
            handlerName: handlerName,
            data: data
        }, responseCallback);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

callHandler调用了内部doSend方法

 function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message.callbackId = callbackId;
        }
        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

dosend方法先是判断是否需要有结果回调,如果有则生成唯一的回调id并将该id与回调方法一起保存进map以便调用结束后通知回调方法。回调id的生成用一个自增长的整数和当前时间戳组成。唯一的id会被放进传递给native的message对象中,使得native执行完毕之后可以告诉js应该将结果返回给哪个id对应的回调方法。

需要注意的是,dosend的方法并没有直接将调用者发送来的数据告诉native,而是将message对象保存进待发送消息队列sendMessageQueue,然后改变iframe标签的src来通知native有消息要取,这是怎么做到的呢?

首先这里要简单介绍下iframe:

iframe 标签规定一个内联框架

一个内联框架被用来在当前 HTML 文档中嵌入另一个文档

iframe能在当前页面加载其他的页面,也就是说制定了其src之后会触发WebViewClient的shouldOverrideUrlLoading方法。

看一下iframe的初始化

var doc = document;
_createQueueReadyIframe(doc);

 function _createQueueReadyIframe(doc) {
        messagingIframe = doc.createElement('iframe');
        messagingIframe.style.display = 'none';
        doc.documentElement.appendChild(messagingIframe);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在documentElement上增加了一个不可见的iframe标签,使用该标签来触发通讯。那么究竟是如何通讯的呢?

看一下native的WebViewClient的shouldOverrideUrlLoading方法(步骤3

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (url.startsWith(JsBridgeUtil.YY_RETURN_DATA)) {
        try {
            url = URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            LogUtil.e("decode url fail:" + url, e);
        }
        mWebView.handlerReturnData(url);
        return true;
    } else if (url.startsWith(JsBridgeUtil.YY_OVERRIDE_SCHEMA)) {
        try {
            url = URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            LogUtil.e("decode url fail:" + url, e);
        }
        mWebView.flushMessageQueue();
        return true;
    } else {
        return super.shouldOverrideUrlLoading(view, url);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

其中JsBridgeUtil.YY_RETURN_DATA 为 ”yy://return/“这部分用来处理从js端获取到的返回数据。

JsBridgeUtil.YY_OVERRIDE_SCHEMA为”yy://“ 刚刚桥js的doSend方法改变的iframe的src为CUSTOM_PROTOCOL_SCHEME + ‘: //’ + QUEUE_HAS_MESSAGE为”yy://fetchQuere“,会匹配仅第二个条件,并执行flushMessageQueue方法(步骤4

public void flushMessageQueue() {
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        loadUrl(JsBridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
            @Override
            public void onCallBack(String data) {
              // callback detail ignored
            }
        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

flushMessageQueue主要做了两件事,一是判断当前线程是否是主线程,只有主线程才会继续执行后面的逻辑。二是调用loadUrl方法,loadUrl方法需要两个参数,第一个是要加载url地址,第二个是一个回调,回调的内容暂且不关注,后面再说。继续看loadUrl方法

 public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
        this.loadUrl(jsUrl);
        responseCallbacks.put(JsBridgeUtil.parseFunctionName(jsUrl), returnCallback);
    }
    public static String parseFunctionName(String jsUrl) {
        return jsUrl.replace("javascript:WebViewJavascriptBridge.", "").replaceAll("\\(.*\\);", "");
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上面部分逻辑一方面执行js的调用,另一方面将回调与该js的调用关联起来。调用loadUrl的时候传入的JsBridgeUtil.JS_FETCH_QUEUE_FROM_JAVA实际为"javascript:WebViewJavascriptBridge._fetchQueue();",由此可以知道现在调用的是桥js的_fetchQueue()方法,那我们接着去分析_fetchQueue(步骤5

function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    //android can't read directly the return data, so we can reload iframe src to communicate with java
    bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

_fetchQueue逻辑很简单,只是将前面放到messageQueue里面的消息转变成json格式并将queue置空,然后再次按照定义好的规则拼接url放置到iframe上去,这次拼接出来的内容大致是 ”yy://return/_fetchQueue/{jsonData}“,一定看上去很眼熟吧,对这次会进入到shouldOverrideUrlLoading的第一个条件并执行webview的handlerReturnData方法。(步骤6

public void handlerReturnData(String url) {
    String functionName = JsBridgeUtil.getFunctionFromReturnUrl(url);
    CallBackFunction f = responseCallbacks.get(functionName);
    String data = JsBridgeUtil.getDataFromReturnUrl(url);
    if (f != null) {
        f.onCallBack(data);
        responseCallbacks.remove(functionName);
    }
}
public static String getFunctionFromReturnUrl(String url) {
    String temp = url.replace(YY_RETURN_DATA, EMPTY_STR); //YY_RETURN_DATA="yy://return/"
    String[] functionAndData = temp.split(SPLIT_MARK); //SPLIT_MARK="/"
    if (functionAndData.length >= 1) {
        return functionAndData[0];
    }
    return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

handlerReturnData先是通过操作字符串获取方法名,在这里获取到的正是_fetchQueue ,可想而知从回调map中取出key为_fetchQueue的回调正式刚才调用fetchQueue方法时我们忽略具体实现的那个回调,现在让我们来看下这个回调到底干了什么:(步骤7)

@Override
public void onCallBack(String data) {
  // deserializeMessage 反序列化消息
  List<Message> 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);
      }
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

首先将js传过来的json格式的字符串转换为message列表,如果列表不为空则挨个处理,处理过程大概如下:如果有repsponseId则说明是native调用js的返回结果,由于我们分析的是js调用native所以这条分路暂时不管,看下另外一条,如果callBackId不为空生成一个新的callBackFunction,并将callBackId在返回数据中一并返回,目的是为了方便js找到该callBackId对应的调用方js的callBackFunction,如果callBackId为空则说明这次请求不需要结果回调,则生成一个空实现的回调。这里callBackId是否为空是由最初调用方js决定的,可以参考 步骤2 处获取设备信息的例子。

接着会根据js设置的 BridgeHandler的名字去匹配对应的BridgeHandler,要调用的BridgeHandler的名字也是在最初调用方js那里确定好的,在 步骤2 处例子是 ”NativeHandler“。

那么这些BridgeHandler是在什么时候又是怎样注册进来的呢?这个问题比较简单,注册时机一般在使用webview的activity或者fragment对webview进行初始化的时候,注册方法更简单这里不浪费口舌。

假如根据handlerName能匹配到对应的BridgeHandler(一般都能匹配到,因为BridgeHandler的name都是两端提前商量好的),则执行BridgeHandler的handler方法,然后handler方法会根据事先定义好的规则匹配要执行的方法和传入的参数(如果有的话),执行完之后回调刚才根据callBackId是否为空生成的CallBackFunction (步骤8),例如:

@Override
public void handler(String data, CallBackFunction function) {
    //获取js端要调用的方法名
    String funcName = getFunctionName(data);
    switch (funcName) {
        case "getDeviceInfo":
            function.onCallBack(new Gson().toJson(new DeviceInfo()));
            break;
      case "xxx":
          ...
          ...
        default:
            function.onCallBack("no such function named: " + funcName);
            break;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

BridgeHandler将处理结果通过执行CallBackFunction的onCallBack方法回传给js,通过步骤7中onCallBack的具体实现我们知道具体传递工作交给了queueMessage方法,queueMessage又调用了dispatchMessage方法,源码如下

 private void queueMessage(Message m) {
        if (startupMessage != null) {
            startupMessage.add(m);
        } else {
            dispatchMessage(m);
        }
    }
void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        // 为json字符串转义特殊字符
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        String javascriptCommand = String.format(MyBridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        // 必须要找主线程才会将数据传递出去
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

可见执行结果通过一些格式处理字符转义之后,以”javascript:WebViewJavascriptBridge._handleMessageFromNative(’{resultData}’);“的形式调用桥接js的_handleMessageFromNative方法(步骤9),

function _handleMessageFromNative(messageJSON) {
  console.log(messageJSON);
  if (receiveMessageQueue) {
    receiveMessageQueue.push(messageJSON);
  }
  _dispatchMessageFromNative(messageJSON);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

_handleMessageFromNative在将处理结果直接放进messageQueue之后执行了_dispatchMessageFromNative

function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                //直接发送
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }

                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName];
                }
                //查找指定handler
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

这段会看着非常眼熟,因为他的逻辑跟 步骤7 中CallBackFunction的实现如出一辙,这也可以理解,因为通讯是双向的,自然两边的实现逻辑也就一致,只是实现的语言不一样。

有了上面 步骤7 的分析,这段就好理解多了,因为 步骤7 中内部生成的CallBackFunction为Message指定了responseId 为js传过去的callBackId,那么我们也就能根据这个callBackId对应到 步骤2 中存入responseCallbacks中的responseCallback,也就是调用方js定义的callBack,在获取设备信息的例子汇总该callBack就是

function(responseData) {
  document.getElementById("show").innerHTML = "getDeviceInfo involved,responseData from native is " + responseData
}
  • 1
  • 2
  • 3

至此,js调用native的过程我们就分析完了。调用过程在js和native之间来回转换,会有些绕,用图表的方式总结一下可能会更清晰一些:

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/561928
推荐阅读
相关标签
  

闽ICP备14008679号