Android native code和Javascript通訊
如果android和js互動的話,那就是要通過一個控制元件WebView。如果js要調android中方法的話,要通過JavascriptInterface,百度一下就能看到很多資料,這裡不詳述。
在API17之前的話,是不需要加@JavascriptInterface註釋的,但是在17版本後,需要在方法上加上@JavascriptInterface。
看個簡單的例子:
這是html的程式碼,點選按鈕會觸發JS方法:showAndroidToast,在這個方法中會呼叫android的方法。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<script type="text/javascript">
function showAndroidToast(toast) {
window.Android.showToast(toast);
}
</script>
</head>
<body>
<input onclick="showAndroidToast('Hello world!')" type="button" value="Say hello"/>
</body>
</html>
看下android的程式碼:
public class MainActivity extends Activity {
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.addJavascriptInterface(new JavaScriptInterface(this), "Android");
mWebView.loadUrl("file:///android_asset/test.html");
}
}
public class JavaScriptInterface {
private Context mContext;
public JavaScriptInterface(Context c) {
mContext = c;
}
/**
* Show a toast from the web page
*/
@JavascriptInterface
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
}
執行起來的效果:
js呼叫Java
呼叫格式為window.jsInterfaceName.methodName(parameterValues) 此例中我們使用的是control作為注入介面名稱。注意,java提供給js的方法不能混淆!!
function toastMessage(message) {
window.control.toastMessage(message)
}
function sumToJava(number1, number2){
window.control.onSumResult(number1 + number2)
}
Java呼叫JS
webView呼叫js的基本格式為
webView.loadUrl(“javascript:methodName(parameterValues)”)
大多數人都知道WebView存在一個漏洞,見WebView中介面隱患與手機掛馬利用,雖然該漏洞已經在Android 4.2上修復了,即使用@JavascriptInterface代替addJavascriptInterface,但是由於相容性和安全性問題,基本上我們不會再利用Android系統為我們提供的addJavascriptInterface方法或者@JavascriptInterface註解來實現,所以我們只能另闢蹊徑,去尋找既安全,又能實現相容Android各個版本的方案。
那這裡就要用到JSBridge。首先我們來了解一下為什麼要使用JSBridge,在開發中,為了追求開發的效率以及移植的便利性,一些展示性強的頁面我們會偏向於使用h5來完成,功能性強的頁面我們會偏向於使用native來完成,而一旦使用了h5,為了在h5中儘可能的得到native的體驗,我們native層需要暴露一些方法給js呼叫,比如,彈Toast提醒,彈Dialog,分享等等,有時候甚至把h5的網路請求放著native去完成,而JSBridge做得好的一個典型就是微信,微信給開發者提供了JSSDK,該SDK中暴露了很多微信native層的方法,比如支付,定位等。
下面這篇文章是介紹如何利用window.prompt來建立兩端通訊的:
Android JSBridge的原理與實現
該文章裡面的程式碼。。。。真是慘不忍睹賭,所以我都貼出程式碼吧。
裡面定的協議是:jsbridge://className:callbackAddress/methodName?jsonObj
public class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = (WebView) findViewById(R.id.webview);
//此方法用來註冊哪些類可以供JS呼叫
JSBridge.register("bridge", BridgeImpl.class);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebChromeClient(new JSBridgeWebChromeClient());
//把myWebView載入html
webView.loadUrl("file:///android_asset/one.html");
}
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>JSBridge</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"/>
<script src="file:///android_asset/JSBridge.js" type="text/javascript"></script>
<script type="text/javascript"></script>
<style></style>
</head>
<body>
<div><h3>JSBridge 測試</h3></div>
<ul class="list">
<li>
<div>
<button onclick="JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">
測試showToast
</button>
</div>
</li>
<br/></ul>
<ul class="list">
<li>
<div>
<button onclick="JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})">
測試子執行緒回撥
</button>
</div>
</li>
<br/></ul>
</body>
</html>
(function (win) {
var hasOwnProperty = Object.prototype.hasOwnProperty;
var JSBridge = win.JSBridge || (win.JSBridge = {});
var JSBRIDGE_PROTOCOL = 'JSBridge';
//然後有一個Inner類,裡面有我們的call和onFinish方法。
var Inner = {
callbacks: {},
call: function (obj, method, params, callback) {
console.log(obj+" "+method+" "+params+" "+callback);
var port = Util.getPort();
console.log(port);
this.callbacks[port] = callback;
var uri=Util.getUri(obj,method,params,port);
console.log(uri);
window.prompt(uri, "");
},
onFinish: function (port, jsonObj){
var callback = this.callbacks[port];
callback && callback(jsonObj);
delete this.callbacks[port];
},
};
//一個Util類,裡面有三個方法,getPort()用於隨機生成port,getParam()用於生成json字串
//getUri()用於生成native需要的協議uri,裡面主要做字串拼接的工作
var Util = {
getPort: function () {
return Math.floor(Math.random() * (1 << 30));
},
getUri:function(obj, method, params, port){
params = this.getParam(params);
var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;
return uri;
},
getParam:function(obj){
if (obj && typeof obj === 'object') {
return JSON.stringify(obj);
} else {
return obj || '';
}
}
};
for (var key in Inner) {
if (!hasOwnProperty.call(JSBridge, key)) {
JSBridge[key] = Inner[key];
}
}
})(window);
public class JSBridge {
private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>();
public static void register(String exposedName, Class<? extends IBridge> clazz) {
if (!exposedMethods.containsKey(exposedName)) {
try {
exposedMethods.put(exposedName, getAllMethod(clazz));
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
HashMap<String, Method> mMethodsMap = new HashMap<>();
Method[] methods = injectedCls.getDeclaredMethods();
for (Method method : methods) {
String name;
if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
continue;
}
Class[] parameters = method.getParameterTypes();
if (null != parameters && parameters.length == 3) {
if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == Callback.class) {
mMethodsMap.put(name, method);
}
}
}
return mMethodsMap;
}
public static String callJava(WebView webView, String uriString) {
String methodName = "";
String className = "";
String param = "{}";
String port = "";
if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
Uri uri = Uri.parse(uriString);
className = uri.getHost();
param = uri.getQuery();
port = uri.getPort() + "";
String path = uri.getPath();
if (!TextUtils.isEmpty(path)) {
methodName = path.replace("/", "");
}
}
if (exposedMethods.containsKey(className)) {
HashMap<String, Method> methodHashMap = exposedMethods.get(className);
if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
Method method = methodHashMap.get(methodName);
if (method != null) {
try {
method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return null;
}
}
public class JSBridgeWebChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(JSBridge.callJava(view, message));
return true;
}
}
public interface IBridge {
}
public class Callback {
private static Handler mHandler = new Handler(Looper.getMainLooper());
private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";
private String mPort;
private WeakReference<WebView> mWebViewRef;
public Callback(WebView view, String port) {
mWebViewRef = new WeakReference<>(view);
mPort = port;
}
public void apply(JSONObject jsonObject) {
final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
if (mWebViewRef != null && mWebViewRef.get() != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mWebViewRef.get().loadUrl(execJs);
}
});
}
}
}
public class BridgeImpl implements IBridge {
public static void showToast(WebView webView, JSONObject param, final Callback callback) {
String message = param.optString("msg");
Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();
if (null != callback) {
try {
JSONObject object = new JSONObject();
object.put("key", "value");
object.put("key1", "value1");
callback.apply(getJSONObject(0, "ok", object));
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void testThread(WebView webView, JSONObject param, final Callback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
JSONObject object = new JSONObject();
object.put("key", "value");
callback.apply(getJSONObject(0, "ok", object));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
}).start();
}
private static JSONObject getJSONObject(int code, String msg, JSONObject result) {
JSONObject object = new JSONObject();
try {
object.put("code", code);
object.put("msg", msg);
object.putOpt("result", result);
return object;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}
程式碼都上了,效果的話就和那篇文章裡是一樣的。
當然,除了上面這種方法以外,還有其他的方法,通過攔截WebViewClient的shouldOverrideUrlLoading方法,主要是用來攔截url,看下程式碼:
public class MainActivity extends Activity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = (WebView) findViewById(R.id.webview);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new MyWebViewClient());
//如果不設定webchromeClient的話,會導致js的alert無法彈出
webView.setWebChromeClient(new WebChromeClient());
//把myWebView載入html
webView.loadUrl("file:///android_asset/ios.html");
}
}
ios.html:
<html>
<head>
<title>WebView和Native通訊</title>
<meta charset="UTF8">
<script type="text/javascript">
function nativeCallback(strJSON) {
var description = "";
console.log("-------------------------");
for(var i in strJSON){
var property=strJSON[i];
description+=i+" = "+property+"\n";
}
alert(description);
return "this";
}
function callNative () {
var msgIframe = document.createElement('iframe');
msgIframe.style.display = 'none';
var msgBody = {"params" : {"id":"hello"},"callback" : ""};
var strBody = JSON.stringify(msgBody);
msgIframe.src = "epaysdk://paysuccess?msg=" + strBody;
document.documentElement.appendChild(msgIframe);
setTimeout(callback(), 0);
function callback() {
alert('dssdsdsdsd');
}
}
</script>
</head>
<body>
<label id="log"></label>
<div style="margin-top:90px">
<button onclick=callNative()>呼叫原生方法</button>
</div>
</body>
</html>
public class MyWebViewClient extends WebViewClient {
private JSONObject obj;
@Override
public WebResourceResponse shouldInterceptRequest(final WebView view, WebResourceRequest request) {
Log.d("tag", "shouldInterceptRequest--------url:" + request.getUrl().toString() + " , " + request.getUrl().getScheme());
try {
obj = new JSONObject();
obj.put("code", "1");
obj.put("msg", "success");
view.post(new Runnable() {
@Override
public void run() {
Log.d("tag","evaluateJavascript");
view.evaluateJavascript("nativeCallback(" + obj.toString() + ")", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.d("tag","onReceiveValue:"+value);
}
});
}
});
} catch (Exception e) {
e.printStackTrace();
}
return super.shouldInterceptRequest(view, request);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.d("tag", "shouldOverrideUrlLoading-------url:" + url);
return super.shouldOverrideUrlLoading(view, url);
}
}