Android-jsbridge

简介

java跟js交互的框架。

git地址:https://github.com/lzyzsd/JsBridge

使用

git地址:https://github.com/kbjay/kj_jsbridge_use

  1. 关于导包,可以按照jsbridge的使用说明来

    1
    2
    3
    4
    5
    6
    7
    8
    repositories {
    // ...
    maven { url "https://jitpack.io" }
    }

    dependencies {
    compile 'com.github.lzyzsd:jsbridge:1.0.4'
    }

    为了方便分析源码,我这边的demo(kj_kjbrigde_use)是将jsbridge作为一个module(library)引入。

  2. java传递消息给js:两种方式一种是默认方式,一种是spec方式

    java中代码如下:

    webView.callHandler(tag, msg,callback):发送消息msg给js中tag对应的function,function执行完成之后有一个callback。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /**
    * java 发送消息给 js
    */
    private void java2jsSpec() {
    // java2js-> step1
    webView.callHandler("functionInJs", "发送消息给js(spec方式!!)", new CallBackFunction() {
    @Override
    public void onCallBack(String data) {
    Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
    }
    });
    }

    /**
    * java 发送消息给 js
    */
    private void java2jsDefault() {
    webView.send("发送消息给js(默认方式!)", new CallBackFunction() {
    @Override
    public void onCallBack(String data) {
    Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
    }
    });
    }

    对应的js代码如下:

    js中需要注册接受消息handler(同样有两种类型:默认跟spec),处理完消息之后调用responseCallback(callbackMsg)将处理结果回传给java

    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
    //注册事件监听,初始化
    // Java2js-> step2
    function initJsBridge(callback) {
    if (window.WebViewJavascriptBridge) {
    callback(WebViewJavascriptBridge)
    } else {
    document.addEventListener(
    'WebViewJavascriptBridgeReady'
    , function () {
    callback(WebViewJavascriptBridge)
    },
    false
    );
    }
    }

    //回调函数,接收java发送来的数据
    initJsBridge(function (bridge) {
    //默认接收
    bridge.init(function (message, responseCallback) {
    document.getElementById("show").innerHTML = '默认接收到Java的数据: ' + message;

    var responseData = 'js默认接收完毕,并回传数据给java';
    responseCallback(responseData); //回传数据给java
    });

    //指定接收,参数functionInJs 与java保持一致
    //java2js -> step 3;
    bridge.registerHandler("functionInJs", function (data, responseCallback) {
    document.getElementById("show").innerHTML = '指定接收到Java的数据: ' + data;

    var responseData = 'js指定接收完毕,并回传数据给java';
    responseCallback(responseData); //回传数据给java
    });
    })
  3. js传递消息给java,两种方式:默认跟spec

    js中发送消息,接收callback代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function jsToJavaDefault() {
    var msg = "js发送给java-->默认消息";
    window.WebViewJavascriptBridge.send(msg, function (resp) {
    document.getElementById("show").innerHTML = resp;
    });
    }

    //js2java-> step1
    function jsToJavaSpec() {
    var msg = "js发送给java-->指定消息";
    window.WebViewJavascriptBridge.callHandler('submitFromWeb', msg, function (resp) {
    document.getElementById("show").innerHTML = resp;
    });
    }

    java中处理消息,回传callback代码如下:同样两种方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * 初始化jsbridge用于接受js发来的消息,并利用回调将传递数据给js。
    */
    private void initJs() {
    webView.setDefaultHandler(new BridgeHandler() {
    @Override
    public void handler(String data, CallBackFunction function) {
    Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
    function.onCallBack("java默认接收完毕,并回传数据给js");
    }
    });
    //js2java-> step2
    webView.registerHandler("submitFromWeb", new BridgeHandler() {
    @Override
    public void handler(String data, CallBackFunction function) {
    Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
    function.onCallBack("java指定接收完毕,并回传数据给js");
    }
    });
    }

源码解析

