Android側webview與Js通信的方式(1)
JsBridge原理介紹
Android側JsBridge一般指 JsBridge,該框架對應ios側的WebViewJavascriptBridge,兩者的實現(xiàn)細節(jié)各有不同综膀,但是總體原理一致豪治。我們主要看一下其Js與Native通信原理的實現(xiàn)叼丑,對于具體的代碼細節(jié)不做深究。
JsBridge集成
-
Js端
集成源碼中的js文件怕犁,WebViewJavascriptBridge.js边篮,注意此處不可以通過注入的方式實現(xiàn),不要被各種講解博客誤導奏甫。
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)) { // 如果是返回數(shù)據(jù)
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調用,該函數(shù)作用:獲取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側
觸發(fā)shouldOverrideUrlLoading方法戈轿,并走第一個if,觸發(fā)handlerReturnData方法
void handlerReturnData(String url) {
// _fetchQueue
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
//取出flushMessageQueue方法中放入responseCallbacks隊列中的callback
CallBackFunction f = responseCallbacks.get(functionName);
//取出js側傳來的數(shù)據(jù)
String data = BridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
//執(zhí)行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執(zhí)行
BridgeHandler handler;
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
} else {
handler = defaultHandler;
}
if (handler != null){
handler.handler(m.getData(), responseFunction);
}
}
}
}
});
}
}
首先將數(shù)據(jù)解析成一個Message的list, 這個Message是自定義的類, 里面包含兩端協(xié)商好格式的信息,最后會執(zhí)行到queueMessage(responseMsg)中
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
走dispatch方法
/**
* 分發(fā)message 必須在主線程才分發(fā)成功
* @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);
// 必須要找主線程才會將數(shù)據(jù)傳遞出去 --- 劃重點
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傳來數(shù)據(jù)阵子,本次交互結束
-
流程圖
問題
看完JsBridge代碼思杯,可能大家都會有疑問,這一次流程中挠进,為什么js側與native側為什么要交互兩次色乾,第一次其實并沒有傳任何有效數(shù)據(jù)過來,是否多余领突。下面我們著重看下這個問題暖璧。
Cordova方案參考
由于業(yè)界hybrid方案并不多,一般大廠的方案又較為復雜君旦,而且網(wǎng)上資料基本沒有任何對該問題的解釋澎办,因此本文參考了云閃付正在使用的hybrid方案cordova的通信邏輯。
Cordova方案相較Jsbridge方案更為重量級金砍,十分復雜局蚀,因此本文并不做深入研究,僅針對其實現(xiàn)的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鹅巍,并發(fā)送一個HEAD請求千扶,并將commondQueue以json串的形式放在請求頭cmds上料祠。
break;
default: // iframe-based.
pokeNativeViaIframe(); // 創(chuàng)建iframe,通過hash值來傳遞commondQueue 或 execIframe.src = "gap://ready"
}
}
}
可以看出有兩種方式县貌,一是新建一個XMLHttpRequest术陶,并發(fā)送一個HEAD請求凑懂,并將commondQueue以json串的形式放在請求頭cmds上煤痕。native側進行攔截;二是創(chuàng)建iframe接谨,通過hash值來傳遞commondQueue 或 execIframe.src = "gap://ready"摆碉,與jsbridge一個原理。
ios端通過UIWebViewDelegate(iframe方式)或 NSURLProtocol攔截(xhr方式)方式接收到commondQueue后脓豪,執(zhí)行插件的實際功能巷帝。
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的特點及優(yōu)劣晶默。
交互方式總結
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反射機制實現(xiàn)任意命令執(zhí)行辫封。在Android >=4.2 (API 17)硝枉,WebView使用Chromium瀏覽器引擎廉丽,并且限制了Javascript對Java對象方法的調用權限,只有聲明了@JavascriptInterace注解的方法才能被Web頁面調用妻味。
優(yōu)點:使用簡單
缺點:API17之前有嚴重的安全漏洞 -
通過 WebViewClient 的shouldOverrideUrlLoading ()攔截url
1.js端通過修改iframe屬性觸發(fā)Android側WebViewClient的回調方法shouldOverrideUrlLoading ()
2.攔截正压、解析該 url 的協(xié)議
3.如果檢測到是預先約定好的協(xié)議,就調用相應方法
優(yōu)點:對Api無要求责球,不存在安全漏洞焦履,較為通用
缺點:需要js與native側協(xié)商格式,JS獲取Android方法的返回值復雜
-
通過 WebChromeClient 的onJsAlert()雏逾、onJsConfirm()嘉裤、onJsPrompt()攔截JS對話框alert()、confirm()栖博、prompt()消息
Android通過 WebChromeClient 的onJsAlert()屑宠、onJsConfirm()、onJsPrompt()方法回調分別攔截JS對話框 (即上述三個方法)仇让,得到他們的消息內容典奉,然后解析即可。對比三個方法我們可以發(fā)現(xiàn)只有prompt()可以返回任意類型的值丧叽,操作最全面方便卫玖、更加靈活;而alert()對話框沒有返回值蠢正;confirm()對話框只能返回兩種狀態(tài)(確定 / 取消)兩個值骇笔,因此promt()方法較為合適
-
總結
對比三種方式如下圖
可以發(fā)現(xiàn),利用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?優(yōu)點:對Api無要求,不存在安全漏洞签财,較為通用
缺點:對注入代碼長度有限制串慰,且該方法執(zhí)行會使頁面刷新,并且無返回值
-
通過WebView的evaluateJavascript()
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() { @Override public void onReceiveValue(String value) { //此處為 js 返回的結果 } }); }
優(yōu)點:1. 該方法的執(zhí)行不會使頁面刷新唱蒸。
2. 有返回值邦鲫,效率更高、使用更簡潔。缺點:1. 要求Android4.4以上
2. onReceiveValue(String value)庆捺,value會多一對引號古今,需要特殊處理 -
總結