Android Hybrid

常见的App开发方式对比;

方式 优点 缺点
Native App 速度快,
用户体验好。
无法跨平台,
升级麻烦,
开发成本高(跨平台)。
Web App 跨平台开发成本低,
版本升级方便。
页面访问速度慢,
用户体验差。
HyBrid App 结合以上两种的优点,
未来发展的趋势。

Java 和 JS 相互调用

Android 调用 JavaScript

步骤

  1. 设置WebView支持JS
    webSettings.setJavaScriptEnabled(true);
  2. 添加JS回调接口
    webView.addJavascriptInterface(object,"interfaceName");
  3. WebView加载网页,外部网页要添加网络权限
    webView.loadUrl("file:///android_asset/index.html");
  4. WebView通过loadUrl方法调用JS方法
    mWebView.loadUrl("javascript:actionFromNative()");
  5. object中接收JS函数的返回值

示例

activity_main.xml
1
2
3
4
5
6
7
8
9
<Button
android:id="@+id/bt_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="text"/>
<WebView
android:id="@+id/wv_text"
android:layout_width="match_parent"
android:layout_height="100dp" />
text.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html>
<body>
<script type="text/javascript">
function showAlert(){
alert("JS弹框");
}

function showAlert2(params){
alert(params);
}

function showAlert3(params){
alert(params);
var result = "返回值";
window.JSInterface.jsMethod(result);
}

function showAlert4(){
var result = window.JSInterface.callAndroid();
alert(result);
}
</script>
</body>
</html>
MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Button button = findViewById(R.id.bt_text);
final WebView webView = findViewById(R.id.wv_text);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.setWebChromeClient(new WebChromeClient());
webView.loadUrl("file:///android_asset/text.html");
// 无参无返回值
webView.loadUrl("javascript:showAlert()");
// 有参无返回值
webView.loadUrl("javascript:showAlert2('hello word')");
// 有参有返回值
webView.loadUrl("javascript:showAlert3('hello word')");
webView.addJavascriptInterface(new JSToJava(MainActivity.this),"JSInterface");
}
});
JSToJava.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JSToJava {
private Context mContext;

public JSToJava(Context mContext){
this.mContext=mContext;
}

@JavascriptInterface
public void jsMethod(String paramFromJS){
Toast.makeText(mContext,paramFromJS,Toast.LENGTH_LONG).show();
}

@JavascriptInterface
public String callAndroid(){
return "返回值";
}
}

JavaScript调用Android

步骤

  1. 设置WebView支持JS
    webSettings.setJavaScriptEnabled(true);
  2. 添加JS回调接口
    webView.addJavascriptInterface(object,"interfaceName");
  3. WebView加载网页,外部网页要添加网络权限
    webView.loadUrl("file:///android_asset/index.html");
  4. JS通过window对象调用Android函数
    window.interfaceName.androidFunction();

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Button button = findViewById(R.id.bt_text);
final WebView webView = findViewById(R.id.wv_text);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.setWebChromeClient(new WebChromeClient());
webView.loadUrl("file:///android_asset/text.html");
// 有参有返回值
webView.loadUrl("javascript:showAlert4()");
webView.addJavascriptInterface(new JSToJava(MainActivity.this),"JSInterface");
}
});

Android4.2以下WebView的安全漏洞

漏洞元凶;addJavascriptInterface(加载了一些外部不安全的网站)
漏洞描述;

  1. 向WebView注册了一个叫”InterfaceName”的对象
  2. JS中可以访问到”InterfaceName”对象
  3. JS中可以通过”getClass”方法获得该对象的类型类
  4. 通过反射机制,得到该类的Runtime对象。
  5. 调用静态方法执行系统命令

漏洞解决;

  • android4.2以上;@JavascriptInterface注解,说明只暴露此注解方法,而不是类。
  • android4.2以下;自定义JS和Android的交互方式。

JSBridge

JSBridge的实现原理–双向通道

