相比于 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();
}
}
}