前言
HyBrid俗稱混合開發(fā)敞峭。使用Android提供的組件——WebView背桐,去加載放在本地或者服務器用h5編寫的UI界面系宜;js與java之間的互調骑丸,使得HyBrid APP的體驗更加趨向原生APP。本章內容主要講述在h5頁面調用Java方法恭取、在Android里調用h5里的js方法和jsbridge的簡單實現泰偿。因此,擁有h5蜈垮、css和js更容易上手甜奄。
三種APP開發(fā)方式比較
安全漏洞
在Android 4.2以下的WebView有個安全漏洞,外部網頁通過得到Runtime
對象窃款,然后執(zhí)行系統(tǒng)命令得到信息,原因出在addJavascriptInterface()
方法牍氛。下面是漏洞的簡單描述:
1晨继、向WebView注冊了一個叫“InterfaceName”
的對象
2、js中可以訪問到“InterfaceName”
對象
3搬俊、js中通過“getClass”
方法獲取該對象的類型類
4紊扬、通過反射機制,得到該類的Runtime
對象
5唉擂、調用靜態(tài)方法執(zhí)行系統(tǒng)命令
核心代碼示例:
<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
目錄創(chuàng)建。然后新建一個文件夾,命名為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的彈窗時渣淳,會根據彈窗的類型執(zhí)行相應的方法,如prompt()
對應onJsPrompt()
伴箩。所以入愧,我們可以重寫onJsPrompt()
方法,請求處理完后將其攔截嗤谚,也就是返回true棺蛛,那這個彈窗就不會顯示了。換句話說巩步,可以在這調用已經寫好的Java方法旁赊。
接下就是要解決自定義協議了。我們可以模仿http的url格式椅野,http://host:port/param=value终畅,轉換過來,JSBridge://className:callbackAddress/methodName?jsonObj竟闪。js向Android發(fā)送信息(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方法的執(zhí)行結果通知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類己儒,并執(zhí)行指定的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()的區(qū)別
// 如果調用江醇,js的回調函數才會被調用陶夜,參數的值是返回給js的
// 如果沒調用裆站,js即使有回調函數也不會執(zhí)行
// 可以使用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;
// 組合出符合規(guī)則的url羽嫡,并傳遞給Java層
var url = 'JSBridge://' + obj + ':' + port + '/' + method + '?' + JSON.stringify(params);
window.prompt(url);
}
/**
* 當js調用完Android層時執(zhí)行
* @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>
對函數的調用過程進行概括:點擊按鈕胳嘲,觸發(fā)jsCallAndroid()
方法厂僧,通過調用window.prompt(url)
向Android發(fā)送請求(信息)。然后在JSBridgeChromeClient.onJsPrompt()
方法對請求進行攔截處理颜屠,就是實現了js調用java,JSBridge.callJava(view, message)
甫窟。執(zhí)行到callJava()
方法內部蛙婴,會調用Methods.showToast()
方法,緊接著會調用CallBack.apply(result)
方法街图,最后是調用js的onAndroidFinished()
方法浇衬。如果JsPromptResult.confirm()
被調用了,js的回調函數會被同步調用餐济。
總結
本章的內容就將完了耘擂。