Android调用JS方法(安全的,不用修改)
webview.loadUrl(“javascript:function()”);

JS调用Android方法(因为安全漏洞,自定义方法)
一般通过拦截WebChromeClient的方法来实现;

  • onJsAlert(警告框)
  • onJsConfirm(确认框)
  • onJsPrompt(提示框,一般使用这个方法)

示例
通过JS层弹出提示框,Android层监听方法,实现信息传递

text.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<style type="text/css">
button {
width: 100%;
height: 20px;
}
</style>
</head>
<body>
<button onclick="promptAndSendMsg()" >button</button>
<script type="text/javascript">
function promptAndSendMsg(){
prompt("来自JS的消息");
}
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

WebView webView = findViewById(R.id.wv_text);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.setWebChromeClient(new WebChromeClient(){
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(showMsg(message));
return true;
}
});
webView.loadUrl("file:///android_asset/text.html");
}

private String showMsg(String message){
Toast.makeText(MainActivity.this,message,Toast.LENGTH_SHORT).show();
return null;
}

JSBridge内部通信协议的制定

JS向Android的通信协议

  1. HTTP URL格式
    http://host:port/path?param=value
    协议名称://主机IP地址:端口号/路径?参数(键值对)
  2. 初步协议格式
    jsbridge://className:port/methodName?jsonObj
    自定义协议名称://java类名:端口号(用于接收Android回调的内容)/类中具体的方法?参数(json格式)
  3. 完整协议格式
    jsbridge://className:callbackAddress/methodName?jsonObj
    自定义协议名称://java类名:Android执行结束后回调的地址/类中具体的方法?参数(json格式)
  4. 调用方式
    js层代码定义函数JSBridge,调用Android层函数jsCallAndroid()
    JSBridge.jsCallAndroid(className.methodName,params,callback);
    JSBridge.jsCallAndroid(Android类.类方法名称,向Android传递的参数(json格式),回调地址);

Android向JS的通信协议

  1. 参数采用JSON格式
  2. 调用方式
    webView.loadUrl(“javascript:JSBridge.onMethodName(port,jsonObj);”);
    webView.loadUrl(“javascript:JS层定义好的回调方法(告诉JS层回调函数位置,json格式的执行结果);”);

注;通常情况下,由JS层调用Android层函数,等Android层执行结束后,回调结果给JS层。

js层代码的实现

一个js文件,是js层的实现。

1.统一管理JS调用Android层函数
2.接收Android层执行的结果回调函数

其中有两个核心函数

  • function jsCallAndroid(obj,method,params,callback)
    function jsCallAndroid(java类名,类方法,参数(json),回调函数)

1.保存callback函数
2.根据参数组合出符合规则的URL
3.通过window.prompt将URL传递给Android程序

  • function onAndroidFinished(port,jsonObj)
    function onAndroidFinished(回调函数地址,Android函数执行结果)

1.根据port取出对应callback函数
2.将jsonObj参数传入callback函数并执行
3.删除callback函数

Java层代码的实现

  • 暴露给JS的Java方法格式
    public static void methodName(WebView webView,JSONObject jo,Callback callback)
    公开的 静态的 void 方法名称 (WebView,JSON格式,回调函数)(参数必须满足这三个规则)
  • Callback类
    在Callback类中将Java方法的执行结果通知JS
  • JSBridge类的功能设计

1.集中管理暴露给JS的类和方法
2.根据JS传入的URl内容找到对应的Java类,并执行指定的Java方法。

JSBridge实例

这里通过JSBridge的方式,来实现一个折线图效果。
自己随便写个html也行。也可以关注下这两个图表库,这里选择的第一个。
因为主要是看JSBridge的使用方式,所以关于HightChart的使用就尽量忽略了。

效果图;

示例