基础知识
  1. java跟js交互的原始方式

    java->js:

    • webView.loadUrl(“javascript:functionInJs(msg)”) jsBridge中选择的方式
      webView.evaluateJavascript(“javascript:functionInJs(msg)”,callback) API19(4.4)之后都可以,带有callback

    js->java:

    • 在java声明JsJavaBridge.class,类中的方法用@JavaScriptInterface标记,设置webView.addJavascriptInterface(new JsJavaBridge(),”bridge”)之后,可以在js中使用bridge.xxx()调用JsJavaBridge中的方法,这种方式在4.4之前有安全问题,不建议使用(不过现在基本上不会去适配4.4以下的了)
    • 自定义WebViewClient,在shouldOverrideUrlLoading()中拦截url之后调用java代码。(这个是jsbridge中选择的方式)需要注意版本适配 API24 (7.0)
    • 自定义WebChromeClient,拦截console之后调用java代码
    • 自定义WebChromeClient,拦截弹框事件之后调用Java 代码

    参考:https://www.jianshu.com/p/eb52ae7633b5

  2. 可以通过重写webviewClient的onPageFinished方法,在方法中通过loadurl的方式给webview注入js代码。

  3. 关于H5中的iframe:通过修改iframe的src来触发webViewClient的shouldOverrideUrlLoading方法。

流程分析

先简单描述下整个流程:

首先在webViewClient的onPageFinished的方法里加载了assets中的WebViewJavascriptBridge.js。

java->js :

  • Java处理消息体:生成一个唯一标示(callbackId)+ msg实体 + functionTag封装成message对象;其中functionTag是js中消息map的key,js可以根据这个找到java希望js调用的方法。

  • 将callbackId作为key,handler作为value放入java中的消息map;(我们给js发一个request,js处理完成之后会有一个response,这个handler就是处理response的

  • 上面的js消息map需要我们在js中注册,其实就是把functionTag作为key,把回调的方法作为value放入map中。

  • 将message对象通过loadurl的方式传递给js

  • js收到消息会先判读是否有callbackId(原因在js传递消息给java中),因为我们传递过来了,那么肯定是有的,之后会去判断js的消息map中是否有functionTag这个key(原因在java传递消息给js的默认方式中),因为我们在js中注册过了,那么肯定是有的,然后取出key对应的回调,处理java传过来的msg实体。

  • 将callbackId跟处理完成的结果放入js中的消息队列queue。

  • 之后会修改iframe控件src的url,在url上会拼接上“我有新消息了”,这样就会触发WebViewClient中的shouldOverrideUrlLoading方法,这样java就知道了js消息队列中有新消息了。

  • java通过loadurl的方式通知js取出js消息队列中的所有消息,搞成jsonString。

  • 修改iframe控件的url,在url上会拼接上“我的所有消息:jsonString”,此时会触发WebViewClient中的shouldOverrideUrlLoading方法,这样java就收到了js消息队列中的所有消息。

  • 最后我们取出消息,根据消息中的callbackId在java的消息map中找到对应的handler,之后处理js的response。

对应的源码流程如下:

1
2
3
4
5
6
7
//发送消息给js
webView.callHandler("functionInJs", "发送消息给js(spec方式!!)", new CallBackFunction() {
@Override
public void onCallBack(String data) {
Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
}
});

进入callHandler方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* call javascript registered handler
* 调用javascript处理程序注册
* @param handlerName handlerName
* @param data data
* @param callBack CallBackFunction
*/
@Override
public void callHandler(String handlerName, String data, CallBackFunction callBack) {
send(handlerName, data, callBack);
}

进入send方法,作用就是上面提到的封装message。并且把callbackId跟处理response的handler放入了responseCallbacks中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 保存message到java消息队列
* @param handlerName handlerName
* @param data data
* @param responseCallback CallBackFunction
*/
private void send(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()));
//重点!!!!!!
responseCallbacks.put(callbackStr, responseCallback);

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

进入queueMessage方法

