Android WebView —— Java 與 JavaScript 交互總結(jié)

相比于 Native App 和 Web App,Hybrid App 憑借其迭代靈活、控制自如、多端同步的優(yōu)勢(shì)在應(yīng)用市場(chǎng)上越發(fā)顯得優(yōu)勝,主要得力于,其將變更頻繁的部分產(chǎn)品功能使用 H5 開發(fā)并在客戶端中借助 WebView 控件嵌入應(yīng)用當(dāng)中。所以,開發(fā)中我們總會(huì)遇到原生 Java 代碼與網(wǎng)頁(yè)中的 Js 代碼之間相互調(diào)用從而產(chǎn)生的交互問(wèn)題。

Java 與 Js 彼此調(diào)用的前提是設(shè)置 WebView 支持 JavaScript 功能:

mWebView.getSettings().setJavaScriptEnabled(true);

Java 調(diào)用 Js


第一步,在網(wǎng)頁(yè)中使用 Js 定義提供給 Java 訪問(wèn)的方法,就像普通方法定義一樣,如:

<script type="text/javascript">
    function javaCallJs(message){
        alert(message);
    }
</script>  

第二步,在 Java 代碼中按照 "javascript:XXX" 的 Url 格式使用 WebView 加載訪問(wèn)即可:

mWebView.loadUrl("javascript:javaCallJs(" + "'Message From Java'" + ")");

注意:String 類型的參數(shù)需要使用單引號(hào) “'” 包裹,數(shù)組類型的參數(shù)則不用,如:javascript:javaCallJs([01, 02, 03]),其他復(fù)雜類型的參數(shù)可以轉(zhuǎn)換為 Json 字符串的形式傳遞。

Js 調(diào)用 Java


第一步,在 Java 對(duì)象中定義 Js 訪問(wèn)的方法,如:

@JavascriptInterface
public void jsCallJava(String message){
    Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}

注意事項(xiàng):提供給 Js 訪問(wèn)的屬性和方法必須定義為 public 類型,并且添加注解 @JavascriptInterface。在 API 17 及更高版本的系統(tǒng)中,任何暴露給 Js 訪問(wèn)的 Java 接口都需要添加這個(gè)注解,否則會(huì)報(bào)異常:Uncaught TypeError: Object [object Object] has no method 'XXX'。系統(tǒng)這種做法也是為了降低應(yīng)用的安全隱患,因?yàn)樵谥暗陌姹局校琂s 可以通過(guò)反射的方式訪問(wèn)注入 WebView 中的 Java 對(duì)象的 public 類型 field 和 method,從而隨意修改宿主程序。

第二步,將提供給 Js 訪問(wèn)的接口內(nèi)容所屬的 Java 對(duì)象注入 WebView 中:

mWebView.addJavascriptInterface(MainActivity.this, "main");

addJavascriptInterface(Object object, String name) 參數(shù)說(shuō)明:object 表示 Js 訪問(wèn)的接口內(nèi)容所在的 Java 對(duì)象;name 表示 Js 調(diào)用 Java 代碼時(shí)的接口名稱,與 Js 中的調(diào)用保持一致即可。

第三步,Js 按照指定的接口名訪問(wèn) Java 代碼,有如下兩種寫法:

<button type="button" onClick="javascript:main.jsCallJava('Message From Js')" >Js Call Java</button>

<!--<button type="button" onClick="window.main.jsCallJava('Message From Js')" >Js Call Java</button>-->

這里簡(jiǎn)單提供一個(gè)可供測(cè)試的 Html 網(wǎng)頁(yè)和 Activity 代碼:

test.html:

<html>  
    <head>  
        <meta http-equiv="Content-Type"  content="text/html;charset=UTF-8">
        <script type="text/javascript">
            function javaCallJs(message){
                alert(message);
            }
        </script>
    </head>  
    <body>
        <button type="button" onClick="window.main.jsCallJava('Message From Js')" >Js Call Java</button>
    </body>  
</html>  

MainActivity.java:

public class MainActivity extends AppCompatActivity {

    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar mToolbarTb = (Toolbar) findViewById(R.id.tb_toolbar);
        setSupportActionBar(mToolbarTb);

        mWebView = (WebView) findViewById(R.id.webview);
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.loadUrl("file:///android_asset/test.html");
        mWebView.addJavascriptInterface(MainActivity.this, "main");

        mWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                return super.onJsAlert(view, url, message, result);
            }
        });
    }

    public void javaCallJs(View v){
        mWebView.loadUrl("javascript:javaCallJs(" + "'Message From Java'" + ")");
    }

    @JavascriptInterface
    public void jsCallJava(String message){
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.search, menu);
        return super.onCreateOptionsMenu(menu);
    }

}

效果圖:

注意:無(wú)論是 Java 調(diào)用 Js 還是 Js 調(diào)用 Java,只能通過(guò)參數(shù)傳遞數(shù)據(jù),而無(wú)法獲取彼此方法的返回值!解決方案就是額外添加一層回調(diào)來(lái)達(dá)到這個(gè)目的。比如 Java 調(diào)用 Js 的方法,Js 計(jì)算結(jié)束所得結(jié)果不能通過(guò) return 語(yǔ)句返回給 Java 調(diào)用者,而是再回調(diào) Java 的另一個(gè)方法,通過(guò)傳參的形式傳遞給 Java。

