首先我们来理解一下什么是JSBridge和为什么要使用JSBridge?
在开发中,为了追求开发的效率以及移植的便利性,少量展现性强的页面我们会偏向于使用h5来完成,功能性强的页面我们会偏向于使用native来完成,而一旦使用了h5,为了在h5中尽可能的得到native的体验,我们native层需要暴露少量方法给js调用,比方,弹Toast提示,弹Dialog,分享等等,有时候甚至把h5的网络请求放到native去完成。
JSBridge做得好的一个典型就是微信,微信给开发者提供了JSSDK,该SDK中暴露了很多微信native层的方法,比方支付,定位等。
本文将对js和Native的通信原理和实现方法的少量讨论。
Android中的JSBridge是H5与Native通信的桥梁,其作用是实现H5与Native间的双向通信。要实现H5与Native的双向通信,处理如下四个问题就可:
1、Java如何调用JavaScript
2、JavaScript如何调用Java
3、方法参数以及回调如何解决
4、通信协议的制定
下面从以上问题依次开始探讨
在WebView中,假如java要调用js的方法,是非常容易做到的,使用WebView.loadUrl(“javascript:function()”)就可,这样,就做到了JSBridge的native层调用h5层的单向通信
WebView.loadUrl("javascript:function()");
JavaScript如何调用Java
js调用Android的方法有以下四种:
1、WebView 的 andJavascriptInterface
2、WebViewClient.shouldOverrideUrlLoading()
3、WebChromeClient.onConsoleMessage()
4、WebChromeClient.onJsPrompt()、onJsAlert()、onJsConfirm()
我们先对此四种方案进行一个详细的形容,最后选择一个方案就可。本文章中采用了第四种方案。
JavascriptInterface是Android官方提供的js和Native通信方案。其实现如下:
1、实现一个java类,供js调用
public class MyJavascriptInterface { @JavascriptInterface public void showToast(String toast) { Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show(); }}
2、在webView中注册这个类
webView.addJavascriptInterface(new MyJavascriptInterface(), "javascriptInterface");
3、在js中直接调用这个接口:
function showToast(text){ window.javascriptInterface.showToast(text);}
4、总结
大多数人都知道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内容 }}
1、在WebView有一个方法,叫setWebChromeClient,可以设置WebChromeClient对象,而这个对象中有三个方法,分别是onJsAlert,onJsConfirm,onJsPrompt,当js调用window对象的对应的方法,即window.alert,window.confirm,window.prompt,WebChromeClient对象中的三个方法对应的就会被触发,那这三个方法究竟要使用哪个呢?
2、这三个方法的区别,可以详见w3c JavaScript 消息框 。
3、一般来说,我们是不会使用onJsAlert的,为什么呢?由于js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响,而confirm和prompt的使用频率相对alert来说,则更低一点。
4、那么究竟是选择confirm还是prompt呢,其实confirm的使用频率也是不低的,比方你点一个链接下载一个文件,这时候假如需要弹出一个提醒进行确认,点击确认就会下载,点取消便不会下载,相似这种场景还是很多的,因而不能占用confirm。
5、而prompt则不一样,在Android中,几乎不会使用到这个方法,就是用,也会进行自己设置,所以我们完全可以使用这个方法。该方法就是弹出一个输入框,而后让你输入,输入完成后返回输入框中的内容。因而,占用prompt是再完美不过了。
public class CustomWebChromeClient extends WebChromeClient { @Override public boolean onJsPrompt() { super.onJsPrompt(); ... }}
myWebView.setWebChromClient(new CustomWebChromeClient());
1、任何IPC通信都涉及到参数序列化的问题,同理,Java与JavaScript之间只能传递基础类型(包括基本类型和字符串),不包括其余对象或者者函数。所以可以采用json格式来传递数据。
2、为了实现异步返回结果,所以JavaScript与Java相互调用不能直接获取返回值,只能通过回调的方式来获取返回结果。
要进行正常的通信,通信协议的制定是必不可少的。我们回想一下熟习的http请求url的组成部分。形如http://host:port/path?param=value, 我们参考http,制定JSBridge的组成部分
jsbridge://className:callbackAddress/methodName?jsonObj// className: 表示java的类名// callbackAddress: js回调的标识// methodName: java中的方法名// jsonObj: 接口数据
调用流程:
1、在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)) });
2、在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);