Android与JS之JsBridge使用与源码分析

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

在Android开发中,由于Native开发的成本较高,H5页面的开发更灵活,修改成本更低,因此前端网页JavaScript(下面简称JS)与Java之间的互相调用越来越常见。

JsBridge就是一个简化Android与JS通信的框架,源码:https://github.com/lzyzsd/JsBridge
我们今天通过一个简单栗子来分析下开源框架JsBridge的源码。栗子的代码我也放在Github,有需要的可以seesee:
https://github.com/juexingzhe/Android_JS
栗子很简单,随便输入信息登陆,会加载一个H5页面,在H5界面点击按钮,Java执行getUserInfo()然后将UserInfo回传给JS,H5页面再显示UserInfo。

1.png
2.png
3.png

JS调用Android基本有下面三种方式

webView.addJavascriptInterface()
WebViewClient.shouldOverrideUrlLoading()
WebChromeClient.onJsAlert()/onJsConfirm()/onJsPrompt() 方法分别回调拦截JS对话框alert()、confirm()、prompt()消息

Android调用JS

webView.loadUrl();
webView.evaluateJavascript()

常用方法的使用后面栗子中会用到,更细节的介绍各位同学可以去网上搜搜看看。

1.JsBridge使用

我们先来看下Java层的代码
首先引入依赖和仓库

dependencies {
   ……
    compile 'com.github.lzyzsd:jsbridge:1.0.4'
    compile 'com.google.code.gson:gson:2.7'
}
repositories {
    jcenter()
    maven { url "https://jitpack.io" }
}

准备工作就是这样,下面可以开始撸代码,首先就是点击按钮登陆,这个简单:

Intent intent = new Intent(LoginActivity.this, WebActivity.class);
intent.putExtra("email", mEmailView.getText().toString());
startActivity(intent);

布局文件中要使用BridgeWebView:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.github.lzyzsd.jsbridge.BridgeWebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

在跳转后的页面,获取登陆信息并存储,再通过loadUrl加载H5页面:

Intent intent = this.getIntent();
String email = intent.getStringExtra("email");

 mUserInfo = new UserInfo(email);

mBridgeWebView = (BridgeWebView) findViewById(R.id.web_view);
mBridgeWebView.setDefaultHandler(new DefaultHandler());
mBridgeWebView.loadUrl("file:///android_asset/getuserinfo.html");

registerHandler();

主要是要注册Handler,供JS调用,

getUserInfo就是注册供JS调用的Handler的id
data是JS传过来的参数
CallBackFunction 函数中需要把JS需要的response返回给JS

private void registerHandler() {
        mBridgeWebView.registerHandler("getUserInfo", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                Log.i(TAG, "handler = getUserInfo, data from web = " + data);
                function.onCallBack(new Gson().toJson(mUserInfo));
            }
        });
}

Java层的代码就这么简单,下面看下JS层工作:
首先需要一个js文件,我们写一个getuserinfo.html文件放在assets目录下,文件内容,不建议把js代码直接放在html文件中,我为了方便直接就写在这了。代码放了两个段落,一个类似于TextView用来显示用户信息,一个Button。点击按钮会调用callHandler,三个参数和Java层一一对应,在Java层返回的时候,会调用function(responseData)函数,显示用于信息。

<html>
<head>
    <meta content="text/html; charset=utf-8" http-equiv="content-type">
    <title>
        js调用java
    </title>
</head>
<body>
<p>
    <xmp id="show">
    </xmp>
</p>
<div align="center">
    <p>
        <input type="button" id="enter" value="获取用户信息" onclick="getUserInfo();"
        />
    </p>
</div>
</body>
<script>
    function getUserInfo(){
        window.WebViewJavascriptBridge.callHandler(
            'getUserInfo',
            {'info': 'I am JS, want to get UserInfo from Java'},
            function(responseData) {
                document.getElementById("show").innerHTML = "repsonseData from java,\ndata = " + responseData;
            }
        )
    }
</script>
</html>

使用基本就是这样了,可以看出来JsBridge通过封装,JS和Java之间的通信只需要实现两个步骤,使用起来很方便。

我们来看下源码是怎么个玩法,先来个华丽丽的分割线



2.JsBridge源码分析

分析之前我把JS调用Java画了个简易交互图,Java调用JS的过程类似:

4.png

是不是感觉反而更复杂了???其实只要捉住主要的三点,JsBridge就原形毕露:

1.Android调用JS是通过loadUrl(url),url中可以拼接要传给JS的对象
2.JS调用Android是通过shouldOverrideUrlLoading
3.JsBridge将沟通数据封装成Message,然后放进Queue,再将Queue进行传输

接下来我们来一步一步跟踪上面栗子的调用过程:

  • JS层点击按钮调用callHandler

handlerName,Java和JS要一致,
data是Java层handlerName函数执行的参数
responseCallback是Java执行完handlerName返回时,JS回调的接口,是JS执行

onclick="getUserInfo();"

function getUserInfo(){
    window.WebViewJavascriptBridge.callHandler(
            'getUserInfo',
            {'info': 'I am JS, want to get UserInfo from Java'},
            function(responseData) {
                document.getElementById("show").innerHTML = "repsonseData from java,\ndata = " + responseData;
            }
    )
}

callHandler会调用_doSend

如果JS需要回调,就将回调的callbackId放进message中,Java执行完会传回callbackId,这里是cb_1_1495182409011
构造完message放进队列sendMessageQueue
通过iframe属性给Java发送通知消息,消息结构yy://QUEUE_MESSAGE/

