導(dǎo)語
現(xiàn)在大多數(shù)App與H5的交互越來越多,jsBridge是一個能使webView和js交互的通信方式,本文只對https://github.com/lzyzsd/JsBridge(以下涉及到的jsBridge源碼都是出自這個框架)進(jìn)行分析,只要你懂得了其中的原理,你也可以封裝一個jsBridge。不過在介紹jsBridge的原理前,我會簡單介紹下原始的webView與js交互以及為什么要用jsBridge。
一、WebView與js交互
原始的js交互非常簡單容易理解,直接給出一段客戶端的代碼。
//開啟支持js交互
mWebView.getSettings().setJavaScriptEnabled(true);
//添加js回調(diào)接口,第一個參數(shù)是我們本地寫的一個專門提供方法給H5的js對象;第二個參數(shù)是雙方規(guī)定好的命名,只有注冊的名稱和H5那邊對應(yīng)才可交互。
mWebView.addJavascriptInterface(new JSRequest(), "jsRequest");
class JSRequest{
@JavascriptInterface //只有加了這個注解的方法才能被h5調(diào)用
public void actionFromH5(){
Log.v("JSRequest","H5調(diào)用了該方法");
}
}
// 本地調(diào)用H5的方法用loadUrl實現(xiàn),actionFromNative是在H5里實現(xiàn)的一個方法
mWebView.loadUrl("javascript:actionFromNative()");
二、WebView的js對象注入漏洞
webView的js對象注入的方式非常簡單,可是為什么建議使用jsBridge呢?因為該方式存在安全隱患。上述提到本地方法加了@JavascriptInterface注解才能被h5調(diào)用,這個是在Android4.2之后加的,是為了避免惡意js代碼獲取本地信息,如SD卡中的用戶信息。但是@JavascriptInterface無法兼容4.2以前的版本,所以4.2之前的系統(tǒng)都有被隨時侵入獲取信息的可能。
那么js是如何做到的?答案是反射。4.2之前沒有加@JavascriptInterface的情況下,js是可以通過你注入的js對象(addJavascriptInterface的第一個參數(shù))直接拿到getClass(這個方法是基類Object的方法),然后再拿到Runtime對象用來執(zhí)行一些命令。原理大概就是這樣,如果想具體了解如何實現(xiàn)的,請閱讀WebView的Js對象注入漏洞解決方案。
三、jsBridge源碼分析
jsBridge的最大作用就是解決了WebView的安全隱患,任何版本的系統(tǒng)都是適用的。還是一樣,下面先介紹下jsBridge的用法,一些配置我就不介紹了,直接拿主干部分。
//一些初始化代碼就不展示了
······································
// 第一個參數(shù)在本地注冊一個叫"submitFromWeb"的方法供H5調(diào)用,
// 第二個參數(shù)是實現(xiàn)了BridgeHandler接口的匿名類用來回調(diào)。
webView.registerHandler("submitFromWeb", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
// 這里的data是H5傳給本地的數(shù)據(jù),function.onCallBack是回調(diào)給H5的字符串?dāng)?shù)據(jù)
Log.i(TAG, "handler = submitFromWeb, data from web = " + data);
function.onCallBack("submitFromWeb exe, response data 中文 from Java");
}
});
// 第一個參數(shù)是H5頁面注冊的一個名為"functionInJs"的方法
// 第二個參數(shù)是客戶端本地傳給H5的字符串
// 第三個參數(shù)是實現(xiàn)回調(diào)接口的匿名內(nèi)部類
webView.callHandler("functionInJs", "data from Java", new CallBackFunction() {
@Override
public void onCallBack(String data) {
// TODO Auto-generated method stub
// data是H5返回給客戶端的數(shù)據(jù)
Log.i(TAG, "reponse data from js " + data);
}
});
3.1 H5調(diào)客戶端
jsBridge的源碼是很少的,理解起來不是那么困難,只要一步步往下走就好了,首先我們從registerHandler出發(fā):
// BridgeWebView.java
public void registerHandler(String handlerName, BridgeHandler handler) {
if (handler != null) {
messageHandlers.put(handlerName, handler);// 每個回調(diào)接口都對應(yīng)一個key值(也就是你命名的方法名)
}
}
registerHandler方法就是這么簡單,客戶端操作已經(jīng)到此結(jié)束了。我認(rèn)為jsBridge最神奇的地方就是WebViewJavaScriptBridge.js這個js文件,對于不熟悉H5開發(fā)的同學(xué)可能有點看不懂(包括我),但是其實這個js文件的內(nèi)容和BridgeWebView.java非常類似,大概看懂幾個重要方法的作用即可。下面是一段H5調(diào)用客戶端方法的代碼:
// demo.html
// testClick1方法是H5頁面點擊某個按鈕觸發(fā)的,然后會調(diào)客戶端的方法。
function testClick1() {
// call native method
// 第一個參數(shù)是客戶端命名的方法
// 第二個參數(shù)是傳給客戶端的數(shù)據(jù)
// 第三個參數(shù)是客戶端返回數(shù)據(jù)給H5的回調(diào)方法
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': '中文測試'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
}
但是這個callHandler方法不是H5寫的,而是客戶端本地的WebViewJavaScriptBridge.js文件里的方法,這個文件里的內(nèi)容是直接可以注入到H5頁面(不得不感嘆H5的方便之處)。
// WebViewJavaScriptBridge.js
// 提供給H5的js方法
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
// 對應(yīng)上面方法里的_doSend,在發(fā)送消息隊列中加入消息,觸發(fā)native請求
function _doSend(message, responseCallback) {
// responseCallback按命名理解就是響應(yīng)回調(diào),也就是說是客戶端再傳數(shù)據(jù)給H5的時候用到的
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
// 我的理解是這行代碼會觸發(fā)WebViewClient中的shouldOverrideUrlLoading,這是交互的關(guān)鍵點
// 返回給客戶端的url是"yy://__QUEUE_MESSAGE__/"
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
上面的注釋已經(jīng)寫明H5最終會觸發(fā)WebViewClient中的shouldOverrideUrlLoading:
// BridgeWebViewClient.java
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // url開頭是否是"yy://return/_fetchQueue/",說明是H5要返回數(shù)據(jù)給客戶端了
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //url開頭是否是"yy://__QUEUE_MESSAGE__/",說明H5要調(diào)用客戶端了。
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
上面的代碼已經(jīng)走到H5調(diào)用客戶端了,接下去跟進(jìn)webView.flushMessageQueue()看看:
// BridgeWebView.java
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
// 先不要看這里,因為代碼還沒走到這一步,等回調(diào)的時候才會走這里,下面會有提示再來看。(省略部分代碼)
List<Message> list = null;
try {
list = Message.toArrayList(data);// 解析H5傳過來的Json數(shù)據(jù)
} catch (Exception e) {
e.printStackTrace();
return;
}
for (int i = 0; i < list.size(); i++) {
Message m = list.get(i);
String responseId = m.getResponseId();
// 如果是客戶端調(diào)用H5方法則會有responseId這個值,也就是webView.callHandler
if (!TextUtils.isEmpty(responseId)) {
CallBackFunction function = responseCallbacks.get(responseId);
String responseData = m.getResponseData();
function.onCallBack(responseData);// 回調(diào)到webView.callHandler里面的回調(diào)方法
responseCallbacks.remove(responseId);
} else {// H5調(diào)用客戶端會走這里
CallBackFunction responseFunction = null;
final String callbackId = m.getCallbackId();// 一般情況下都是有callbackId的,這是H5那邊設(shè)置的
if (!TextUtils.isEmpty(callbackId)) {
// 這里實現(xiàn)的回調(diào)接口是提供給客戶端再次去和H5交互的機(jī)會,對應(yīng)webView.registerHandler(name,handler)里面的function
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);// js傳過來的callbackId賦值給responseId回傳給js,這樣就可以配對了。
responseMsg.setResponseData(data);
queueMessage(responseMsg);// 向H5發(fā)送消息
}
};
}
BridgeHandler handler;
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
}
if (handler != null){// 客戶端只有registerHandler后取出來的handler才不為null
// 這一步就是調(diào)到了webView.registerHandler(name,handler)第二個參數(shù)BridgeHandler里了
handler.handler(m.getData(), responseFunction);
}
}
}
}
}
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl); // 加載jsUrl="javascript:WebViewJavascriptBridge._fetchQueue();"
// 鍵值對形式存放響應(yīng)回調(diào)接口,這里的key是"_fetchQueue"
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
現(xiàn)在整理下發(fā)現(xiàn)H5第一次調(diào)客戶端時只是實現(xiàn)一個回調(diào)方法(當(dāng)然這個回調(diào)方法非常重要),然后用鍵值對的方式存儲之后供下次配對。客戶端會再一次loadUrl加載本地js文件中的_fetchQueue()方法:
// WebViewJavaScriptBridge.js
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
// 會觸發(fā)客戶端的shouldOverrideUrlLoading,傳遞url的形式是:"yy://return/_fetchQueue/"+H5給客戶端的數(shù)據(jù)
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
shouldOverrideUrlLoading的代碼已經(jīng)在之前貼出過,這里就不再貼出,然后會調(diào)用webView.handlerReturnData(url):
void handlerReturnData(String url) {
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);// 拿到functionName="_fetchQueue"
CallBackFunction f = responseCallbacks.get(functionName);// 拿到的key就是為配對鍵值對的啊!還記得上面存儲過了嗎?
String data = BridgeUtil.getDataFromReturnUrl(url);// 拿到H5給客戶端的數(shù)據(jù)
if (f != null) {
f.onCallBack(data);// 回調(diào)
responseCallbacks.remove(functionName);
return;
}
}
f.onCallBack(data)就是在flushMessageQueue實現(xiàn)的那個回調(diào)方法啊,所以這個時候就要回去看看那個方法里面具體做了什么(重點已注釋),到此為止H5調(diào)客戶端的方法流程基本已經(jīng)走完,queueMessage(responseMsg)方法就不再具體講了,作用就是向H5發(fā)消息(類似于客戶端調(diào)用H5方法,但是有區(qū)別)。
3.2 客戶端調(diào)用H5方法
我覺得再從源碼一步步講解是沒什么意義的,只要理解了H5調(diào)用客戶端方法就可以了,因為流程和H5調(diào)用客戶端方法是相反的,也就是說WebViewJavaScriptBridge.js和BridgeWebView.java是功能相似的不同語言所寫的文件,接下來我通過一張流程圖過一遍客戶端調(diào)用H5方法 :
總結(jié)
現(xiàn)在的App開發(fā)熟練使用WebView以及和js交互是很有必要的,jsBridge的實現(xiàn)也不復(fù)雜,只要和H5定好協(xié)議,完全可以自己寫一個jsBridge通信方式的框架。而且多閱讀源碼有助于自己的提升,從這些簡單而精妙的源碼入手是再合適不過了。