前言
HyBrid俗称混合开发。使用Android提供的组件——WebView,去加载放在本地或者服务器用h5编写的UI界面;js与java之间的互调,使得HyBrid APP的体验更加趋向原生APP。本章内容主要讲述在h5页面调用Java方法、在Android里调用h5里的js方法和jsbridge的简单实现。因此,拥有h5、css和js更容易上手。
三种APP开发方式比较
安全漏洞
在Android 4.2以下的WebView有个安全漏洞,外部网页通过得到Runtime
对象,然后执行系统命令得到信息,原因出在addJavascriptInterface()
方法。下面是漏洞的简单描述:
1、向WebView注册了一个叫“InterfaceName”
的对象
2、js中可以访问到“InterfaceName”
对象
3、js中通过“getClass”
方法获取该对象的类型类
4、通过反射机制,得到该类的Runtime
对象
5、调用静态方法执行系统命令
核心代码示例:
<script type="text/javascript">
function execute(cmd) {
return demo.getClass().forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec(cmd);
}
execute(["ls", "/mnt/sdcard"]);
</script>
解决方案:
- Android 4.2以上:
@JavascriptInterface
- Android 4.2以下:自定义js和Android交互方式
因此,在讲述js与java互调时基于Android 4.2以上。这个了解了解就好。
项目结构
JS调用Java方法
在main
目录,选择new->Folder->Assets Folder
,完成assets
目录创建。然后新建一个文件夹,命名为jscalljava
。接着新建一个空白的html文件命名为index
。
新建一个空Activity,命名为JSAndJavaActivity
,且设置为启动Activity,代码如下:
public class JSAndJavaActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jsand_java);
webView = findViewById(R.id.web_view);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
// 第二个参数可以简单理解为android表示AndroidAndrJsInterface的对象
// 在js,通过它调用AndroidAndrJsInterface类下的方法
// 名字可以自定义
webView.addJavascriptInterface(new AndroidAndrJsInterface(), "android");
webView.setWebViewClient(new WebViewClient());
webView.loadUrl("file:///android_asset/jscalljava/index.html");
}
class AndroidAndrJsInterface {
// 该注解可以解决Android 4.2以上的安全漏洞,4.2以下没有这个注解
@JavascriptInterface
public void showToast() {
Toast.makeText(JSAndJavaActivity.this, "我被js调用了", Toast.LENGTH_LONG).show();
}
@JavascriptInterface
public void showToast(String info) {
Toast.makeText(JSAndJavaActivity.this, "来自js的消息:" + info, Toast.LENGTH_LONG).show();
}
}
}
布局文件activity_jsand_java.xml
的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".JSAndJavaActivity">
<WebView
android:id="@+id/web_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
为index.html
添加如下代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<style type="text/css">
div {
margin: 0 auto;
width: 200px;
}
button {
width: 150px;
font-weight: bolder;
font-family: "微软雅黑";
}
</style>
</head>
<body>
<div>
<p>
<button onclick="android.showToast()">调用Java无参方法</button>
</p>
<p>
<button onclick="android.showToast('I am come from js.')">调用Java有参方法</button>
</p>
</div>
</body>
</html>
Java调用JS方法
示例代码主要演示以下内容:
- Android调用js的无参函数
- Android调用js的有参函数
- Android调用js的函数并获取返回值
在assets目录下新建文件夹,命名为javacalljs
。然后新建一个空的html,命名为index
。
新建一个空Activity,命名为JavaAndJSActivity
,且设置为启动Activity,代码如下:
public class JavaAndJSActivity extends AppCompatActivity implements View.OnClickListener {
private Button btnNoParamter;
private Button btnYesParamter;
private Button btnNoParamterAndReturn;
// 加载网页或者说H5页面
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_java_and_js);
btnNoParamter = findViewById(R.id.btn_no_parameter);
btnYesParamter = findViewById(R.id.btn_yes_parmater);
btnNoParamterAndReturn = findViewById(R.id.btn_no_parmater_return);
btnNoParamter.setOnClickListener(this);
btnYesParamter.setOnClickListener(this);
btnNoParamterAndReturn.setOnClickListener(this);
webView = new WebView(this);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true); // 设置支持js脚本语言
settings.setUseWideViewPort(true); // 支持双击-前提是页面要支持才显示
settings.setBuiltInZoomControls(true); // 支持缩放按钮-前提是页面要支持才显示
webView.setWebViewClient(new WebViewClient()); // 不跳转到默认浏览器
webView.setWebChromeClient(new WebChromeClient()); // 支持js弹窗
webView.addJavascriptInterface(new GetJsResult(), "Result");
// 加载本地文件:file:///android_asset/文件具体路径
// 网络资源,如:http://www.baidu.com
// 此处asset后面是没有s的
webView.loadUrl("file:///android_asset/javacalljs/index.html"); // 加载网络资源(需要网络权限),也可以时assets目录下的资源
// 加载h5写的页面,会替换当前原生页面,在这里不需要
// setContentView(webView);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
// 格式:WebView.loadUrl("javascript:js方法")
case R.id.btn_no_parameter:
// 调用js无参函数
webView.loadUrl("javascript:noParamter()");
break;
case R.id.btn_yes_parmater:
String info = "Hello,I am come from java.";
// 调用js有参参数
// 传递字符串要加个单引号,数字可以不加;传递数组可以传递json格式的字符串
webView.loadUrl("javascript:yesParamter('" + info + "')");
break;
case R.id.btn_no_parmater_return:
webView.loadUrl("javascript:returnResult()");
break;
default:
break;
}
}
class GetJsResult {
@JavascriptInterface
public void getResult(String res) {
Toast.makeText(JavaAndJSActivity.this, "js返回的结果:" + res, Toast.LENGTH_SHORT).show();
}
}
}
布局文件activity_java_and_js.xml
的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_no_parameter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="调用无参方法"
app:layout_constraintBottom_toTopOf="@+id/btn_yes_parmater"
app:layout_constraintEnd_toEndOf="@+id/btn_yes_parmater"
app:layout_constraintStart_toStartOf="@+id/btn_yes_parmater" />
<Button
android:id="@+id/btn_yes_parmater"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="调用有参方法"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_no_parmater_return"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="调用无参且有返回值"
app:layout_constraintEnd_toEndOf="@+id/btn_yes_parmater"
app:layout_constraintStart_toStartOf="@+id/btn_yes_parmater"
app:layout_constraintTop_toBottomOf="@+id/btn_yes_parmater" />
</android.support.constraint.ConstraintLayout>
为index.html
添加如下代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>java调用js</title>
<script type="text/javascript">
function noParamter() {
alert("我是js无参函数");
}
function yesParamter(info) {
alert("来自java的信息:" + info);
}
function returnResult() {
var a = "我处理完了。"
// 将结果返回给Android
window.Result.getResult(a);
}
</script>
</head>
<body>
</body>
</html>
小结:java和js互调的基本操作就讲完了,一些要注意地方已经在代码中注释了。
JSBridge的实现
JSBridge位置处于js和java之间,如下图所示:
在前面也曾提过,在Android 4.2以下的
addJavascriptInterface()
方法存在漏洞,其解决方案是JSBridge
。简单来说就是自定义协议,暴漏对app没影响的信息,有影响的隐藏掉。当然,Android 4.2以上也可以使用这个方案。
java调用js依然采用WebView.loadUrl()
,而js调用java就不能再采用addJavascriptInterface()
了,需要换一个思路。WebChromeClient
类,我们已经接触过了,它允许app显示js的弹窗。常见的弹窗有alert
(警告框)、confirm
(确认框)和prompt
(提示框),前两个出现的频率相对后者更高,而prompt
更加适合用来传递信息到Android,以下是它们在Android对应的源码实现:
/**
* Tell the client to display a javascript alert dialog. If the client
* returns {@code true}, WebView will assume that the client will handle the
* dialog. If the client returns {@code false}, it will continue execution.
* @param view The WebView that initiated the callback.
* @param url The url of the page requesting the dialog.
* @param message Message to be displayed in the window.
* @param result A JsResult to confirm that the user hit enter.
* @return boolean Whether the client will handle the alert dialog.
*/
public boolean onJsAlert(WebView view, String url, String message,
JsResult result) {
return false;
}
/**
* Tell the client to display a confirm dialog to the user. If the client
* returns {@code true}, WebView will assume that the client will handle the
* confirm dialog and call the appropriate JsResult method. If the
* client returns false, a default value of {@code false} will be returned to
* javascript. The default behavior is to return {@code false}.
* @param view The WebView that initiated the callback.
* @param url The url of the page requesting the dialog.
* @param message Message to be displayed in the window.
* @param result A JsResult used to send the user's response to
* javascript.
* @return boolean Whether the client will handle the confirm dialog.
*/
public boolean onJsConfirm(WebView view, String url, String message,
JsResult result) {
return false;
}
/**
* Tell the client to display a prompt dialog to the user. If the client
* returns {@code true}, WebView will assume that the client will handle the
* prompt dialog and call the appropriate JsPromptResult method. If the
* client returns false, a default value of {@code false} will be returned to to
* javascript. The default behavior is to return {@code false}.
* @param view The WebView that initiated the callback.
* @param url The url of the page requesting the dialog.
* @param message Message to be displayed in the window.
* @param defaultValue The default value displayed in the prompt dialog.
* @param result A JsPromptResult used to send the user's reponse to
* javascript.
* @return boolean Whether the client will handle the prompt dialog.
*/
public boolean onJsPrompt(WebView view, String url, String message,
String defaultValue, JsPromptResult result) {
return false;
}
当app接受到要显示js的弹窗时,会根据弹窗的类型执行相应的方法,如prompt()
对应onJsPrompt()
。所以,我们可以重写onJsPrompt()
方法,请求处理完后将其拦截,也就是返回true,那这个弹窗就不会显示了。换句话说,可以在这调用已经写好的Java方法。
接下就是要解决自定义协议了。我们可以模仿http的url格式,http://host:port/param=value,转换过来,JSBridge://className:callbackAddress/methodName?jsonObj。js向Android发送信息(url)必须按这个格式,而Java层只处理符合这个协议(格式)的请求,其它的一概不处理。下面对这个协议进行解释:
-
JSBridge
:便于检验该url是否合格 -
className
:要暴露出去的类的名字,但它不是js要调用的目标类,在本demo中是JSBridge
-
callbackAddress
:js回调函数存在数组的位置,也就是下标 -
methodName
:js要调用的方法,它的具体参数(比如个数)是无法得知的,在本demo中是showToast
-
jsonObj
:真正传递给Android的信息,要求是json格式的字符串,至于具体是什么格式看需求了
最后,将协议转换成代码。
根据上述的项目结构图,在assets/jsbridge
目录下新建空白的index.html
和JSBridge.js
文件。新建CallBack
类,负责将Java方法的执行结果通知js,其代码如下:
public class CallBack {
private String mPort;
private WebView mWebView;
public CallBack(WebView webView, String mPort) {
this.mPort = mPort;
this.mWebView = webView;
}
/**
* 通知js
* @param jsonObject Java层处理完后返回给js层的信息
*/
public void apply(JSONObject jsonObject) {
if (mWebView != null) {
mWebView.loadUrl("javascript:onAndroidFinished('" + mPort + "', " + String.valueOf(jsonObject) + ")");
}
Log.d("TAG", "CallBack:apply");
}
}
新建Methods
类,用于封装供js调用的方法且有以下约定:
- 方法必须是
public
和static
的 - 参数必须有
3
个 - 第一个参数必须是
WebView
,第二个参数必须是JSONObject
,第三个参数必须是CallBack
只有满足以上三个条件的方法才能被js调用,才会暴露出去。其代码如下:
public class Methods {
public static void showToast(WebView view, JSONObject param, CallBack callBack) {
// 解析得到key=msg的值
String message = param.optString("msg");
Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show();
if (callBack != null) {
try {
JSONObject result = new JSONObject();
result.put("key", "value");
result.put("key1", "value1");
callBack.apply(result);
} catch (JSONException e) {
e.printStackTrace();
}
}
}
}
新建JSBridge
类,负责管理暴露给js的类和方法,以及根据js传入的url内容找到对应的java类,并执行指定的Java方法,代码如下:
public class JSBridge {
// 存储需要暴露给js的方法
private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>();
/**
* 注册要暴露的类
* @param exposeName JSBridge
* @param classz 要暴露的类
*/
public static void register(String exposeName, Class<?> classz) {
// 将符合要求的classz类中的所有方法添加到exposedMethods中
if (!exposedMethods.containsKey(exposeName)) {
exposedMethods.put(exposeName, getAllMethod(classz));
}
Log.d("TAG", "JSBridge:register");
}
private static HashMap<String, Method> getAllMethod(Class injectedCls) {
HashMap<String, Method> methodHashMap = new HashMap<>();
// 获取该类的所有方法
Method[] methods = injectedCls.getDeclaredMethods();
for (Method method : methods) {
// 剔除不符合要求的方法
if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || method.getName() == null) {
continue;
}
// 方法的参数
Class[] paramters = method.getParameterTypes();
// 进一步寻找符合要求的方法
if (paramters != null && paramters.length == 3) {
if (paramters[0] == WebView.class && paramters[1] == JSONObject.class && paramters[2] == CallBack.class) {
methodHashMap.put(method.getName(), method);
}
}
}
return methodHashMap;
}
/**
* 调用相应的java方法去处理js的请求
* @param webView WebView
* @param urlString 根据协议,js层给java传递的信息
* @return null
*/
public static String callJava(WebView webView, String urlString) {
String className = "";
String methodName = "";
String param = "";
String port = "";
// 验证该urlString是否符合协议的基本要求
if (!urlString.equals("") && urlString != null && urlString.startsWith("JSBridge")) {
Uri uri = Uri.parse(urlString);
className = uri.getHost(); // 要调用的类
param = uri.getQuery(); // js层给Java层传递的信息(json格式)
port = uri.getPort() + ""; // js层回调函数的地址
methodName = uri.getPath().replace("/", ""); // 要调用的方法
if (exposedMethods.containsKey(className)) {
// 找到该类的所有符合要求的方法
HashMap<String, Method> methodHashMap = exposedMethods.get(className);
if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
// 根据方法名找到指定的方法
Method method = methodHashMap.get(methodName);
if (method != null) {
try {
// 在这里真正处理js的请求,CallBack用于告诉js层我的活干完了,该你了
method.invoke(null, webView, new JSONObject(param), new CallBack(webView, port));
Log.d("TAG", "JSBridge:callJava");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
return null;
}
}
新建JSBridgeChromeClient
类且继承WebChromeClient
,在此类处理js的请求,代码如下:
public class JSBridgeChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 在简单讲述是否调用result.confirm()的区别
// 如果调用,js的回调函数才会被调用,参数的值是返回给js的
// 如果没调用,js即使有回调函数也不会执行
// 可以使用console.log()的方式来调式js,项目运行起来后可在run窗口查看
result.confirm(JSBridge.callJava(view, message));
// JSBridge.callJava(view, message);
Log.d("TAG", "JSBridgeChromeClient");
return true;
}
}
新建JSBridgeActivity
且设置为启动Activity,代码如下:
public class JSBridgeActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jsbridge);
webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.setWebChromeClient(new JSBridgeChromeClient());
webView.loadUrl("file:///android_asset/jsbridge/index.html");
JSBridge.register("JSBridge", Methods.class);
Log.d("TAG", "JSBridgeActivity");
}
}
布局文件activity_jsbridge.xml
的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".jsbridge.JSBridgeActivity">
<WebView
android:id="@+id/webView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
JSBridge.js
的代码如下:
var callbacks = new Array();
/**
* js层调用Android层方法
* @param {Object} obj Android层的类
* @param {Object} method 该obj中的那个方法
* @param {Object} params 使用json的数据格式给Android传递信息
* @param {Object} callback js层的回调方法,当Android层处理好了js层要如何处理
*/
function jsCallAndroid(obj, method, params, callback) {
// 保存callback回调函数
var port = callbacks.length;
callbacks[port] = callback;
// 组合出符合规则的url,并传递给Java层
var url = 'JSBridge://' + obj + ':' + port + '/' + method + '?' + JSON.stringify(params);
window.prompt(url);
}
/**
* 当js调用完Android层时执行
* @param {Object} port 回调函数的地址,也就是在数组中的位置
* @param {Object} jsonObj 从Android层传过来的参数
*/
function onAndroidFinished(port, jsonObj) {
// 从callbacks取出对应的回调函数
var callback = callbacks[port];
callback(jsonObj);
// 从callbacks中删除callback
delete callbacks[port];
}
index.html
的代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript" src="JSBridge.js" ></script>
</head>
<body>
<button onclick="jsCallAndroid('JSBridge', 'showToast',
{'msg':'hello I come from js.'},function(res) {alert(JSON.stringify(res))})">Js调用Android</button>
</body>
</html>
对函数的调用过程进行概括:点击按钮,触发jsCallAndroid()
方法,通过调用window.prompt(url)
向Android发送请求(信息)。然后在JSBridgeChromeClient.onJsPrompt()
方法对请求进行拦截处理,就是实现了js调用java,JSBridge.callJava(view, message)
。执行到callJava()
方法内部,会调用Methods.showToast()
方法,紧接着会调用CallBack.apply(result)
方法,最后是调用js的onAndroidFinished()
方法。如果JsPromptResult.confirm()
被调用了,js的回调函数会被同步调用。
总结
本章的内容就将完了。