function callHandler(handlerName, data, responseCallback) {
    _doSend({
        handlerName: handlerName,
        data: data
    }, responseCallback);
}

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;
}
  • Java收到通知消息
    WebView在shouldOverrideUrlLoading拦截到url:yy://QUEUE_MESSAGE/
    然后会执行webView.flushMessageQueue(),在主线程执行loadUrl通知JS层推送队列到Java;

JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();"
调用JS层的_fetchQueue,通知JS层发送队列到Java层
在responseCallbacks中注册回调接口,接口id是函数名_fetchQueue,在JS推送消息队列时进行回调

void flushMessageQueue() {
          if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
               loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
                    @Override
                    public void onCallBack(String data) {
                         //
                    });
          }
}

public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
          this.loadUrl(jsUrl);
          responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
  • JS 发送Request Queue
    执行_fetchQueue

将sendMessageQueue转化成JSON
通过iframe属性给Java发送通知消息,消息结构:yy://return/_fetchQueue/消息队列的内容

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收到调用通知,进行处理并发送Response Queue到JS
    WebView在shouldOverrideUrlLoading会拦截到url:
yy://return/_fetchQueue/[{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]

执行webView.handlerReturnData(url);

根据函数名_fetchQueue拿到之前注册的回调函数CallBackFunction returnCallback
执行回调函数,并且从注册中移除

void handlerReturnData(String url) {
          String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
          CallBackFunction f = responseCallbacks.get(functionName);
          String data = BridgeUtil.getDataFromReturnUrl(url);
          if (f != null) {
               f.onCallBack(data);
               responseCallbacks.remove(functionName);
               return;
          }
}

接下来就是对Request Queue的解析然后找到JS希望调用Handler并且执行,代码中我写了注释,可以直接看:

//回调接口执行onCallBack函数
//其中data [{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]
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<Message> list = null;
                         try {
                              //将JSON数组转化成Java list
                              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++) {
                              //从list中取出Message
                              Message m = list.get(i);
                              //在我们的栗子中没有responseId,因此到else分支
                              String responseId = m.getResponseId();
                              // 是否是response
                              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
                                   //如果有callbackId就说明JS需要回调,因此Java层需要构造responseMsg
                                   //从message中取出callbackId,放进responseMsg
                                   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 handler;
                                   //从message中取出Handler名字,再从messageHandlers中取
                                   //如果没有就使用默认的Handler
                                   if (!TextUtils.isEmpty(m.getHandlerName())) {
                                        handler = messageHandlers.get(m.getHandlerName());
                                   } else {
                                        handler = defaultHandler;
                                   }
                                   if (handler != null){
                                        //执行handler
                                        handler.handler(m.getData(), responseFunction);
                                   }
                              }
                         }
                    }
               });
          }
}

那么这个handler是什么?就是Java调用registerHandler注册的getUserInfo

private void registerHandler() {
        mBridgeWebView.registerHandler("getUserInfo", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                Log.i(TAG, "handler = getUserInfo, data from web = " + data);
                function.onCallBack(new Gson().toJson(mUserInfo));
            }
}

上面的function就是在flushMessageQueue 解析时构造的responseFunction,在message中包括JS层需要回调的函数Id,然后就是getUserInfo执行的结果
调用queueMessage

responseFunction = new CallBackFunction() {
          @Override
          public void onCallBack(String data) {
                    Message responseMsg = new Message();
                    responseMsg.setResponseId(callbackId);
                    responseMsg.setResponseData(data);
                    queueMessage(responseMsg);
          }
};

queueMessage调用dispatchMessage发送message给JS

通过构造String指令,然后loadUrl执行JS代码,注意对象也是通过这样方式传递过去的,就类似调用本地函数,不发起网络请求

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);
        }
}

其中

BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA ="javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');"
javascriptCommand = javascript:WebViewJavascriptBridge._handleMessageFromNative('{\"responseData\":\"{\\\"email\\\":\\\"jue@126.com\\\"}\",\"responseId\":\"cb_1_1495182558893\"}');
//data = {\"responseData\":\"{\\\"email\\\":\\\"jue@126.com\\\"}\",\"responseId\":\"cb_1_1495182409011\"}
  • JS收到Response JSON
    来到_handleMessageFromNative,
function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue && receiveMessageQueue.length > 0) {
            receiveMessageQueue.push(messageJSON);
        } else {
            _dispatchMessageFromNative(messageJSON);
        }
}

最后都会调用到_dispatchMessageFromNative,由于是JS主动调用Java,因此有responseId,执行registerHandler时传入的CallBack,也就是显示用户信息。我在代码里加了注释,很容易看懂。

function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
             //将数据解析成JSON
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            //根据responseId:cb_1_1495182409011拿到responseCallback,就是我们前门注册的alert
            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);
                    }
                }
            }
        });
}

源码的分析就到这结束了,代码不多,但是封装的接口很是好用。最后再来个分割线~~



3.总结

最后总结下,使用上很方便主要两个步骤

被调用方注册Handler

registerHandler(String handlerName, BridgeHandler handler) 

调用方调用Handler

callHandler(String handlerName, String data, CallBackFunction callBack)

原理上还是那三句话,请原谅我从上面直接copy过来:

1.Android调用JS是通过loadUrl(url),url中可以拼接要传给JS的对象
2.JS调用Android是通过shouldOverrideUrlLoading
3.JsBridge将沟通数据封装成Message,然后放进Queue,再将Queue进行传输

好了,今天我们JsBridge的使用和源码分析就到这了,谢谢!

文中栗子的链接:
https://github.com/juexingzhe/Android_JS

欢迎关注公众号:JueCode

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

推荐阅读更多精彩内容