一 Android WebView Js 原生API
Android WebView 提供了Js 和 WebView相互調(diào)用的接口雁仲,js 調(diào)用Android 代碼通過
- @JavascriptInterface 注解
- WebView.addJavascriptInterface(Object object, String name) 方法
實(shí)現(xiàn)JS 和java 對象的映射。
同樣 WebView 也提供了 java 調(diào)用Js 代碼的機(jī)制鹃答。通過以下兩個(gè)方法:
- WebView.evaluateJavascript(String script, ValueCallback<String> resultCallback);
- WebView.loadUrl(String script); Android 4.4 以下版本使用
private void evaluateJavascript(String script) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.evaluateJavascript(String script, ValueCallback<String> resultCallback);
} else {
WebView.loadUrl(String script);
}
}
二 DSBridge 分析
github 上提供了一個(gè)Js Bridage, DSBridge-Android干厚, 分析下實(shí)現(xiàn)原理:
一共三個(gè)java 文件:
文件 | 功能 |
---|---|
DWebView.java | 繼承WebView 封裝了Js調(diào)用 |
CompletionHandler.java | 處理異步請求使用 |
OnReturnValue.java | 返回值 接口 |
DWebView 類 繼承自 WebView 主要包括這幾個(gè)函數(shù)
- init 在WebView 的構(gòu)造函數(shù)中調(diào)動(dòng)李滴,完成一些WebView 的設(shè)置螃宙。
- injectJs
- evaluateJavascript(final String script);
1. init
init 函數(shù)中主要有兩個(gè)部分處理一個(gè)是注冊一個(gè) WebChromeClient, 一個(gè)是調(diào)用addJavascriptInterface 接口注冊一個(gè) Js 調(diào)用Java的通用api
- super.setWebChromeClient(mWebChromeClient); 在mWebChromeClient 的回調(diào)中調(diào)用 injectJs 完成Js 注入
- super.addJavascriptInterface(new Object(){}, BRIDGE_NAME)所坯,這是DSBridge的核心功能谆扎,向 Js 頁面注冊一個(gè)通用的Js 對象,這個(gè)對象有一個(gè) call 方法芹助,通過這個(gè)call 方法實(shí)現(xiàn)對其它 Android Api 的調(diào)用堂湖,下面主要分析這個(gè)方法。
2. js 調(diào)用方式
在 js 頁面調(diào)用 Andoid 代碼時(shí)通過:dsBridge 為java evaluateJavascript 調(diào)用是的Object 在Js的映射對象状土,然后調(diào)用 這個(gè)對象的call 方法:
// init dsBridge
<script src="https://unpkg.com/dsbridge/dist/dsbridge.js"> </script>
var dsBridge=require("dsbridge")
//Call synchronously
var str=dsBridge.call("testSyn", {msg: "testSyn"});
//Call asynchronously
dsBridge.call("testAsyn", {msg: "testAsyn"}, function (v) {
alert(v);
})
3. Java 注冊 js Api
java 代碼注冊无蜂,js 調(diào)用的函數(shù)都封裝在 JsApi 這個(gè)對象中,注意是DWebView 的 setJavascriptInterface蒙谓,不是原生WebView .
DWebView.setJavascriptInterface(new JsApi());
public class JsApi{
@JavascriptInterface
public void testAsyn(JSONObject jsonObject, CompletionHandler handler) throws JSONException {
handler.complete(jsonObject.getString("msg")+" [ asyn call]");
}
}
4. call 函數(shù)
call 需要使用 JavascriptInterface 注解注釋斥季,Js 中的三個(gè)參數(shù)被到這里被簡化為兩個(gè)參數(shù),原因在Js 代碼中分析累驮。
- methodName java 方法名
- args 參數(shù)酣倾, 注意String 格式其實(shí)是Json 字符串
具體過程看代碼注釋:
@JavascriptInterface
public String call(String methodName, String args) {
String error = "Js bridge method called, but there is " +
"not a JavascriptInterface object, please set JavascriptInterface object first!";
// 首先檢查是否注冊了Js api 相關(guān)的對象
if (jsb == null) {
Log.e("SynWebView", error);
return "";
}
// 獲取注冊的Js api 對象的Class 對象
Class<?> cls = jsb.getClass();
try {
Method method;
// 異步標(biāo)記
boolean asyn = false;
// String 類型的參數(shù)轉(zhuǎn)化為 Json 對象
JSONObject arg = new JSONObject(args);
String callback = "";
try {
// 檢查 json 對象中是否有_dscbstub 這個(gè)Key,如果有表示有回調(diào)Js的函數(shù),是一個(gè)異步調(diào)用谤专,
// 然后移除躁锡,那么json對象中保存的都是參數(shù)
// 有了對象,知道了對象的方法的String置侍,通過反射獲取這個(gè)方法映之。通過反射的參數(shù)可知
// 方法的函數(shù)簽名為:xxxMethod(JSONObject object, CompletionHandler handler);
callback = arg.getString("_dscbstub");
arg.remove("_dscbstub");
method = cls.getDeclaredMethod(methodName,
new Class[]{JSONObject.class, CompletionHandler.class});
asyn = true;
} catch (Exception e) {
method = cls.getDeclaredMethod(methodName, new Class[]{JSONObject.class});
}
// 錯(cuò)誤檢查
if (method == null) {
error = "ERROR! \n Not find method \"" + methodName + "\" implementation! ";
Log.e("SynWebView", error);
evaluateJavascript(String.format("alert(decodeURIComponent(\"%s\"})", error));
return "";
}
// Js 調(diào)用的API 需要使用 @JavascriptInterface 注解,
// 在4.4 以前的平臺(tái)上有Js 安全漏洞蜡坊,通過這個(gè)注解檢查是否合法的API.
// call 函數(shù)已經(jīng)用 @JavascriptInterface 標(biāo)注杠输,是一個(gè)合法的API。jsp 對象由于繞過了WebView
// 的 @JavascriptInterface 注解檢查算色,需要手動(dòng)校驗(yàn)抬伺。
JavascriptInterface annotation = method.getAnnotation(JavascriptInterface.class);
if (annotation != null) {
Object ret;
// 設(shè)置方法為可訪問的
method.setAccessible(true);
if (asyn) {
// 異步調(diào)用, 講異步調(diào)用的邏輯封裝在CompletionHandler 中螟够,
// 使用閉包的方式實(shí)現(xiàn)callback.
final String cb = callback;
ret = method.invoke(jsb, arg, new CompletionHandler() {
灾梦、、妓笙、
// 可以再method 方法中調(diào)用這個(gè)函數(shù)若河,實(shí)現(xiàn)異步。
private void complete(String retValue,boolean complete) {
try {
// retValue 為 method 執(zhí)行的結(jié)果寞宫,complete 可以控制多次回調(diào)萧福。
// 將callback 和參數(shù)組合為 javascript 語句,然后通過evaluateJavascript
// 方法調(diào)用js 執(zhí)行
if (retValue == null) retValue = "";
retValue = URLEncoder.encode(retValue, "UTF-8").replaceAll("\\+", "%20");
String script = String.format("%s(decodeURIComponent(\"%s\"));", cb, retValue);
// 將callback 方法從Html 的window 對象刪除辈赋,原因在js 代碼分析
if(complete) {
script += "delete window."+cb;
}
evaluateJavascript(script);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
});
} else {
// 同步調(diào)用
ret = method.invoke(jsb, arg);
}
if (ret == null) {
ret = "";
}
// 返回結(jié)果
return ret.toString();
} else {
error = "Method " + methodName + " is not invoked, since " +
"it is not declared with JavascriptInterface annotation! ";
evaluateJavascript(String.format("alert('ERROR \\n%s')", error));
Log.e("SynWebView", error);
}
} catch (Exception e) {
evaluateJavascript(String.format("alert('ERROR! \\nCall failed:Function does not exist or parameter is invalid[%s]')", e.getMessage()));
e.printStackTrace();
}
return "";
}
@Keep
@JavascriptInterface
public void returnValue(int id, String value) {
OnReturnValue handler = handlerMap.get(id);
if (handler != null) {
handler.onValue(value);
handlerMap.remove(id);
}
}
}, BRIDGE_NAME);
5. injectJs 注入js 代碼
在WebChromeClient 的回調(diào)中鲫忍,會(huì)調(diào)用injectJs 方法注入js膏燕,onProgressChanged onReceivedTitle 保證在js 代碼運(yùn)行前 js注入完成
private WebChromeClient mWebChromeClient = new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
injectJs();
}
@Override
public void onReceivedTitle(WebView view, String title) {
injectJs();
}
}
private void injectJs() {
evaluateJavascript("function getJsBridge(){window._dsf=window._dsf||{};return{call:function(b,a,c){\"function\"==typeof a&&(c=a,a={});if(\"function\"==typeof c){window.dscb=window.dscb||0;var d=\"dscb\"+window.dscb++;window[d]=c;a._dscbstub=d}a=JSON.stringify(a||{});return window._dswk?prompt(window._dswk+b,a):\"function\"==typeof _dsbridge?_dsbridge(b,a):_dsbridge.call(b,a)},register:function(b,a){\"object\"==typeof b?Object.assign(window._dsf,b):window._dsf[b]=a}}}dsBridge=getJsBridge();");
}
6. javascript 代碼注入分析
injectJs 調(diào)用的Js 代碼如下:
function getJsBridge() {
// window 對象的 _dsf 賦值, 如果沒有定義過悟民, 則定義為 {}坝辫;
// dsf 域用來保存 java 調(diào)用js 的function.
window._dsf = window._dsf || {};
// 返回 json 一個(gè)匿名json對象, json 對象包含兩個(gè)function:call 和 register。
return {
// call function 包含三個(gè)參數(shù)射亏, 方法名近忙, 參數(shù),回調(diào)函數(shù)智润,
call: function (method, args, cb) {
var ret = "";
// 檢查第二個(gè)參數(shù)類型是否為 function , 如果為function 則表示為回調(diào)函數(shù)
if (typeof args == "function") {
cb = args;
args = {}
}
// 這一步處理很有技巧及舍,在設(shè)置回調(diào)函數(shù)的時(shí)候,回調(diào)函數(shù)可能為匿名函數(shù)窟绷,
// 在這里通過window 對象的一個(gè)域保存锯玛,避免垃圾回收和Java 回調(diào)的時(shí)候能夠找到。
// args 對象中 回調(diào)函數(shù)的Key 被設(shè)置為"_dscbstub"兼蜈, java 中是根據(jù)這個(gè)名字找到的callback
// 也解釋了java 中的call API 為兩個(gè)參數(shù)更振, Js 中為三個(gè)參數(shù)的原因。
if (typeof cb == "function") {
window.dscb = window.dscb || 0;
var cbName = "dscb" + window.dscb++;
window[cbName] = cb;
args["_dscbstub"] = cbName
}
args = JSON.stringify(args || {});
if (window._dswk) {
// debug 分支饭尝, window 的 _dswk 域決定
ret = prompt(window._dswk + method, args)
} else {
// _dsbridge 對象為java 中調(diào)
addJavascriptInterface(new Object(){}, BRIDGE_NAME) 映射的JS 對象
if (typeof _dsbridge == "function") {
ret = _dsbridge(method, args)
} else {
// 我們的代碼走這里
ret = _dsbridge.call(method, args)
}
}
return ret
}, register: function (name, fun) {
if (typeof name == "object") {
Object.assign(window._dsf, name)
} else {
window._dsf[name] = fun
}
}
}
}
// 最后把這個(gè)匿名對象掛在 window dsBridage 域下肯腕。
window.dsBridge = getJsBridge();
7 evaluateJavascript
DWebView 對 evaluateJavascript 做了兩次封裝,主要解決兩個(gè)問題:
- 在非主線程中調(diào)用的問題, 通過handler post 到主線程中處理钥平。
- 4.4 以前的版本兼容的問題实撒。
public void evaluateJavascript(final String script) {
if (Looper.getMainLooper() == Looper.myLooper()) {
_evaluateJavascript(script);
} else {
Message msg=new Message();
msg.what=EXEC_SCRIPT;
msg.obj=script;
mainThreadHandler.sendMessage(msg);
}
}
private void _evaluateJavascript(String script) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
DWebView.super.evaluateJavascript(script, null);
} else {
loadUrl("javascript:" + script);
}
}
8 java 調(diào)用js
android 的原生方式中java調(diào)用js 的接口已經(jīng)很完善了。DSBridge 使用callHandler涉瘾。js function 在調(diào)用前需要掛到
window._dsf 域下知态,參考 js代碼的 register 函數(shù)。
//Register javascript function for Native invocation
dsBridge.register('addValue',function(l,r){
return l+r;
})
java 中調(diào)用前指明window._dsf 下的function立叛。 對代碼做了一個(gè)約束负敏。
DWebView.callHandler("addValue",new Object[]{1,"hello"},new OnReturnValue(){
@Override
public void onValue(String retValue) {
Log.d("jsbridge","call succeed,return value is "+retValue);
}
})
public void callHandler(String method, Object[] args, final OnReturnValue handler) {
if (args == null) args = new Object[0];
String arg = new JSONArray(Arrays.asList(args)).toString();
String script = String.format("(window._dsf.%s||window.%s).apply(window._dsf||window,%s)", method,method, arg);
if(handler!=null){
script = String.format("%s.returnValue(%d,%s)",BRIDGE_NAME,callID, script);
handlerMap.put(callID++, handler);
}
evaluateJavascript(script);
}