index.html部分代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
</head>
<body>
<script src="LineChartJS/JSBridge.js"></script>
<div style="margin-left: 10px;margin-bottom: 10px;font-size: 12px;">单位:元/个</div>
<div id="containerStock">折线图加载中...</div>
<script type="text/javascript">
window.onload=function(){
jsCallAndroid('JSBridge','ShowToast',{'msg':'0'},function(res){
<!--msg;传递给java层的值-->
<!--function;回调方法-->
<!--res为Android传过来的值(JSON)-->
})
}
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.xwxwaa.myapplication.MainActivity">

<WebView
android:id="@+id/wv_linechart"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</LinearLayout>
部分代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Hybrid
*/
public class MainActivity extends AppCompatActivity implements Methods.OnWebViewDataListener {

private WebView webView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

webView = findViewById(R.id.wv_linechart);
firstView();

}

private void firstView(){
webView.loadUrl("file:///android_asset/index.html");
Methods methods=new Methods();
methods.setOnWebViewDataListener(this);
// 调用注册方法,将java方法暴露出去。
JSBridge.register("JSBridge", Methods.class);
// 使用自定义的
webView.setWebChromeClient(new JSBridgeChromeClient());
webView.setWebViewClient(new WebViewClient());
webView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
return true;
}
});
WebSettings webSettings=webView.getSettings();
webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
webSettings.setLoadWithOverviewMode(true);
webSettings.setJavaScriptEnabled(true);
webSettings.setDisplayZoomControls(false);

}

@Override
public void onWebViewDataListener(CallBack callBack) {
callBack.apply(jsonObject);
}

}

JS层

JSBridge.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var callbacks=new Array();

function jsCallAndroid(obj,method,params,callback){
var port=callbacks.length;
callbacks[port]=callback;
var url='JSBridge://'+obj+":"+port+"/"+method+"?"+JSON.stringify(params);
// 组合出符合规则的url,并传递给java层
prompt(url);
}
function onAndroidFinished(port,jsonObj){
// 从callbacks中取出对应的回调函数
var callback=callbacks[port];
callback(jsonObj);
// 从callbacks中删除callback
delete callback[port];
}

Java层

暴露给JS的Java方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Methods {
/**
* 暴露给js的一个方法
* @param v
* @param param
* @param callBack
*/
public static void ShowToast(WebView v, JSONObject param, CallBack callBack){
// 表示js层成功将消息传递给java层,同时java层执行了此方法,
// 并且将执行结果告诉js层,让js层执行自己的回调函数。

// 将参数传化为一个字符串
String message=param.optString("msg");
// Toast.makeText(v.getContext(),message,Toast.LENGTH_SHORT).show();
if (callBack!=null){
// 如果回调函数不为空,则调用js层的回调函数。
if ("0".equals(message)){
// 根据msg信息做区分,执行相应的回调函数。
onWebViewDataListener.onWebViewDataListener(callBack);
}
}
}

/**
* 回调
*/
public static OnWebViewDataListener onWebViewDataListener;
public interface OnWebViewDataListener {
void onWebViewDataListener(CallBack callBack);
}
public void setOnWebViewDataListener(OnWebViewDataListener onWebViewDataListener) {
this.onWebViewDataListener = onWebViewDataListener;
}
}
Callback类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CallBack {

public String mPort;
public WebView mWebView;

/**
* 将java代码执行后的结果,告诉js层,让js执行自己的回调函数。
* @param webView WebView
* @param port 回调函数在js层中存储的地址
*/
public CallBack(WebView webView, String port){
this.mWebView= webView;
this.mPort=port;
}

/**
* java层执行这个回调函数,通知js层执行自己的回调函数。
* @param jsonObject JSONObject
*/
public void apply(JSONObject jsonObject){
if (mWebView!=null){
// js层的代码 定义好的回调方法 地址 值
mWebView.loadUrl("javascript:onAndroidFinished('"+mPort+"',"+ String.valueOf(jsonObject)+")");
}
}
}

