首先咱们来了解一下什么是JSBridge和为何要使用JSBridge?javascript
在开发中,为了追求开发的效率以及移植的便利性,一些展现性强的页面咱们会偏向于使用h5来完成,功能性强的页面咱们会偏向于使用native来完成,而一旦使用了h5,为了在h5中尽量的获得native的体验,咱们native层须要暴露一些方法给js调用,好比,弹Toast提醒,弹Dialog,分享等等,有时候甚至把h5的网络请求放到native去完成。前端
JSBridge作得好的一个典型就是微信,微信给开发者提供了JSSDK,该SDK中暴露了不少微信native层的方法,好比支付,定位等。java
本文将对js和Native的通讯原理和实现方法的一些探讨。web
Android中的JSBridge是H5与Native通讯的桥梁,其做用是实现H5与Native间的双向通讯。要实现H5与Native的双向通讯,解决以下四个问题便可:面试
一、Java如何调用JavaScriptjson
二、JavaScript如何调用Java安全
三、方法参数以及回调如何处理性能优化
四、通讯协议的制定微信
下面从以上问题依次开始讨论网络
在WebView中,若是java要调用js的方法,是很是容易作到的,使用WebView.loadUrl(“javascript:function()”)便可,这样,就作到了JSBridge的native层调用h5层的单向通讯
WebView.loadUrl("javascript:function()");
JavaScript如何调用Java
js调用Android的方法有如下四种:
一、WebView 的 andJavascriptInterface
二、WebViewClient.shouldOverrideUrlLoading()
三、WebChromeClient.onConsoleMessage()
四、WebChromeClient.onJsPrompt()、onJsAlert()、onJsConfirm()
咱们先对此四种方案进行一个详细的描述,最后选择一个方案便可。本文章中采用了第四种方案。
JavascriptInterface是Android官方提供的js和Native通讯方案。其实现以下:
一、实现一个java类,供js调用
public class MyJavascriptInterface { @JavascriptInterface public void showToast(String toast) { Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show(); } }
二、在webView中注册这个类
webView.addJavascriptInterface(new MyJavascriptInterface(), "javascriptInterface");
三、在js中直接调用这个接口:
function showToast(text){ window.javascriptInterface.showToast(text); }
四、总结
大多数人都知道WebView存在一个漏洞,见WebView中接口隐患与手机挂马利用,虽然该漏洞已经在Android 4.2上修复了(即便用@JavascriptInterface代替addJavascriptInterface),可是因为兼容性和安全性问题,基本上咱们不会再利用Android系统为咱们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现,因此咱们只能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本的方案。
这个方法是拦截全部webView的跳转,页面能够构造一个特殊格式的Url跳转,shouldOverrideUrlLoading拦截Url后判断其格式,而后Native就能执行自身的逻辑了。
public class CustomWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (isJsBridgeUrl(url)) { // JSbridge的处理逻辑 return true; } return super.shouldOverrideUrlLoading(view, url); } }
在js中执行console.log(), 会进入Android的WebChromeClient.consoleMessage()回调。
public class CustomWebChromeClient extends WebChromeClient { @Override public boolean onConsoleMessage(ConsoleMessage consoleMessage) { super.onConsoleMessage(consoleMessage); String msg = consoleMessage.message();//Javascript输入的Log内容 } }
一、在WebView有一个方法,叫setWebChromeClient,能够设置WebChromeClient对象,而这个对象中有三个方法,分别是onJsAlert,onJsConfirm,onJsPrompt,当js调用window对象的对应的方法,即window.alert,window.confirm,window.prompt,WebChromeClient对象中的三个方法对应的就会被触发,那这三个方法到底要使用哪一个呢?
二、这三个方法的区别,能够详见w3c JavaScript 消息框 。
三、通常来讲,咱们是不会使用onJsAlert的,为何呢?由于js中alert使用的频率仍是很是高的,一旦咱们占用了这个通道,alert的正常使用就会受到影响,而confirm和prompt的使用频率相对alert来讲,则更低一点。
四、那么究竟是选择confirm仍是prompt呢,其实confirm的使用频率也是不低的,好比你点一个连接下载一个文件,这时候若是须要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,相似这种场景仍是不少的,所以不能占用confirm。
五、而prompt则不同,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,因此咱们彻底可使用这个方法。该方法就是弹出一个输入框,而后让你输入,输入完成后返回输入框中的内容。所以,占用prompt是再完美不过了。
public class CustomWebChromeClient extends WebChromeClient { @Override public boolean onJsPrompt() { super.onJsPrompt(); ... } }
myWebView.setWebChromClient(new CustomWebChromeClient());
一、任何IPC通讯都涉及到参数序列化的问题,同理,Java与JavaScript之间只能传递基础类型(包括基本类型和字符串),不包括其余对象或者函数。因此能够采用json格式来传递数据。
二、为了实现异步返回结果,因此JavaScript与Java相互调用不能直接获取返回值,只能经过回调的方式来获取返回结果。
要进行正常的通讯,通讯协议的制定是必不可少的。咱们回想一下熟悉的http请求url的组成部分。形如http://host:port/path?param=value, 咱们参考http,制定JSBridge的组成部分
jsbridge://className:callbackAddress/methodName?jsonObj // className: 表示java的类名 // callbackAddress: js回调的标识 // methodName: java中的方法名 // jsonObj: 接口数据
调用流程:
一、在js中,能够采用以下方法调用java方法
var JSBridge = { call: function(className, method, params, callback) { var uri = 'jsbridge://' + className + ':' + callback + '/' + method + '?' + params; window.prompt(uri, ""); } } // 下面会调用java中的 bridge.showToast方法 JSBridge.call('bridge', 'showToast', {'msg':'Hello JSBridge'}, function(res) { alert(JSON.stringify(res)) });
二、在java中, 能够以下实现:
// 进入prompt回调 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; } } // 调用java逻辑 public class JSBridge { ... 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("/", ""); } } // 基于上面的className、methodName和port path调用对应类的方法 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; } } // 直接进入showToast函数的实现 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(); } } } // 上述程序的callback.apply方法实现以下: 即经过webView.loadUrl实现java调用js的方法 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); } }); } } }
JSBridge类管理暴露给前端方法,前端调用的方法应该在此类中注册才可以使用。register的实现是从Map中查找key是否存在,不存在则反射取得对应class中的全部方法,具体方法是在BridgeImpl中定义的,方法包括三个参数分别为WebView、JSONObject、CallBack。若是知足条件,则将全部知足条件的方法put到map中。
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(); } } }
JSBridge类中的callJava方法就是将js传递过来的URL解析,根据将要调用的类名从刚刚创建的Map中找出,根据方法名调用具体的方法,并将解析出的三个参数传递进去。
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; }
CallBack类是用来回调js中回调方法的Java对应类。Java层处理好的返回结果是经过CallBack类来实现的。在这个回调类中传递的参数是JSONObject(返回结果)、WebView和port,port应与js传递过来的port相对应。
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); } }); } }
在java层的JSBridge中注册方法,例如
JSBridge.register("bridge", BridgeImpl.class);