Android JsBridge 就是用来在 Android app的原生 java 代码与 javascript 代码中架设通讯(调用)桥梁的辅助工具。javascript
原文地址点这里html
github点这里java
使用方式戳这里android
有问题请联系 xesamgit
Javascript 运行在 WebView 中,而 WebView 只是 Javascript 执行引擎与页面渲染引擎的一个包装而已。github
因为这种自然的隔离效应,咱们能够将这种状况与 IPC 进行类比,将 Java 与 Javascript 的每次互调都看作一次 IPC 调用。
如此一来,咱们能够模仿各类已有的 IPC 方式来进行设计,好比 RPC。本文模仿 Android 的 Binder 机制来实现一个 JsBridge。web
首先回顾一一下基于 Binder 的经典 RPC 调用:json
固然,client 与 server 只是用来区分通讯双方责任的叫法而已,并非一成不变的。
对于 java 与 javascript 互调的状况,当 java 主动调用 javascript 的时候,java 充当 client 角色,javascript 则扮演 server 的角色,
javascript 中的函数执行完毕后回调 java 方法,这个时候,javascript 充当 client 角色,而 javascript 则承担 server 的责任。安全
剩下的问题就是怎么来实现这个机制了,大体有这么几个须要解决的问题:app
下面逐个讨论这些问题:
要实现 Java 与 Javascript 的相互调用,有两条途径能够考虑:
对于第一种途径,代价比较大,并且技术方案比较复杂,通常只有基于 Javascript 的跨平台开发方案才会这么作。
因此,如今着重考查第二种途径。
Android 的默认 Sdk 中, Java 与 Javascript 的一切交互都是依托于 WebView 的,大体有如下几个可用方法:
第一:
webView.loadUrl("javascript:scriptString"); //其中 scriptString 为 Javascript 代码
第二,在 KITKAT 以后,又新增了一个方法:
webView.evaluateJavascript(scriptString, new ValueCallback<String>() { @Override public void onReceiveValue(String value) { } });//其中 scriptString 为 Javascript 代码,ValueCallback 的用来获取 Javascript 的执行结果。这是一个异步掉用。
这个调用看起比上面的正常,并且更像是一个方法调用。
须要注意的是,ValueCallback 并非在 UI 线程里面执行的。
要实现 Javascript 调用 java 方法,须要先在 Javascript 环境中注入一个 Java 代理:
class JavaProxy{ @JavascriptInterface //注意这里的注解。出于安全的考虑,4.2 以后强制要求,否则没法从 Javascript 中发起调用 public void javaFn(){ //xxxxxx }; } webView.addJavascriptInterface(new JavaProxy();, "java_proxy");
而后在 Javascript 环境中直接调用 obj_proxy 代理上的方法便可。
java_proxy.javaFn();
这里有两个方面须要统一:
因此,咱们先将 Javascript 的执行包装成相似 java 同样的代理对象,而后经过在各自的 stub 上注册回调来增长功能支持。
好比,若是 java 想增长 getPackageName 方法,那么,直接在 JavaProxy 上注册便可:
javaProxy.register("getPackageName", new JavaHandler(){ @Override public void handle(Object value){ //xxxxx } })
如图:
很显然,任何 IPC 通讯都涉及到参数序列化的问题, 同理 java 与 Javascript 之间只能传递基础类型(注意,不单纯是基本类型),包括基本类型与字符串,不包括其余对象或者函数。
因为只涉及到简单的相互调用,这里就能够考虑采用 JSON 格式来传递各类数据,轻量而简洁。
Java 调用 Javascript 没有返回值(这里指 loadUrl 形式的调用),所以若是 java 端想从 Javascript 中获取返回值,只能使用回调的形式。
可是在执行完毕以后如何找到正确的回调方法信息,这是一个重要的问题。好比有下面的例子:
在 java 环境中,JavaProxy 对象有一个无参数的 getPackageName 方法用来获取当前应用的 PackageName。
获取到 packageName 以后,传递给 Javascript 调用者的对应回调中。
在 Javascript 环境中,获取当前应用的 PackageName 的大体调用以下:
bridge.invoke('getPackageName', null, function(packageName){ console.log(packageName); });
显然
function(packageName){ console.log(packageName); }
这个 Javascript 函数是没法传递到 java 环境中的,因此,能够采起的一个策略就是,
在 Javascript 环境中将全部回调统一管理起来,而只是将回调的 id 传递到 java 环境去,java 方法执行完毕以后,
将回调参数以及对应的回调 id 返回给 Javascript 环境,由 Javascript 来负责执行正确的回调。
这样,咱们就能够实现一个简单的回调机制:
在 java 环境中
class JavaProxy{ public void onTransact(String jsonInvoke, String jsonParam){ json = new Json(jsonInvoke); invokeName = json.getInvokeName(); // getPackageName callbackId = json.getCallbackId(); // 12345678xx invokeParam = new Param(jsonParam);// null ... ... JsProxy.invoke(callbackId, callbackParam); //发起 Javascript 调用,让 Javascript 去执行对应的回调 } }
在 javascript 环境中
bridge.invoke = function(name, param, callback){ var callbackId = new Date().getTime(); _callbacks[callbackId] = callback; var invoke = { "invokeName" : name, "callbackId" : callbackId }; JavaProxy.onTransact(JSON.stringify(invoke), JSON.stringify(param)); } bridge.invoke('getPackageName', null, function(packageName){ console.log(packageName); });
反之亦然。
问题都处理了,只须要设计对应的协议便可。
按照上面的讨论,
在 client 端,咱们使用:
Proxy.transact(invoke, callback);
来调用 server 端注册的方法。
在 server 端,咱们使用:
Stub.register(name, handler);
来注册新功能,使用
Stub.onTransact(invoke, handler);
来处理接收到的 client 端调用。
其中,invoke 包含所要执行的方法以及回调的信息,所以,invoke 的设计以下:
{ _invoke_id : 1234, _invoke_name : "xxx", _callback_id : 5678, _callback_name : "xxx" }
注意 _invoke_id 与 _invoke_name 的区别:
若是当前 invoke 是一个直接方法调用,那么 _invoke_id 应该是无效的。 若是当前 invoke 是一个回调,那么 _invoke_id + _invoke_name 共同决定回调的具体对象
因为咱们使用一 Hash 来保存各自环境中的回调函数。若是某个回调因为某种缘由没有被触发,那么,这个引用的对象就永远不会被回收。
针对这种问题,处理方案以下:
在 Java 环境中:
若是 WebView 被销毁了,应该手动移除全部的回调,而后禁用 javascript 。
另外,一个 WebView 可能加载多个 Html 页面,若是页面的 URL 发生了改变,这个时候也应该清理全部的回调,由于 Html 页面是无状态的,也不会传递相互数据。
这里有一点须要注意的是,若是 javascript 端是一个单页面应用,应该忽略 url 中 fragment (也就是 # 后面的部分) 的变化,由于并无发生传统意义上的页面跳转,
全部单应用的 Page 之间是可能有交互的。
在 javascript 环境中:
javascript 端状况好不少,由于 WebView 会本身管理每一个页面的资源回收问题。
请在对应的 html 页面中引入
<script src="js-bridge.js"></script>
初始化 JsBridge:
jsBridge = new JsBridge(vWebView);
加入 url 监控:
vWebView.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); Log.e("onPageFinished", url); jsBridge.monitor(url); } });
Java 注册处理方法:
jsBridge.register(new SimpleServerHandler("showPackageName") { @Override public void handle(String param, ServerCallback serverCallback) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { String packageName = getPackageName(); Tip.showTip(getApplicationContext(), "showPackageName:" + packageName); } }); } });
Java 在处理方法中回调 Javascript:
@Override public void handle(final String param, final ServerCallback serverCallback) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { User user = getUser(); Map<String, String> map = new Gson().fromJson(param, Map.class); String prefix = map.get("name_prefix"); Tip.showTip(mContext, "user.getName():" + prefix + "/" + user.getName()); if ("standard_error".equals(prefix)) { Map<String, String> map1 = new HashMap<>(); map1.put("msg", "get user failed"); String userMarshalling = new Gson().toJson(map1); serverCallback.invoke("fail", new MarshallableObject(userMarshalling)); } else { String userMarshalling = new Gson().toJson(user); serverCallback.invoke("success", new MarshallableObject(userMarshalling)); } } }); }
Java 执行 Js 函数:
jsBridge.invoke("jsFn4", new MarshallableString("yellow"), new ClientCallback<String>() { @Override public void onReceiveResult(String invokeName, final String invokeParam) { if ("success".equals(invokeName)) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { Tip.showTip(getApplicationContext(), invokeParam); } }); } } @Override public String getResult(String param) { return param; } });
销毁 JsBridge
@Override protected void onDestroy() { super.onDestroy(); jsBridge.destroy(); }
Javascript 的灵活性比较高,因此要简单一些:
Javascript 注册处理函数:
window.JavaBridge.serverRegister('jsFn4', function (transactInfo, color) { log("jsFn4:" + color); title.style.background = color; log("jsFn4:callback"); transactInfo.triggerCallback('success', 'background change to ' + color); });
Javascript 执行 Java 方法:
var sdk = { getUser: function (params) { var _invokeName = 'getUser'; var _invokeParam = params; var _clientCallback = params; window.JavaBridge.invoke(_invokeName, _invokeParam, _clientCallback); } }; sdk.getUser({ "name_prefix": "standard_error", "success": function (user) { log('sdk.getUser,success:' + user.name); }, "fail": function (error) { log('sdk.getUser,fail:' + error.msg); } })
详细 Demo 请参见 js-bridge-demo 工程