Android原生与JavaScript交互详解
这几天公司项目里提到了原生与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~