注意事項(xiàng)


1.使用 loadUrl() 方法實(shí)現(xiàn) Java 調(diào)用 Js 功能時(shí),必須放置在主線程中,否則會(huì)發(fā)生崩潰異常。比如修改上面的代碼:

new Thread(new Runnable() {
    @Override
    public void run() {
        mWebView.loadUrl("javascript:javaCallJs(" + "'Message From Java'" + ")");
    }
}).start();

運(yùn)行時(shí)會(huì)得到如下 logcat 異常信息:

java.lang.RuntimeException: java.lang.Throwable: A WebView method was called on thread 'Thread-18022'. All WebView methods must be called on the same thread.

如果真的在子線程中遇到調(diào)用 Js 的功能,也要將其轉(zhuǎn)換到主線程中去:

mWebView.post(new Runnable() {
    @Override
    public void run() {
        mWebView.loadUrl("javascript:javaCallJs(" + "'Message From Java'" + ")");
    }
});

2.Js 調(diào)用 Java 方法時(shí),不是在主線程 (Thread Name:main) 中運(yùn)行的,而是在一個(gè)名為 JavaBridge 的線程中執(zhí)行的,通過(guò)如下代碼可以測(cè)試:

    @JavascriptInterface
    public void jsCallJava(String message){
        Log.i("thread", Thread.currentThread().getName());
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }

所以這里需要注意的是,當(dāng) Js 調(diào)用 Java 時(shí),如果需要 Java 繼續(xù)回調(diào) Js,千萬(wàn)別在 JavascriptInterface 方法體中直接執(zhí)行 loadUrl() 方法,而是像前面一樣進(jìn)行線程切換操作。

3.代碼混淆時(shí),記得保持 JavascriptInterface 內(nèi)容,在 proguard 文件中添加如下類似規(guī)則 (有關(guān)類名按需修改):

keepattributes *Annotation*
keepattributes JavascriptInterface
-keep public class com.mypackage.MyClass$MyJavaScriptInterface
-keep public class * implements com.mypackage.MyClass$MyJavaScriptInterface
-keepclassmembers class com.mypackage.MyClass$MyJavaScriptInterface { 
    <methods>; 
}

Url 攔截


除了上面這種 Java 與 Js 互調(diào)方法的方式,還可以利用 WebView 攔截 Url 的方式實(shí)現(xiàn)原生應(yīng)用與 H5 之間的交互動(dòng)作。通過(guò) WebViewClient 提供的接口攔截網(wǎng)頁(yè)內(nèi)諸如二級(jí)跳轉(zhuǎn)的 Url 鏈接,便可以進(jìn)行業(yè)務(wù)邏輯上的判斷處理、Url 參數(shù)傳遞等功能,如:

mWebView.setWebViewClient(new WebViewClient(){
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        // request.getUrl()
        return super.shouldOverrideUrlLoading(view, request);
    }
});

注意:過(guò)去常用的 shouldOverrideUrlLoading(WebView view, String url) 方法已經(jīng)被廢棄。

參考使用


通過(guò) Java 與 Js 之間的交互可以做很多事情,比如獲取網(wǎng)頁(yè)中的圖片,利用原生控件予以展示,類似響應(yīng)微信公眾號(hào)文章中的圖片點(diǎn)擊事件。參考代碼如下:

public class MainActivity extends AppCompatActivity {

    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = (WebView) findViewById(R.id.webview);
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.loadUrl("https://www.taobao.com/");
        mWebView.addJavascriptInterface(new MyJavascriptInterface(), "imageClick");

        mWebView.setWebViewClient(new MyWebViewClient());
    }

    /**
     * 遍歷 <img> 標(biāo)簽, 添加圖片點(diǎn)擊事件, 將圖片 Url 地址回調(diào)給 Java 方法
     */
    private void addImageClickListner() {
        mWebView.loadUrl("javascript:(function(){" +
                "var objs = document.getElementsByTagName(\"img\"); " +
                "for(var i=0;i<objs.length;i++)  " +
                "{"
                + "    objs[i].onclick=function()  " +
                "    {  "
                + "        window.imageClick.openImage(this.src);  " +
                "    }  " +
                "}" +
                "})()");
    }

    public class MyJavascriptInterface {

        public MyJavascriptInterface() {

        }

        @android.webkit.JavascriptInterface
        public void openImage(String imageUrl) {
            Log.i("imageUrl", imageUrl);
            // TODO 獲取圖片地址后, 通過(guò)原生控件 ImageView 展示, 添加縮放、保存等功能
        }
    }

    private class MyWebViewClient extends WebViewClient {

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
            return super.shouldOverrideUrlLoading(view, request);
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            addImageClickListner();
        }

    }

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評(píng)論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評(píng)論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評(píng)論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,779評(píng)論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評(píng)論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評(píng)論 0 291
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評(píng)論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,515評(píng)論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,750評(píng)論 1 375
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,933評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評(píng)論 1 296
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,492評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,703評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容