導(dǎo)語
現(xiàn)在大多數(shù)App與H5的交互越來越多,jsBridge是一個(gè)能使webView和js交互的通信方式,本文只對https://github.com/lzyzsd/JsBridge(以下涉及到的jsBridge源碼都是出自這個(gè)框架)進(jìn)行分析甫菠,只要你懂得了其中的原理册倒,你也可以封裝一個(gè)jsBridge。不過在介紹jsBridge的原理前思犁,我會(huì)簡單介紹下原始的webView與js交互以及為什么要用jsBridge。
一进肯、WebView與js交互
原始的js交互非常簡單容易理解激蹲,直接給出一段客戶端的代碼。
//開啟支持js交互
mWebView.getSettings().setJavaScriptEnabled(true);
//添加js回調(diào)接口江掩,第一個(gè)參數(shù)是我們本地寫的一個(gè)專門提供方法給H5的js對象学辱;第二個(gè)參數(shù)是雙方規(guī)定好的命名,只有注冊的名稱和H5那邊對應(yīng)才可交互环形。
mWebView.addJavascriptInterface(new JSRequest(), "jsRequest");
class JSRequest{
@JavascriptInterface //只有加了這個(gè)注解的方法才能被h5調(diào)用
public void actionFromH5(){
Log.v("JSRequest","H5調(diào)用了該方法");
}
}
// 本地調(diào)用H5的方法用loadUrl實(shí)現(xiàn)策泣,actionFromNative是在H5里實(shí)現(xiàn)的一個(gè)方法
mWebView.loadUrl("javascript:actionFromNative()");
二、WebView的js對象注入漏洞
webView的js對象注入的方式非常簡單抬吟,可是為什么建議使用jsBridge呢萨咕?因?yàn)樵摲绞酱嬖诎踩[患。上述提到本地方法加了@JavascriptInterface注解才能被h5調(diào)用火本,這個(gè)是在Android4.2之后加的危队,是為了避免惡意js代碼獲取本地信息聪建,如SD卡中的用戶信息。但是@JavascriptInterface無法兼容4.2以前的版本茫陆,所以4.2之前的系統(tǒng)都有被隨時(shí)侵入獲取信息的可能金麸。
那么js是如何做到的?答案是反射簿盅。4.2之前沒有加@JavascriptInterface的情況下挥下,js是可以通過你注入的js對象(addJavascriptInterface的第一個(gè)參數(shù))直接拿到getClass(這個(gè)方法是基類Object的方法),然后再拿到Runtime對象用來執(zhí)行一些命令桨醋。原理大概就是這樣棚瘟,如果想具體了解如何實(shí)現(xiàn)的,請閱讀WebView的Js對象注入漏洞解決方案讨盒。
三解取、jsBridge源碼分析
jsBridge的最大作用就是解決了WebView的安全隱患,任何版本的系統(tǒng)都是適用的返顺。還是一樣禀苦,下面先介紹下jsBridge的用法,一些配置我就不介紹了遂鹊,直接拿主干部分振乏。
//一些初始化代碼就不展示了
······································
// 第一個(gè)參數(shù)在本地注冊一個(gè)叫"submitFromWeb"的方法供H5調(diào)用,
// 第二個(gè)參數(shù)是實(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");
}
});
// 第一個(gè)參數(shù)是H5頁面注冊的一個(gè)名為"functionInJs"的方法
// 第二個(gè)參數(shù)是客戶端本地傳給H5的字符串
// 第三個(gè)參數(shù)是實(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);// 每個(gè)回調(diào)接口都對應(yīng)一個(gè)key值(也就是你命名的方法名)
}
}
registerHandler方法就是這么簡單,客戶端操作已經(jīng)到此結(jié)束了秦躯。我認(rèn)為jsBridge最神奇的地方就是WebViewJavaScriptBridge.js這個(gè)js文件忆谓,對于不熟悉H5開發(fā)的同學(xué)可能有點(diǎn)看不懂(包括我),但是其實(shí)這個(gè)js文件的內(nèi)容和BridgeWebView.java非常類似踱承,大概看懂幾個(gè)重要方法的作用即可倡缠。下面是一段H5調(diào)用客戶端方法的代碼:
// demo.html
// testClick1方法是H5頁面點(diǎn)擊某個(gè)按鈕觸發(fā)的,然后會(huì)調(diào)客戶端的方法茎活。
function testClick1() {
// call native method
// 第一個(gè)參數(shù)是客戶端命名的方法
// 第二個(gè)參數(shù)是傳給客戶端的數(shù)據(jù)
// 第三個(gè)參數(shù)是客戶端返回?cái)?shù)據(jù)給H5的回調(diào)方法
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': '中文測試'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
}
但是這個(gè)callHandler方法不是H5寫的昙沦,而是客戶端本地的WebViewJavaScriptBridge.js文件里的方法,這個(gè)文件里的內(nèi)容是直接可以注入到H5頁面(不得不感嘆H5的方便之處)载荔。
// WebViewJavaScriptBridge.js
// 提供給H5的js方法
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
// 對應(yīng)上面方法里的_doSend盾饮,在發(fā)送消息隊(duì)列中加入消息,觸發(fā)native請求
function _doSend(message, responseCallback) {
// responseCallback按命名理解就是響應(yīng)回調(diào),也就是說是客戶端再傳數(shù)據(jù)給H5的時(shí)候用到的
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
// 我的理解是這行代碼會(huì)觸發(fā)WebViewClient中的shouldOverrideUrlLoading丐谋,這是交互的關(guān)鍵點(diǎn)
// 返回給客戶端的url是"yy://__QUEUE_MESSAGE__/"
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
上面的注釋已經(jīng)寫明H5最終會(huì)觸發(fā)WebViewClient中的shouldOverrideUrlLoading:
// BridgeWebViewClient.java
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // url開頭是否是"yy://return/_fetchQueue/"芍碧,說明是H5要返回?cái)?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) {
// 先不要看這里定庵,因?yàn)榇a還沒走到這一步吏饿,等回調(diào)的時(shí)候才會(huì)走這里,下面會(huì)有提示再來看蔬浙。(省略部分代碼)
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方法則會(huì)有responseId這個(gè)值猪落,也就是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)用客戶端會(huì)走這里
CallBackFunction responseFunction = null;
final String callbackId = m.getCallbackId();// 一般情況下都是有callbackId的,這是H5那邊設(shè)置的
if (!TextUtils.isEmpty(callbackId)) {
// 這里實(shí)現(xiàn)的回調(diào)接口是提供給客戶端再次去和H5交互的機(jī)會(huì)畴博,對應(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)第二個(gè)參數(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)客戶端時(shí)只是實(shí)現(xiàn)一個(gè)回調(diào)方法(當(dāng)然這個(gè)回調(diào)方法非常重要)官疲,然后用鍵值對的方式存儲(chǔ)之后供下次配對×料叮客戶端會(huì)再一次loadUrl加載本地js文件中的_fetchQueue()方法:
// WebViewJavaScriptBridge.js
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
// 會(huì)觸發(fā)客戶端的shouldOverrideUrlLoading途凫,傳遞url的形式是:"yy://return/_fetchQueue/"+H5給客戶端的數(shù)據(jù)
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
shouldOverrideUrlLoading的代碼已經(jīng)在之前貼出過,這里就不再貼出溢吻,然后會(huì)調(diào)用webView.handlerReturnData(url):
void handlerReturnData(String url) {
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);// 拿到functionName="_fetchQueue"
CallBackFunction f = responseCallbacks.get(functionName);// 拿到的key就是為配對鍵值對的拔选!還記得上面存儲(chǔ)過了嗎促王?
String data = BridgeUtil.getDataFromReturnUrl(url);// 拿到H5給客戶端的數(shù)據(jù)
if (f != null) {
f.onCallBack(data);// 回調(diào)
responseCallbacks.remove(functionName);
return;
}
}
f.onCallBack(data)就是在flushMessageQueue實(shí)現(xiàn)的那個(gè)回調(diào)方法啊犀盟,所以這個(gè)時(shí)候就要回去看看那個(gè)方法里面具體做了什么(重點(diǎn)已注釋),到此為止H5調(diào)客戶端的方法流程基本已經(jīng)走完蝇狼,queueMessage(responseMsg)方法就不再具體講了阅畴,作用就是向H5發(fā)消息(類似于客戶端調(diào)用H5方法,但是有區(qū)別)题翰。
3.2 客戶端調(diào)用H5方法
我覺得再從源碼一步步講解是沒什么意義的恶阴,只要理解了H5調(diào)用客戶端方法就可以了,因?yàn)榱鞒毯虷5調(diào)用客戶端方法是相反的豹障,也就是說WebViewJavaScriptBridge.js和BridgeWebView.java是功能相似的不同語言所寫的文件冯事,接下來我通過一張流程圖過一遍客戶端調(diào)用H5方法 :
總結(jié)
現(xiàn)在的App開發(fā)熟練使用WebView以及和js交互是很有必要的,jsBridge的實(shí)現(xiàn)也不復(fù)雜血公,只要和H5定好協(xié)議昵仅,完全可以自己寫一個(gè)jsBridge通信方式的框架。而且多閱讀源碼有助于自己的提升,從這些簡單而精妙的源碼入手是再合適不過了摔笤。