Android中JSBridge的原理与实现

首先我们来了解一下什么是JSBridge和为什么要使用JSBridge?

在开发中,为了追求开发的效率以及移植的便利性,一些展示性强的页面我们会偏向于使用h5来完成,功能性强的页面我们会偏向于使用native来完成,而一旦使用了h5,为了在h5中尽可能的得到native的体验,我们native层需要暴露一些方法给js调用,比如,弹Toast提醒,弹Dialog,分享等等,有时候甚至把h5的网络请求放到native去完成。

JSBridge做得好的一个典型就是微信,微信给开发者提供了JSSDK,该SDK中暴露了很多微信native层的方法,比如支付,定位等。

本文将对js和Native的通信原理和实现方法的一些探讨。

实现JSBridge关键点的原理剖析

Android中的JSBridge是H5与Native通信的桥梁,其作用是实现H5与Native间的双向通信。要实现H5与Native的双向通信,解决如下四个问题即可:

  1. Java如何调用JavaScript
  2. JavaScript如何调用Java
  3. 方法参数以及回调如何处理
  4. 通信协议的制定

下面从以上问题依次开始讨论

Java如何调用JavaScript

在WebView中,如果java要调用js的方法,是非常容易做到的,使用WebView.loadUrl(“javascript:function()”)即可,这样,就做到了JSBridge的native层调用h5层的单向通信

1
WebView.loadUrl("javascript:function()");

JavaScript如何调用Java

js调用Android的方法有以下四种:

  1. WebView 的 JavascriptInterface
  2. WebViewClient.shouldOverrideUrlLoading()
  3. WebChromeClient.onConsoleMessage()
  4. WebChromeClient.onJsPrompt()、onJsAlert()、onJsConfirm()
    我们先对此四种方案进行一个详细的描述,最后选择一个方案即可。本文章中采用了第四种方案。

JavascriptInterface

JavascriptInterface是Android官方提供的js和Native通信方案。其实现如下:

  1. 实现一个java类,供js调用

    1
    2
    3
    4
    5
    6
    public class MyJavascriptInterface {
    @JavascriptInterface
    public void showToast(String toast) {
    Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
    }
    }
  2. 在webView中注册这个类:

    1
    webView.addJavascriptInterface(new MyJavascriptInterface(), "javascriptInterface");
  3. 在js中直接调用这个接口:

    1
    2
    3
    function showToast(text){
    window.javascriptInterface.showToast(text);
    }
  4. 总结
    大多数人都知道WebView存在一个漏洞,见WebView中接口隐患与手机挂马利用,虽然该漏洞已经在Android 4.2上修复了(即使用@JavascriptInterface代替addJavascriptInterface),但是由于兼容性和安全性问题,基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现,所以我们只能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本的方案。

WebViewClient.shouldOverrideUrlLoading()

这个方法是拦截所有webView的跳转,页面可以构造一个特殊格式的Url跳转,shouldOverrideUrlLoading拦截Url后判断其格式,然后Native就能执行自身的逻辑了。

1
2
3
4
5
6
7
8
9
10
public class CustomWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (isJsBridgeUrl(url)) {
// JSbridge的处理逻辑
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
}

WebChromeClient.onConsoleMessage()

在js中执行console.log(), 会进入Android的WebChromeClient.consoleMessage()回调。

1
2
3
4
5
6
7
public class CustomWebChromeClient extends WebChromeClient {
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
super.onConsoleMessage(consoleMessage);
String msg = consoleMessage.message();//Javascript输入的Log内容
}
}

WebChromeClient.onJsPrompt()

  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是再完美不过了。
    1
    2
    3
    4
    5
    6
    7
    8
    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的组成部分

1
2
3
4
5
6
jsbridge://className:callbackAddress/methodName?jsonObj
// className: 表示java的类名
// callbackAddress: js回调的标识
// methodName: java中的方法名
// jsonObj: 接口数据

具体实践

调用流程

  1. 在js中,可以采用如下方法调用java方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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中, 可以如下实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    // 进入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);
    }
    });
    }
    }
    }

安全性及其它

  1. JSBridge类管理暴露给前端方法,前端调用的方法应该在此类中注册才可使用。register的实现是从Map中查找key是否存在,不存在则反射取得对应class中的所有方法,具体方法是在BridgeImpl中定义的,方法包括三个参数分别为WebView、JSONObject、CallBack。如果满足条件,则将所有满足条件的方法put到map中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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();
    }
    }
    }
  2. JSBridge类中的callJava方法就是将js传递过来的URL解析,根据将要调用的类名从刚刚建立的Map中找出,根据方法名调用具体的方法,并将解析出的三个参数传递进去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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;
    }
  3. CallBack类是用来回调js中回调方法的Java对应类。Java层处理好的返回结果是通过CallBack类来实现的。在这个回调类中传递的参数是JSONObject(返回结果)、WebView和port,port应与js传递过来的port相对应。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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);
    }
    });
    }
    }
  4. 在java层的JSBridge中注册方法,例如

    1
    JSBridge.register("bridge", BridgeImpl.class);

参考文献

文章目录
  1. 1. 实现JSBridge关键点的原理剖析
    1. 1.1. Java如何调用JavaScript
    2. 1.2. JavaScript如何调用Java
      1. 1.2.1. JavascriptInterface
      2. 1.2.2. WebViewClient.shouldOverrideUrlLoading()
      3. 1.2.3. WebChromeClient.onConsoleMessage()
      4. 1.2.4. WebChromeClient.onJsPrompt()
    3. 1.3. 方法参数以及回调处理
    4. 1.4. 通信协议的制定
  2. 2. 具体实践
    1. 2.1. 调用流程
    2. 2.2. 安全性及其它
  3. 3. 参考文献