JSBridge类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class JSBridge {
/**
* 用于存放所有暴露给JS层的类,和满足规则的方法。
*/
public static Map<String,HashMap<String,Method>> exposeMethods=new HashMap<>();

/**
* 注册方法
* @param exposeName 类名
* @param myClass 类
*/
public static void register(String exposeName, Class<?> myClass){
if (!exposeMethods.containsKey(exposeName)){
// 将所有验证过后的方法,放入表中。
exposeMethods.put(exposeName,getAllMethod(myClass));
}
}

/**
* 获取一个类中,所有满足规则的方法。
* @param injectedCls 类
* @return 通过验证的方法
*/
public static HashMap<String,Method> getAllMethod(Class injectedCls){
HashMap<String,Method> methodHashMap=new HashMap<>();
// 保存类中,所有声明过的方法。
Method[] methods=injectedCls.getDeclaredMethods();
// 遍历上面的数组
for (Method method:methods){
// 过滤规则
// 1.方法必须为public
// 2.方法必须为static
// 3.name不为空
if (method.getModifiers()!=(Modifier.PUBLIC| Modifier.STATIC)||method.getName()==null){
// 如果不满足,则不继续后面的步骤。
continue;
}
// 保存方法中的所有参数
Class[] paramters=method.getParameterTypes();
// 过滤规则
// 1.参数不为空
// 2.参数的个数为,3个。
if (paramters!=null&&paramters.length == 3){
// 过滤规则
// 1.第一个参数为WebView
// 2.第二个参数为JSONObject
// 3.第三个参数为回调函数
if (paramters[0] == WebView.class&&paramters[1]== JSONObject.class&&paramters[2] == CallBack.class){
// 参数类型符合
methodHashMap.put(method.getName(),method);
}
}
}
// 返回值
return methodHashMap;
}

/**
* 根据传入的Url,找到相应的类,相应的Java方法
* @param webView WebView
* @param urlString Url
* @return
*/
public static String callJava(WebView webView, String urlString){
// 解析Url(类名。。方法名。。参数。。回调函数)
String className;
String methodName;
String param;
String port;
if (!urlString.equals("")&&urlString!=null&&urlString.startsWith("JSBridge")){
// 转成Uri
Uri uri= Uri.parse(urlString);
className=uri.getHost();
methodName=uri.getPath().replace("/","");
param=uri.getQuery();
port=uri.getPort()+"";
// Log.e("TAG","uri;"+uri+" className;"+className+" methodName;"+methodName+" param;"+param+" port;"+port);
// 如果类在表中
if (exposeMethods.containsKey(className)){
// 获取类中方法
HashMap<String,Method> methodHashMap=exposeMethods.get(className);
if (methodHashMap!=null&&methodHashMap.size()!=0&&methodHashMap.containsKey(methodName)){
// 要寻找的方法,存在于表中。
// 获取该方法
Method method=methodHashMap.get(methodName);
if (method!=null){
// 方法不为空,则执行它。
try {
method.invoke(null, webView, new JSONObject(param), new CallBack(webView,port));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
}
}
}
return null;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 自定义的ChromeClient
* Created by xwxwaa on 2018/7/27.
*/

public class JSBridgeChromeClient extends WebChromeClient {
/**
* 截取js层传上来的url消息,执行相应的java方法。
* @param view
* @param url
* @param message
* @param defaultValue
* @param result
* @return
*/
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 执行自己的方法
result.confirm(JSBridge.callJava(view,message));
return true;
}
}

防混淆

注意;如果开启了代码混淆,要做防混淆。

1
2
3
4
5
6
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
1
2
3
4
#解决混合开发,无法与js交互的问题
-keepclassmembers class com.example.xwxwaa.myapplication.JSBridge.Methods {
public *;
}

备注

传送门GitHub

单词音标:
  • chrome 英 [krəʊm] 美 [kroʊm]
  • client 英 [ˈklaɪənt] 美 [ˈklaɪənt]