前言
本文的介绍了WebView以及JsBridge的相关知识,所涉及到的Demo托管在gitlib上。
WebView
WebView是android中加载网页的一个控件,通过该控件可以加载网络/本地的HTML页面,同时也可以运行网络/本地的JS代码。
在WebView中,你可以通过特定方法进行JS和Native之间的相互调用,对于Native调用JS,有以下2种常用方法:
- loadUrl方法
- evaluateJavascript方法
而在页面中使用JS调用Native,在android的WebView中提供了以下常用的3种方法,相应的在JS中需要使用对应的调用函数:
- addJavascriptInterface方法
- WebViewClient中的shouldOverrideUrlLoading方法
- WebChromeClient中的onJsxxx方法
Native调用JS方法
进行该操作的前提是需要在WebView中开启对JS的支持,需要在初始化WebView时添加如下代码:
// 设置与Js交互的权限
webSettings.setJavaScriptEnabled(true);
loadUrl
通过WebView中的loadUrl方法,在传入的参数中添加以“javascript:”为前缀,以对应的JS方法为后缀的字符串,使用该方法运行JS代码,在onPageFinished后才有效:
/**
* 使用loadUrl来调用js
*/
private void callJavaScriptByLoad(){
//参数名需要以javaScript为开头,方法名必须与js中对应。
mWebView.loadUrl("javascript:callJS()");
}
evaluateJavascript
evaluateJavascript提供了一个性能更佳高效的选择,该方法不会去刷新页面,从而提高效率,且支持直接通过接口获取JS处理后的回调,但是只支持4.4+以上的系统。
/**
* 使用evaluateJavascript方法,该方法需要API>19
* 不会去刷新页面,因此效率更高
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private void callJSByEvaluate(){
// 只需要将第一种方法的loadUrl()换成下面该方法即可
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
Toast.makeText(WebViewActivity.this, value, Toast.LENGTH_SHORT).show();
}
});
}
以上均会调用JS中名为callJS的函数。
JS调用Native方法
addJavascriptInterface
addJavascriptInterface是WebView提供的一个可以让JS直接访问Native的方法。
1.声明一个提供JS调用的类,该类提供JS使用的方法
public class JSCallJava extends Object{
private Context mContext;
public JSCallJava(Context context){
mContext = context;
}
@JavascriptInterface
public String callJava(){
Toast.makeText(mContext, "hello java", Toast.LENGTH_SHORT).show();
return "fromJavascriptInterface";
}
}
2.在addJavascriptInterface中传入需要提供给JS使用的类,以及对应的在JS中的对象名
/**
* 第1种:
* 映射的JS对象名
* 4.2版本前直接使用该方法存在严重漏洞
*/
@SuppressLint("JavascriptInterface")
private void forJSByJSInterface() {
//@param o Java方法的对象
//@param jsMethodName Javascript对象名
mWebView.addJavascriptInterface(new JSCallJava(this), "caller");
}
3.在JS代码中调用对应native方法:
function testClick1() {
//call native method
var result = caller.callJava();
document.getElementById("text3").value = result;
}
需要注意的是,在早期版本中(4.2版本前),该方式存在严重的漏洞,攻击者可以通过JS加载java中的反射类去执行本地命令,虽然在高版本中通过使用@JavascriptInterface进行规避该漏洞,但是老版本的问题依旧存在。
WebViewClient的shouldOverrideUrlLoading
WebView中,可以通过拦截URL跳转来实现JS调用Native方法,通过自定义uri协议格式来传递值,在WebViewClient的shouldOverrideUrlLoading中进行拦截处理。
1.为WebView设置WebViewClient,拦截URL跳转:
mWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri = Uri.parse(url);
if ("js".equals(uri.getScheme())) {
if ("webview".equals(uri.getAuthority())) {
Toast.makeText(WebViewActivity.this, "shouldOverrideUrlLoading", Toast.LENGTH_SHORT).show();
}
//不进行跳转
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
});
2.在JS中通过设置URL来进行调用
function testClick() {
/*约定的url协议为:js://webview?arg1=111&arg2=222*/
document.location = "js://webview?arg1=111&arg2=222";
}
WebChromeClient的onJsxxx
通过设置WebChromeClient,拦截JS的对话框来实现对Native的调用。
在WebChromeClient存在三种弹框,分别为alert,confirm和prompt。三者除了弹窗级别不一样,还有返回的参数不同,其中prompt相对其他两个在JS使用率低,同时可以返回任意参数,所以prompt是最合适用与调用Native的入口。
1.设置WebChromeClient,重写onJsPrompt方法
mWebView.setWebChromeClient(new WebChromeClient(){
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 根据协议的参数,判断是否是所需要的url(原理同方式2)
// 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
//假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
Uri uri = Uri.parse(message);
// 如果url的协议 = 预先约定的 js 协议
// 就解析往下解析参数
if ( uri.getScheme().equals("js")) {
// 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议
// 所以拦截url,下面JS开始调用Android需要的方法
if (uri.getAuthority().equals("demo")) {
//
// 执行JS所需要调用的逻辑
Toast.makeText(WebViewActivity.this, "js调用了Android的方法", Toast.LENGTH_SHORT).show();
// 可以在协议上带有参数并传递到Android上
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
//参数result:代表消息框的返回值(输入值)
result.confirm("fromPrompt");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
});
2.JS中使用弹窗进行回调:
function testClick2() {
//call native method
// 调用prompt()
var result=prompt("js://demo?arg1=111&arg2=222");
document.getElementById("text3").value = result;
}
JsBridge
JsBridge是一个Native和Html交互的一个桥梁,类似RPC的一个控件,使用该控件可以方便的实现JS和Native之间的相互调用和数据的传递。
原理简介
在JsBridge中,最重要的是要制定如下两个协议:
- 数据的序列化协议
- 调用控制协议
前者使用了json和URL,而后者,在java层中,以下三个接口中可以查看:
对于协议的实现,对应在Native和JS的桥接类:
- BridgeWebView
- WebViewJavascriptBridge.js
在WebView中提到了2中调用JS的方法以及3种调用Java的方法,而在JsBridge中,其实就是使用了loadUrl和shouldOverrideUrlLoading,来实现JS和Native的互调。
在实际使用中,需要用BridgeWebView替代WebView,该类在init时,会默认去加载一个BridgeWebViewClient,而Client重写了shouldOverrideUrlLoading来对从JS获取到的数据进行分发,另外,也在onPageFinished中将WebViewJavascriptBridge.js加载到html页面中。
使用
在Android Studio中使用JsBridge,需要在project的build.gradle中添加maven库:
maven { url "https://jitpack.io" }
然后再module中添加如下依赖:
dependencies {
compile 'com.github.lzyzsd:jsbridge:1.0.4'
}
在使用前,可以分别在Native和HTML中设置默认的handle,以处理未指定handle的调用。
Native中:
mBridgeWebView.setDefaultHandler(new DefaultHandler());
HTML中:
bridge.init(function(message, responseCallback) {
…
responseCallback(data);
});
Native调用JS方法
JsBridge通过实现一个自定义的WebView——BridgeWebView以及在JS层提供WebViewJavascriptBridge.js来实现两端使用代码的简化和统一。所有的操作最后都会在handle中处理,
在Native中,最简单的使用,通过send方法调用JS
mBridgeWebView.send(data);
该方法还可以设置一个回调,接收从JS获取到的结果:
mBridgeWebView.send(data, new CallBackFunction() {
@Override
public void onCallBack(String data) {
}
});
send方法会使用JS中默认的handle进行数据的处理。
更多时候,我们需要使用自定义的方法,这时我们需要在Native中使用callHandler来调用JS:
mBridgeWebView.callHandler(handlerName, data, new CallBackFunction() {
@Override
public void onCallBack(String data) {
…
}
});
该方法可以指定JS中的handle来处理该数据,当然你需要在JS中注册对应的handle:
bridge.registerHandler(handlerName, function(data, responseCallback) {
…
responseCallback(responseData);
});
这样就完成了从Native到JS的一次调用。
JS调用Native方法
因为JsBridge在WebViewJavascriptBridge.js的封装,我们可以在JS中以类似的步骤实现对Native层调用。
使用send方法调用Java中默认的handle:
window.WebViewJavascriptBridge.send(
data
, function(responseData) {
…
}
);
使用callHandle调用Native中指定的方法处理:
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': '中文测试'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
相应的,你需要在Native中注册好对应的handle来处理事件:
mBridgeWebView.registerHandler(handleName, new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
…
}
});
以上就可完成一次JS到Native的调用。
XGBridge
XGBridge是参考了一起赚/一起逛项目中的Bridge的实现,以及对JsBridge的二次封装。Demo托管在gitlib上的jsbridgedemo。大致的结构如下图所示:
XGBridge封装在baseSDK中,而Native和JS之间互相调用方法需要依赖一些本地视图以及业务相关,所以抽离出来到project中实现,对应的JavaApi封装了JS使用的Native代码,JSApi则提供了可调用的JS方法。
XGBridge通过抽离WebView降低耦合度,其实例化依赖于WebView,因此,在含有WebView的界面中,通过如下构造方法完成实例化即可:
mXGBridge = new XGBridge(mWebView);
Api的实现,依赖于XGBridge中提供的以下两个方法实现通信:
- callHandler
- registerXGBridgeHandler
而具体的处理方法,需要和前端一起制定统一的调用协议,同时可以根据需求不断添加,完善。
Native调用JS方法
所有JS方法都将在JSApi中封装,其通过XGBridge中设置需要调用的JS方法名,传入参数以及回答借口实现。比如一个getJSContent来获取H5页面中的特定数据,JSApi中实现如下(需要对应在JS中实现getJSContent方法):
public class JSApi{
…
/**
* 获取H5的信息
* @param callback JS处理后的回调,包括其处理后的返回参数
*/
public void getJSContent(CallBackFunction callback){
mXGBridge.callHandler("getJSContent", null, callback);
}
}
在需要使用的Activity中,实例化JSApi,然后在需要的事件中调用JS方法,Demo中展示了一个按钮触发该事件的一个过程:
//实例化JSApi
mJSApi = new DemoJSApi(mXGBridge);
…
//通过按钮触发,调用JS代码
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//调用JS代码
mJSApi.getJSContent(new CallBackFunction() {
@Override
public void onCallBack(String data) {
Toast.makeText(H5Activity.this, data, Toast.LENGTH_SHORT).show();
}
});
}
});
JS调用Native方法
JavaApi提供了给JS调用的handler,通过注册这些方法,实现JS端对Native端端调用。
编写一个Api,需要实现IXGBridgeHandler接口,该接口需要传入一个方法名(对应JS中调用的方法名),以及一个handler来执行相应的处理,比如提供JS关于Android系统的版本号等信息的IXGBridgeHandler:
public static IXGBridgeHandler getNativeInfo(final Context context){
return new IXGBridgeHandler() {
@Override
public String getMethodName() {
return "getNativeInfo";
}
@Override
public void handler(String data, CallBackFunction function) {
Toast.makeText(context, data, Toast.LENGTH_SHORT).show();
function.onCallBack(android.os.Build.VERSION.RELEASE);
}
};
}
然后在需要使用的Activity中进行注册:
//注册Java的handler,用于JS调用
mXGBridge.registerXGBridgeHandler(JavaApi.getNativeInfo(this));
mXGBridge.registerXGBridgeHandler(JavaApi.setTitle(this));
总结
根据以上整理总结,对于Native调用JS的几个方法对比如下:
方法 | 发送数据 | 回调接口 | 兼容性 |
---|---|---|---|
loadUrl | ✓ | ✕ | ✓ |
evaluateJavascript | ✓ | ✓ | 仅4.4以上支持 |
JsBridge | ✓ | ✓ | ✓ |
JS调用Native的对比:
方法 | 发送数据 | 回调接口 | 安全性 |
---|---|---|---|
addJavascriptInterface | ✓ | ✓ | 4.2以下存在严重的漏洞 |
shouldOverrideUrlLoading | ✓ | ✕ | ✓ |
onJsPrompt | ✓ | ✓ | ✓ |
JsBridge | ✓ | ✓ | ✓ |