1
2
3
4
5
6
7
8
9
10
11
/**
* list<message> != null 添加到消息集合否则分发消息
* @param m Message
*/
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}

进入dispatchMessage,重点最后一句(将消息传递给了js)。

1
2
3
4
5
6
7
8
9
10
11
/**
* 分发message 必须在主线程才分发成功
* @param m Message
*/
void dispatchMessage(Message m) {
String messageJson = m.toJson();
...
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
}
}

进入js的_handleMessageFromNative方法

1
2
3
4
5
6
//提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
function _handleMessageFromNative(messageJSON) {
...
_dispatchMessageFromNative(messageJSON);

}

进入_dispatchMessageFromNative方法(根据functionTag找到在js中注册处理方法handler并调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//提供给native使用,
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
...

var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId,
responseData: responseData
});
};
...
handler = messageHandlers[message.handlerName];
//查找指定handler
...
handler(message.data, responseCallback);
});
}

handler处理完成之后有个回调responseCallback,会调用_doSend方法。(将java传递过来的callbackid和js处理完成之后的response封装成一个对象作为参数)

进入_doSend方法,将消息放入消息队列。之后修改iframe控件的src。

1
2
3
4
5
6
//sendMessage add message, 触发native处理 sendMessage
function _doSend(message, responseCallback) {
...
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

进入WebViewClient的shouldOverrideUrlLoading方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
...
String url = request.getUrl().toString();
...
url = URLDecoder.decode(url, "UTF-8");
...
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {
//js返回消息队列中的消息
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
//js返回有新消息的标示
webView.flushMessageQueue();
return true;
}

进入flushMessageQueue方法,其实就调用了一个方法loadUrl,有一个callback(可以在回调的时候再看)

1
2
3
4
5
6
7
8
9
10
11
/**
* 刷新消息队列
*/
void flushMessageQueue() {
...
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
。。。
});
}

loadUrl方法:调用js的_fetchQueue方法并把“_fetchQueue”作为key,上面的callback作为value放入responseCallbacks中。

1
2
3
4
5
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
// 添加至 Map<String, CallBackFunction>
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}

进入js中的_fetchQueue方法:取出js中所有的消息,放在url的后面

1
2
3
4
5
6
7
8
9
// 提供给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
if (messageQueueString !== '[]') {
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
}

进入WebViewClient的shouldOverrideUrlLoading方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
...
String url = request.getUrl().toString();
...
url = URLDecoder.decode(url, "UTF-8");
...
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {
//js返回消息队列中的消息
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
//js返回有新消息的标示
webView.flushMessageQueue();
return true;
}

进入handlerReturnData方法,根据callbackid在responseCallbacks中找到处理response的handler。执行handler的callback方法。流程结束!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 获取到CallBackFunction data执行调用并且从数据集移除
* @param url
*/
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;
}
}

js->java:

跟java->js在本质上没有什么区别,有兴趣可以自己跟一下(如果理解了上面的流程,会很顺利的跟下来的~)。

源码中的一个小问题

源码中BridgeWebView实现了WebViewJavascriptBridge接口,该接口是对外暴露的发送消息给js的接口,源码里面只声明了两种方式(1->默认方式发送消息给js,不带回调 ; 2->默认方式发送消息给js,带回调 ),但是实际上BridgeWebView对外暴露的有3种方式,还有一种(3-> callHandler(spec方式,带回调的)),所以我把callHandler也声明到了接口里。

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
/**
* 对外暴露的方法接口
*/
public interface WebViewJavascriptBridge {
/**
* 发送消息给js(默认方式不带回调)
*
* @param data
*/
void send(String data);

/**
* 发送消息给js (默认方式带回调)
*
* @param data
* @param responseCallback
*/
void send(String data, CallBackFunction responseCallback);

/**
* 发送消息给js(spec带回调)
*
* @param funNameInJs
* @param data
* @param responseCallback
*/
void callHandler(String funNameInJs, String data, CallBackFunction responseCallback);

}