JsBridge 源码分析

作者: nothingwxq | 来源:发表于2016-05-31 10:40 被阅读4940次

    源码:
    https://github.com/lzyzsd/JsBridge

    1 背景

    近年来混合框架很火,一些大型的公司如BAT的移动客户端app几乎都采用了混合架构。这样实现有什么好处呢?首先就必须了解采用webview开发和采用原生开发的客户端的优缺点。这里我仅列举个人的观点:

    1.1 使用webview:

    优点: 便于敏捷开发、便于维护和可以热修复和定制
    缺点:UI没原生的美观

    1.2 使用原生开发:

    优点:当然是可以方便使用原生UI
    缺点:无法热修复等

    貌似很多公司都采用类似的原理实现,本文选了个相近的JsBridge来分析下混合框架下app开发的架构。

    2 准备工作(可以忽略)

    2.1 传送的消息结构见Message类:

    private String callbackId; //callbackId
    
    private String responseId; //responseId
    
    private String responseData; //responseData
    
    private String data; //data of message
    
    private String handlerName; //name of handler
    

    2.2 工具类:BridgeUtil

    几个函数命名上可以大致猜出,函数功能,作如下注释。具体想了解细节请看源代码。

    public static String parseFunctionName(String jsUrl){  //jsUrl中解析函数名
    
    }
    
    public static String getDataFromReturnUrl(String url) {//return数据中获取data
    
    }
    
    public static String getFunctionFromReturnUrl(String url) {
    }
    
    /**
    
     * js 文件将注入为第一个script引用
    
     * @param view
    
     * @param url
    
     */
    
    public static void webViewLoadJs(WebView view, String url){//从url中加载js
    
    }
    
    public static void webViewLoadLocalJs(WebView view, String path){//加载本地path路径的js
    
    }
    
    public static String assetFile2Str(Context c, String urlStr){
    
    }
    

    3寻找入口

    3.1 入口处

    当我们阅读源码时,不知道从何入手时,要先想到寻找入口。(由于本文的源码不多,如果直接阅读的也行。)源码文件较多时,我们该如何入手呢?首先我们需要查找怎么使用(调用处就是一个很好的入口),然后层层抽丝剥茧。看源代码demo部分:

            webView.setDefaultHandler(new DefaultHandler());
            
            webView.registerHandler("submitFromWeb", new BridgeHandler() {
    
                @Override
                public void handler(String data, CallBackFunction function) {
                    Log.i(TAG, "handler = submitFromWeb, data from web = " + data);
                    function.onCallBack("submitFromWeb exe, response data 中文 from Java");
                }
    
            });
    
    
            webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {
                @Override
                public void onCallBack(String data) {
    
                }
            });
    
            webView.send("hello");
    
        }
    
    
        @Override
        public void onClick(View v) {
            if (button.equals(v)) {
                webView.callHandler("functionInJs", "data from Java", new CallBackFunction() {
    
                    @Override
                    public void onCallBack(String data) {
                        // TODO Auto-generated method stub
                        Log.i(TAG, "reponse data from js " + data);
                    }
    
                });
            }
    
        }
    

    3.2 初始化部分

    webView.setDefaultHandler(new DefaultHandler());一句设置了一个DefaultHandler。

    4 客户端调用JavaSript(android 端例子)

    原理:是使用本身webview.loadUrl("javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');" );调用时序图如下图:

    Paste_Image.png

    发觉按这个图,阅读基本上就了解了android 端调用JavaSript的流程。这里补充说一下js 的_dispatchMessageFromNative()函数中调用的handler的名字“functionInJs”是客户端、web前端提前约定好的。而最后调用的_doSend()就是javasript回调给java的了。

    4.1 doSend()方法

    //native 
    private void doSend(String handlerName, String data, CallBackFunction responseCallback) {
    
    Message m = new Message();
    
    if (!TextUtils.isEmpty(data)) {
    
    m.setData(data);
    
    }
    
    if (responseCallback != null) {
    
    String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis())); //生成id,后自增,以便区分,不过源码中貌似没有用到
    
    responseCallbacks.put(callbackStr, responseCallback);
    
    m.setCallbackId(callbackStr);
    
    }
    
    if (!TextUtils.isEmpty(handlerName)) {
    
    m.setHandlerName(handlerName);
    
    }
    
    queueMessage(m);
    
    }
    

    4.2 queueMessage()

    private void queueMessage(Message m) {
    
    if (startupMessage != null) { //本地调用javasript为null
    
    startupMessage.add(m);
    
    } else {
    
    dispatchMessage(m);
    
    }
    
    }
    

    本地调用javasript时,startupMessage 为null。很简单,直接跳到dispatchMessage函数

    4.3 dispatchMessage()

    void dispatchMessage(Message m) {
    
    String messageJson = m.toJson();
    
    //escape special characters for json string
    
    messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
    
    messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
    
    String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
    
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
    
    this.loadUrl(javascriptCommand);
    
    }
    

    if前面的都是初始化回调数据的,if语句才是精华,java调用js代码。注意javascriptCommand的值String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson); 其中JS_HANDLE_MESSAGE_FROM_JAVA为常量"javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');" 。也即调用了WebViewJavascriptBridge._handleMessageFromNative(messageJson ).

    4.4 js _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);
                        }
                    }
                }
            });
    }
    

    java回调的Message对象messageJson 的responseId为null,故直接走else语句。handler = messageHandlers[message.handlerName]; 通过队列拿到handler。handlerName为“functionInJs”。这个的初始化是在这:

    connectWebViewJavascriptBridge(function(bridge) {
                  ....
                bridge.registerHandler("functionInJs", function(data, responseCallback) {
                    document.getElementById("show").innerHTML = ("data from Java: = " + data);
                    var responseData = "Javascript Says Right back aka!";
                    responseCallback(responseData);
                });
            })
    

    5 web前端调用native

    Paste_Image.png

    5.1 实现原理

    参照时序图,大致了解了调用过程。实现原理的思想也比较简单,利用js的iFrame(不显示)的src动态变化,触发java层webClient的shouldOverrideUrlLoading,然后让本地去调用javasript。

    5.2 _doSend(message, responseCallback)

     //sendMessage add message, 触发native处理 sendMessage
        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;
        }
    

    js 调用java时,会调用到 _doSend()函数。在该函数中使用sendMessageQueue队列把消息存起来,并且改变Iframe的src,提醒native java端来取消息。
    Iframe的初始化在这里:

     _createQueueReadyIframe(doc);
        var readyEvent = doc.createEvent('Events');
        readyEvent.initEvent('WebViewJavascriptBridgeReady');
        readyEvent.bridge = WebViewJavascriptBridge;
        doc.dispatchEvent(readyEvent);
    

    5.3 js端 _fetchQueue()

    // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
    
    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
    
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
    
    }
    

    java端拦截到_doSend()函数更改的iFrame的src后,经过一系列本地调用,中间略,通过loadUrl又调js层的 _fetchQueue()。在该函数中才是真正的调用java函数。

    6 参考文献:

    1 http://www.cnblogs.com/wingyip/p/5426477.html
    2 http://zjutkz.net/2016/04/17/%E5%A5%BD%E5%A5%BD%E5%92%8Ch5%E6%B2%9F%E9%80%9A%EF%BC%81%E5%87%A0%E7%A7%8D%E5%B8%B8%E8%A7%81%E7%9A%84hybrid%E9%80%9A%E4%BF%A1%E6%96%B9%E5%BC%8F/
    3 http://blog.csdn.net/sk719887916/article/details/47189607

    相关文章

      网友评论

        本文标题:JsBridge 源码分析

        本文链接:https://www.haomeiwen.com/subject/hgbfdttx.html