常见的App开发方式对比;
方式 |
优点 |
缺点 |
Native App |
速度快,用户体验好。 |
无法跨平台,升级麻烦,开发成本高(跨平台)。 |
Web App |
跨平台开发成本低,版本升级方便。 |
页面访问速度慢,用户体验差。 |
HyBrid App |
结合以上两种的优点,未来发展的趋势。 |
|
Java 和 JS 相互调用
Android 调用 JavaScript
步骤
- 设置WebView支持JS
webSettings.setJavaScriptEnabled(true);
- 添加JS回调接口
webView.addJavascriptInterface(object,"interfaceName");
- WebView加载网页,外部网页要添加网络权限
webView.loadUrl("file:///android_asset/index.html");
- WebView通过loadUrl方法调用JS方法
mWebView.loadUrl("javascript:actionFromNative()");
- object中接收JS函数的返回值
示例
activity_main.xml1 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.html1 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.java1 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.java1 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
步骤
- 设置WebView支持JS
webSettings.setJavaScriptEnabled(true);
- 添加JS回调接口
webView.addJavascriptInterface(object,"interfaceName");
- WebView加载网页,外部网页要添加网络权限
webView.loadUrl("file:///android_asset/index.html");
- 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(加载了一些外部不安全的网站)
漏洞描述;
- 向WebView注册了一个叫”InterfaceName”的对象
- JS中可以访问到”InterfaceName”对象
- JS中可以通过”getClass”方法获得该对象的类型类
- 通过反射机制,得到该类的Runtime对象。
- 调用静态方法执行系统命令
漏洞解决;
- android4.2以上;@JavascriptInterface注解,说明只暴露此注解方法,而不是类。
- android4.2以下;自定义JS和Android的交互方式。
JSBridge
JSBridge的实现原理–双向通道
Android调用JS方法(安全的,不用修改)
webview.loadUrl(“javascript:function()”);
JS调用Android方法(因为安全漏洞,自定义方法)
一般通过拦截WebChromeClient的方法来实现;
- onJsAlert(警告框)
- onJsConfirm(确认框)
- onJsPrompt(提示框,一般使用这个方法)
示例
通过JS层弹出提示框,Android层监听方法,实现信息传递
text.html1 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的通信协议
- HTTP URL格式
http://host:port/path?param=value
协议名称://主机IP地址:端口号/路径?参数(键值对)
- 初步协议格式
jsbridge://className:port/methodName?jsonObj
自定义协议名称://java类名:端口号(用于接收Android回调的内容)/类中具体的方法?参数(json格式)
- 完整协议格式
jsbridge://className:callbackAddress/methodName?jsonObj
自定义协议名称://java类名:Android执行结束后回调的地址/类中具体的方法?参数(json格式)
- 调用方式
js层代码定义函数JSBridge,调用Android层函数jsCallAndroid()
JSBridge.jsCallAndroid(className.methodName,params,callback);
JSBridge.jsCallAndroid(Android类.类方法名称,向Android传递的参数(json格式),回调地址);
Android向JS的通信协议
- 参数采用JSON格式
- 调用方式
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){ }) } </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
|
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); 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.js1 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);
prompt(url); } function onAndroidFinished(port,jsonObj){
var callback=callbacks[port]; callback(jsonObj);
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 {
public static void ShowToast(WebView v, JSONObject param, CallBack callBack){
String message=param.optString("msg"); if (callBack!=null){ if ("0".equals(message)){ 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;
public CallBack(WebView webView, String port){ this.mWebView= webView; this.mPort=port; }
public void apply(JSONObject jsonObject){ if (mWebView!=null){ 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 {
public static Map<String,HashMap<String,Method>> exposeMethods=new HashMap<>();
public static void register(String exposeName, Class<?> myClass){ if (!exposeMethods.containsKey(exposeName)){ exposeMethods.put(exposeName,getAllMethod(myClass)); } }
public 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&¶mters.length == 3){ if (paramters[0] == WebView.class&¶mters[1]== JSONObject.class&¶mters[2] == CallBack.class){ methodHashMap.put(method.getName(),method); } } } return methodHashMap; }
public static String callJava(WebView webView, String urlString){ String className; String methodName; String param; String port; if (!urlString.equals("")&&urlString!=null&&urlString.startsWith("JSBridge")){ Uri uri= Uri.parse(urlString); className=uri.getHost(); methodName=uri.getPath().replace("/",""); param=uri.getQuery(); port=uri.getPort()+""; 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
|
public class JSBridgeChromeClient extends WebChromeClient {
@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]