一 Android WebView Js 原生API
Android WebView 提供了Js 和 WebView相互调用的接口,js 调用Android 代码通过
- @JavascriptInterface 注解
- WebView.addJavascriptInterface(Object object, String name) 方法
实现JS 和java 对象的映射。
同样 WebView 也提供了 java 调用Js 代码的机制。通过以下两个方法:
- 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 上提供了一个Js Bridage, DSBridge-Android, 分析下实现原理:
一共三个java 文件:
文件 | 功能 |
---|---|
DWebView.java | 继承WebView 封装了Js调用 |
CompletionHandler.java | 处理异步请求使用 |
OnReturnValue.java | 返回值 接口 |
DWebView 类 继承自 WebView 主要包括这几个函数
- init 在WebView 的构造函数中调动,完成一些WebView 的设置。
- injectJs
- evaluateJavascript(final String script);
1. init
init 函数中主要有两个部分处理一个是注册一个 WebChromeClient, 一个是调用addJavascriptInterface 接口注册一个 Js 调用Java的通用api
- super.setWebChromeClient(mWebChromeClient); 在mWebChromeClient 的回调中调用 injectJs 完成Js 注入
- super.addJavascriptInterface(new Object(){}, BRIDGE_NAME),这是DSBridge的核心功能,向 Js 页面注册一个通用的Js 对象,这个对象有一个 call 方法,通过这个call 方法实现对其它 Android Api 的调用,下面主要分析这个方法。
2. js 调用方式
在 js 页面调用 Andoid 代码时通过:dsBridge 为java evaluateJavascript 调用是的Object 在Js的映射对象,然后调用 这个对象的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 调用的函数都封装在 JsApi 这个对象中,注意是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 函数
call 需要使用 JavascriptInterface 注解注释,Js 中的三个参数被到这里被简化为两个参数,原因在Js 代码中分析。
- methodName java 方法名
- args 参数, 注意String 格式其实是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 相关的对象
if (jsb == null) {
Log.e("SynWebView", error);
return "";
}
// 获取注册的Js api 对象的Class 对象
Class<?> cls = jsb.getClass();
try {
Method method;
// 异步标记
boolean asyn = false;
// String 类型的参数转化为 Json 对象
JSONObject arg = new JSONObject(args);
String callback = "";
try {
// 检查 json 对象中是否有_dscbstub 这个Key,如果有表示有回调Js的函数,是一个异步调用,
// 然后移除,那么json对象中保存的都是参数
// 有了对象,知道了对象的方法的String,通过反射获取这个方法。通过反射的参数可知
// 方法的函数签名为: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});
}
// 错误检查
if (method == null) {
error = "ERROR! \n Not find method \"" + methodName + "\" implementation! ";
Log.e("SynWebView", error);
evaluateJavascript(String.format("alert(decodeURIComponent(\"%s\"})", error));
return "";
}
// Js 调用的API 需要使用 @JavascriptInterface 注解,
// 在4.4 以前的平台上有Js 安全漏洞,通过这个注解检查是否合法的API.
// call 函数已经用 @JavascriptInterface 标注,是一个合法的API。jsp 对象由于绕过了WebView
// 的 @JavascriptInterface 注解检查,需要手动校验。
JavascriptInterface annotation = method.getAnnotation(JavascriptInterface.class);
if (annotation != null) {
Object ret;
// 设置方法为可访问的
method.setAccessible(true);
if (asyn) {
// 异步调用, 讲异步调用的逻辑封装在CompletionHandler 中,
// 使用闭包的方式实现callback.
final String cb = callback;
ret = method.invoke(jsb, arg, new CompletionHandler() {
、、、
// 可以再method 方法中调用这个函数,实现异步。
private void complete(String retValue,boolean complete) {
try {
// retValue 为 method 执行的结果,complete 可以控制多次回调。
// 将callback 和参数组合为 javascript 语句,然后通过evaluateJavascript
// 方法调用js 执行
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 {
// 同步调用
ret = method.invoke(jsb, arg);
}
if (ret == null) {
ret = "";
}
// 返回结果
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 的回调中,会调用injectJs 方法注入js,onProgressChanged onReceivedTitle 保证在js 代码运行前 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 调用的Js 代码如下:
function getJsBridge() {
// window 对象的 _dsf 赋值, 如果没有定义过, 则定义为 {};
// dsf 域用来保存 java 调用js 的function.
window._dsf = window._dsf || {};
// 返回 json 一个匿名json对象, json 对象包含两个function:call 和 register。
return {
// call function 包含三个参数, 方法名, 参数,回调函数,
call: function (method, args, cb) {
var ret = "";
// 检查第二个参数类型是否为 function , 如果为function 则表示为回调函数
if (typeof args == "function") {
cb = args;
args = {}
}
// 这一步处理很有技巧,在设置回调函数的时候,回调函数可能为匿名函数,
// 在这里通过window 对象的一个域保存,避免垃圾回收和Java 回调的时候能够找到。
// args 对象中 回调函数的Key 被设置为"_dscbstub", java 中是根据这个名字找到的callback
// 也解释了java 中的call API 为两个参数, Js 中为三个参数的原因。
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 中调
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
}
}
}
}
// 最后把这个匿名对象挂在 window dsBridage 域下。
window.dsBridge = getJsBridge();
7 evaluateJavascript
DWebView 对 evaluateJavascript 做了两次封装,主要解决两个问题:
- 在非主线程中调用的问题, 通过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 调用js
android 的原生方式中java调用js 的接口已经很完善了。DSBridge 使用callHandler。js function 在调用前需要挂到
window._dsf 域下,参考 js代码的 register 函数。
//Register javascript function for Native invocation
dsBridge.register('addValue',function(l,r){
return l+r;
})
java 中调用前指明window._dsf 下的function。 对代码做了一个约束。
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);
}