這幾天公司項目裏提到了原生與HTML交互的需求,之前一直用的前人封裝好的工具類。今天打算好好梳理下Android中原生與網頁交互的方法和注意事項。
談到Android與HTML交互,其本質還是WebView與JavaScript的交互過程。這就分爲兩種情況:
- WebView或者說App調用JS方法
- JS調用APP的原生方法
我們就從這兩大方面逐步講解這兩種情況的實現。
App調用JS方法
Android原生調用JS方法有兩種方式:
- WebView.loadUrl(String url)
- WebView.evaluateJavascript(String script, ValueCallback\ resultCallback)
我們用一個例子來分別講述這兩種方式的用法:
HTML代碼如下(使用了菜鳥教程的範例):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鳥教程(runoob.com)</title>
</head>
<body>
<script>
function changeImage()
{
element=document.getElementById('myimage')
if (element.src.match("bulbon"))
{
element.src="pic_bulboff.gif";
alert("燈滅了");
}
else
{
element.src="pic_bulbon.gif";
alert("燈亮了");
}
}
function callAndroid() {
<!-- android是對象映射時設置的名稱 -->
alert(android.getPhoneBand());
}
</script>
<img id="myimage"
src="pic_bulboff.gif" width="100" height="180">
<button id="call_android" type="button" onclick="callAndroid()">調用Android方法</button>
</body>
</html>
我這裏將html文件和圖片資源都放在了assets目錄下,Android中訪問assets目錄下的文件使用file:///android_asset/...
來獲取assets目錄下的文件
頁面佈局是這樣的:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:orientation="vertical"
tools:context=".NativeCallJsActivity">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="horizontal">
<Button
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="loadUrl"
android:id="@+id/btn_load_url"/>
<Button
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="evaluateJavascript"
android:id="@+id/btn_evaluate"/>
<Button
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="both"
android:id="@+id/btn_both"/>
</LinearLayout>
<FrameLayout
android:id="@+id/fl_web_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
這裏我們採用動態添加WebView的方式,即不在xml中直接定義WebView(容易造成內存泄漏),而是在xml中定義一個WebView容器,在代碼中動態添加和刪除WebView,在代碼中如下:
private void initView() {
...
//使用弱引用,防止內存泄漏
WeakReference<Context> weakContext = new WeakReference<>((Context)this);
webView = new WebView(weakContext.get());
flWebViewContainer.addView(webView);
//設置WebView與JS交互的權限
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
//載入網頁
webView.loadUrl("file:///android_asset/test.html");
}
最後不要忘了在onDestroy中銷燬WebView:
@Override
protected void onDestroy() {
if( webView!=null) {
// 如果先調用destroy()方法,則會命中if (isDestroyed()) return;
// 這一行代碼,需要先onDetachedFromWindow(),再destory()
ViewParent parent = webView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(webView);
}
webView.stopLoading();
// 退出時調用此方法,移除綁定的服務,否則某些特定系統會報錯
webView.getSettings().setJavaScriptEnabled(false);
webView.clearHistory();
webView.clearView();
webView.removeAllViews();
webView.destroy();
}
super.onDestroy();
}
loadUrl
首先我們來看使用loadUrl來調用JS方法:
webView.loadUrl("javascript:changeImage()");
很簡單,使用方法名調用即可,此方法的缺陷是沒有完成後的回調
evaluateJavascript
使用evaluateJavascript要比上面代碼更簡單,而且有方法回調,使用方法:
if (version >= Build.VERSION_CODES.KITKAT) {
webView.evaluateJavascript("javascript:changeImage()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//獲取返回值,如果存在
}
});
}
可以看出,這個方法是在Android4.4之後才引入的,因此如果需要使用該方法則需要minSdkVersion大於等於19,在執行完js方法後會執行ValueCallback中的onReceiveValue方法,完成回調操作。
總結
上述兩種方法都可以實現Android調用JS方法,但是後者比前者效率更高,並且前者不能很簡單的獲取到返回值,但是後者又有SDK版本的限制,因此現在一般推薦在高版本使用evaluateJavascript
,低版本使用loadUrl
,如下:
if (version < Build.VERSION_CODES.KITKAT) {
webView.loadUrl("javascript:changeImage()");
} else {
webView.evaluateJavascript("javascript:changeImage()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//獲取返回值,如果存在
}
});
}
App調用JS方法演示如下:
JS調用APP原生方法
JS調用原生方法有三種方式:
WebView.addJavascriptInterface(Object obj,String name)
- 複寫
WebViewClient
的shouldOverrideUrlLoading
- 複寫
WebViewChromeClient
的onJsAlert()
、onJsConfirm()
、onJsPrompt()
方法攔截JS的alert
、confimr
、prompt
方法
HTML代碼如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>js調用Android</title>
</head>
<body>
<script>
function callAndroid() {
<!-- android是對象映射時設置的名稱 -->
alert("獲取到手機名稱是"+android.getPhoneBand());
}
</script>
<button id="call_android" type="button" onclick="callAndroid()">調用Android方法</button>
</body>
</html>
這裏有一個按鈕,點擊之後會調用android.getPhoneBand()
方法(android這個元素是java代碼中建立的對象映射)
addJavascriptInterface
要使用addJavaInterface,首先我們需要有一個類來擴展JS能力:
public class WebViewJsInterface {
private static final String TAG = "WebViewJsInterface";
private WeakReference<Context> weakReference;
public WebViewJsInterface(Context context) {
weakReference = new WeakReference<>(context);
}
@JavascriptInterface
public String getPhoneBand() {
String result = android.os.Build.BRAND;
Toast.makeText(weakReference.get(),"JS called Android By addJavascriptInterface~",Toast.LENGTH_SHORT).show();
return result;
}
}
注意要在方法前加上註解@JavascriptInterface
將這個對象與WebView綁定:
//對WebView與JsInterface建立對象映射
webView.addJavascriptInterface(new WebViewJsInterface(this),"android");
在這之後,在JS中使用android.xx()就可以調用原生方法了,點擊頁面上的按鈕,可以看到在logcat中打印出獲取到的手機品牌:Android
使用此方法可以很輕易地實現JS調用原生方法,不過有一個重大的問題,在Android4.2之前的版本,由於未要求使用@JavascriptInterface
,會導致來自JS的惡意攻擊
演示如下:
shouldOverrideUrlLoading
使用複寫WebViewClient的shouldOverrideUrlLoading方法,其本質是獲取網頁跳轉鏈接,並判斷是否攔截,代碼如下,首先是HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>js調用Android</title>
</head>
<body>
<script>
function callAndroidByWebClient() {
<!-- 定義好協議,跳轉 -->
document.location = "xylitolz://toastHello?arg=2000";
}
</script>
<button type="button" onclick="callAndroidByWebClient()">使用shouldOverrideUrlLoading調用Android方法</button>
</body>
</html>
在Java代碼中,使用:
//設置WebViewClient
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//判斷Url
if(TextUtils.isEmpty(url)) {
return false;
}
Uri uri = Uri.parse(url);
if(uri.getScheme().equals("xylitolz")) {
//url爲定義好的開頭爲xytlitolZ的協議,則通過攔截
if(uri.getAuthority().equals("toastHello")) {
//區分協議
String arg = uri.getQueryParameter("arg");
JsUtil.getInstance().toastHello(JsCallNativeActivity.this,arg);
}
return true;
} else {
return false;
}
}
});
注意Uri的schema不區分大小寫,在Java代碼中使用equals時需要特別注意
此方法調用過後JS很難獲取到Android的返回結果,若要獲取則需要使用loadUrl()的方式,將需要傳遞的值傳給JS,略顯繁瑣
該方法演示如下:
onJsAlert、onJsConfirm、onJsPrompt
這個方式是通過攔截JS的三個方法alert、confirm、prompt來實現JS調用原生方法的
HTML如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>js調用Android</title>
</head>
<body>
<script>
function callAndroidByWebViewChromeClient() {
alert("WebViewChromeClient");
}
</script>
<div style="text-align:center;border: green solid 1px;">
<button style="margin-top:10px;margin-bottom:10px;" type="button" onclick="callAndroidByWebViewChromeClient()">使用webViewChromeClient調用Android方法</button>
</div>
</body>
</html>
java代碼中使用:
//設置WebView與JS交互的權限
WebSettings settings = webView.getSettings();
settings.setJavaScriptCanOpenWindowsAutomatically(true);
//設置響應alert、confirm、Prompt方法
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b = new AlertDialog.Builder(JsCallNativeActivity.this);
b.setTitle("Alert");
b.setMessage("JS通過alert調用原生,參數是"+message);
b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
});
b.setCancelable(false);
b.create().show();
return true;
}
});
onJsAlert、onJsConfirm、onJsPrompt這三個WebChromeClient類的方法分別對應JS的alert、confirm、prompt方法
當然,如果有需要可以在alert等方法中添加協議限制,我這裏偷個懶就不寫了。
演示如下:
總結
總的來說,這三種方法各有優劣,第一種在低版本上容易造成嚴重的安全漏洞,但勝在使用簡單,可以應付較爲簡單的情景;第二種適用於不需要返回值的場景,缺點也在於獲取返回值過於繁瑣;第三種同樣使用較爲複雜,不過適用於大部分場景。
總結
那麼本篇文章的所有內容就是這樣了。所有代碼地址在github,歡迎指正~
我的個人博客,歡迎訪問、交流~
enjoy~