欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android原生与JavaScript交互详解

程序员文章站 2024-02-05 20:52:58
...

这几天公司项目里提到了原生与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方法演示如下:

Android原生与JavaScript交互详解

JS调用APP原生方法

JS调用原生方法有三种方式:

  • WebView.addJavascriptInterface(Object obj,String name)
  • 复写WebViewClientshouldOverrideUrlLoading
  • 复写WebViewChromeClientonJsAlert()onJsConfirm()onJsPrompt()方法拦截JS的alertconfimrprompt方法

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的恶意攻击

演示如下:

Android原生与JavaScript交互详解

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,略显繁琐

该方法演示如下:

Android原生与JavaScript交互详解

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等方法中添加协议限制,我这里偷个懒就不写了。

演示如下:

Android原生与JavaScript交互详解

总结

总的来说,这三种方法各有优劣,第一种在低版本上容易造成严重的安全漏洞,但胜在使用简单,可以应付较为简单的情景;第二种适用于不需要返回值的场景,缺点也在于获取返回值过于繁琐;第三种同样使用较为复杂,不过适用于大部分场景。

总结

那么本篇文章的所有内容就是这样了。所有代码地址在github,欢迎指正~

我的个人博客,欢迎访问、交流~

enjoy~