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)
  • 複寫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的惡意攻擊

演示如下:

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~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章