Android侧webview与Js通信的方式(1)
JsBridge原理介绍
Android侧JsBridge一般指 JsBridge,该框架对应ios侧的WebViewJavascriptBridge,两者的实现细节各有不同,但是总体原理一致。我们主要看一下其Js与Native通信原理的实现,对于具体的代码细节不做深究。
JsBridge集成
-
Js端
集成源码中的js文件,WebViewJavascriptBridge.js,注意此处不可以通过注入的方式实现,不要被各种讲解博客误导。
Android侧
dependencies {
compile 'com.github.lzyzsd:jsbridge:1.0.4'
}
Js调用Native
步骤
- js侧
function _doSend(message, responseCallback) {
if (responseCallback) {
//生成唯一callbackid用于标识该次jsbridge通信过程
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
//src:"yy://__QUEUE_MESSAGE__/"
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
- 2.native侧
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
这里会走第二个if, 调用BridgeWebView的flushMessageQueue()方法
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
...
}
});
}
}
在这个flushMessageQueue方法里, 如果当前是主线程就调用一个loadUrl方法
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
// jsUrl = "javascript:WebViewJavascriptBridge._fetchQueue();"
this.loadUrl(jsUrl);
// 添加至 Map<String, CallBackFunction>
String functionName = BridgeUtil.parseFunctionName(jsUrl);
// functionName = "_fetchQueue"
responseCallbacks.put(functionName, returnCallback);
}
在这个方法里, 首先会调用WebViewJavascriptBridge的_fetchQueue()方法, 然后解析方 法名字, 因为这里的方法名字是写死的, 其实就是_fetchQueue, 请记住这个名字, 因为后面会用到.然后将以这个_fetchQueue为key, 回调方法为value, 放到一个map里面.然后我们再去看js那端的方法.
- 3.js侧
// 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
console.log('messageQueueString = ' + messageQueueString);
sendMessageQueue = [];
// android can't read directly the return data, so we can reload iframe src to communicate with java
var src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
messagingIframe.src = src;
}
-
4.native侧
触发shouldOverrideUrlLoading方法,并走第一个if,触发handlerReturnData方法
void handlerReturnData(String url) {
// _fetchQueue
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
//取出flushMessageQueue方法中放入responseCallbacks队列中的callback
CallBackFunction f = responseCallbacks.get(functionName);
//取出js侧传来的数据
String data = BridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
//执行callback
f.onCallBack(data);
responseCallbacks.remove(functionName);
return;
}
}
在看一下这个callback
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 {
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();
<br>
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);
}
};
<br/>
} 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);
}
}
}
}
});
}
}
首先将数据解析成一个Message的list, 这个Message是自定义的类, 里面包含两端协商好格式的信息,最后会执行到queueMessage(responseMsg)中
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
走dispatch方法
/**
* 分发message 必须在主线程才分发成功
* @param m Message
*/
void dispatchMessage(Message m) {
String messageJson = m.toJson();
//escape special characters for json string 为json字符串转义特殊字符
messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'");
// javascript:WebViewJavascriptBridge._handleMessageFromNative('{\"responseData\":\"http:\\\/\\\/ww3.sinaimg.cn\\\/mw690\\\/96a29af5jw8fdfu43tnvlj20ro0rotab.jpg\",\"responseId\":\"cb_4_1532856634427\"}');
String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
// 必须要找主线程才会将数据传递出去 --- 划重点
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
}
}
首先将这个Message转化成json格式的字符串, 去掉一些特殊字符, 然后再主线程调用js方法, 方法是WebViewJavascriptBridge._handleMessageFromNative方法
- 5.js侧
// 提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue && receiveMessageQueue.length > 0) {
receiveMessageQueue.push(messageJSON);
} else {
_dispatchMessageFromNative(messageJSON);
}
}
_handleMessageFromNative方法会处理native传来数据,本次交互结束
-
流程图
avatar 问题
看完JsBridge代码,可能大家都会有疑问,这一次流程中,为什么js侧与native侧为什么要交互两次,第一次其实并没有传任何有效数据过来,是否多余。下面我们着重看下这个问题。
Cordova方案参考
由于业界hybrid方案并不多,一般大厂的方案又较为复杂,而且网上资料基本没有任何对该问题的解释,因此本文参考了云闪付正在使用的hybrid方案cordova的通信逻辑。
Cordova方案相较Jsbridge方案更为重量级,十分复杂,因此本文并不做深入研究,仅针对其实现的Native、JS端通信逻辑进行研究。
ios侧
ios侧一般有两种方式,核心代码如下
if (bridgeMode === jsToNativeModes.WK_WEBVIEW_BINDING) {
window.webkit.messageHandlers.cordova.postMessage(command);
} else {
// If we're in the context of a stringByEvaluatingJavaScriptFromString call,
// then the queue will be flushed when it returns; no need for a poke.
// Also, if there is already a command in the queue, then we've already
// poked the native side, so there is no reason to do so again.
if (!isInContextOfEvalJs && commandQueue.length == 1) {
switch (bridgeMode) {
case jsToNativeModes.XHR_NO_PAYLOAD:
case jsToNativeModes.XHR_WITH_PAYLOAD:
case jsToNativeModes.XHR_OPTIONAL_PAYLOAD:
pokeNativeViaXhr(); // 新建一个XMLHttpRequest,并发送一个HEAD请求,并将commondQueue以json串的形式放在请求头cmds上。
break;
default: // iframe-based.
pokeNativeViaIframe(); // 创建iframe,通过hash值来传递commondQueue 或 execIframe.src = "gap://ready"
}
}
}
可以看出有两种方式,一是新建一个XMLHttpRequest,并发送一个HEAD请求,并将commondQueue以json串的形式放在请求头cmds上。native侧进行拦截;二是创建iframe,通过hash值来传递commondQueue 或 execIframe.src = "gap://ready",与jsbridge一个原理。
ios端通过UIWebViewDelegate(iframe方式)或 NSURLProtocol拦截(xhr方式)方式接收到commondQueue后,执行插件的实际功能。
ios侧处理完后回消息给js侧也有两种方式一是通过UIWebView的stringByEvaluatingJavaScriptFromString方法,二是通过注入方式调用js侧iOSExec.nativeCallback方法。
Android侧
var messages = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson); //
// If argsJson was received by Java as null, try again with the PROMPT bridge mode.
// This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666.
if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && messages === "@Null arguments.") {
androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
androidExec(success, fail, service, action, args);
androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
return;
} else {
androidExec.processMessages(messages, true);
}
- 如果是JS_OBJECT方式,那么nativeApiProvider.get().exec= 安卓端源码中注解了@JavascriptInterface的 exec方法
- 如果是PROMPT方式,那么nativeApiProvider.get().exec 为如下方法:
exec: function(bridgeSecret, service, action, callbackId, argsJson) {
return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));
},
即通过promt()与native侧的onJsPromt()方法通信。
android侧回调回js侧也有两种方式,一是通过evaluateJavascript,二是通过loadurl。两种方式都是通过注入直接调用js侧androidExec.processMessages(messages, true)方法。
cordova总结
从以上分析可以看出,cordova不管在ios侧还是android侧,都是只通信一次。其中android侧js与native之间的通信使用了webview提供的多种api,接下来我们看一下这些api的特点及优劣。
交互方式总结
Android 端webview与Js通信的方式很多,要了解jsbridge两次通信是否合理,首先要了解下Android通过WebView与JS交互的方式。
- 总体目录
Js主动调用Native
Js主动调Native主要有三种方式
-
通过 WebView的addJavascriptInterface()(@JavascriptInterface)
该方法通过addJavascriptInterface()将java对象映射到Js对象,js端直接调用即可,十分方便。但是该方法在Android4.2(17)之前有较大的安全漏洞,在Android <=4.1.2 (API 16),WebView使用WebKit浏览器引擎,并未正确限制addJavascriptInterface的使用方法,在应用权限范围内,攻击者可以通过Java反射机制实现任意命令执行。在Android >=4.2 (API 17),WebView使用Chromium浏览器引擎,并且限制了Javascript对Java对象方法的调用权限,只有声明了@JavascriptInterace注解的方法才能被Web页面调用。
优点:使用简单
缺点:API17之前有严重的安全漏洞 -
通过 WebViewClient 的shouldOverrideUrlLoading ()拦截url
1.js端通过修改iframe属性触发Android侧WebViewClient的回调方法shouldOverrideUrlLoading ()
2.拦截、解析该 url 的协议
3.如果检测到是预先约定好的协议,就调用相应方法
优点:对Api无要求,不存在安全漏洞,较为通用
缺点:需要js与native侧协商格式,JS获取Android方法的返回值复杂
-
通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()拦截JS对话框alert()、confirm()、prompt()消息
Android通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调分别拦截JS对话框 (即上述三个方法),得到他们的消息内容,然后解析即可。对比三个方法我们可以发现只有prompt()可以返回任意类型的值,操作最全面方便、更加灵活;而alert()对话框没有返回值;confirm()对话框只能返回两种状态(确定 / 取消)两个值,因此promt()方法较为合适
avatar -
总结
对比三种方式如下图
image可以发现,利用WebChromeClient的onJsPrompt()方法拦截js侧的promt(),这种方式最合理
Native主动调用Js
-
通过WebView的loadUrl(),及我们熟知的js注入
通过webview的loadUrl()方法, mWebView.loadUrl("javascript:callJS()"),注意javascript为必加的前缀,callJS()为js对应方法名
特别注意:
1. JS代码调用一定要在 onPageFinished() 回调之后才能调用,否则不会调用。
2. loadurl方法在url过长(2000个字符)时会失败,所以不要尝试将一些js文件通过注入的方式直接使用,What is the maximum length of a URL in different browsers?优点:对Api无要求,不存在安全漏洞,较为通用
缺点:对注入代码长度有限制,且该方法执行会使页面刷新,并且无返回值
-
通过WebView的evaluateJavascript()
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() { @Override public void onReceiveValue(String value) { //此处为 js 返回的结果 } }); }
优点:1. 该方法的执行不会使页面刷新。
2. 有返回值,效率更高、使用更简洁。缺点:1. 要求Android4.4以上
2. onReceiveValue(String value),value会多一对引号,需要特殊处理 -
总结
avatar