JsBridge 源码分析

源码:
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容