三种常见的 hybrid 通信方式

4,286 阅读24分钟
原文链接: www.androidchina.net

说起hybrid大家不会陌生,主要意思就是native和h5混合开发。为什么要这样做呢?大家可以想象一下针对于同一个活动,如果使用纯native的开发方式,Android和iOS两边都要维护同一套界面甚至是逻辑,这样开发和维护的成本会很大,而使用hybrid的开发方式的话,让前端的同学去写一套界面和逻辑,对于native端来说只要使用对应的容器去展示就可以了(对于Android来说这个容器当然就是WebView)。那为什么不所有的页面都使用这种方式开发呢?因为使用h5来展示界面的话用户体验始终是不如native的,所以在这两者之间我们需要一个权衡。

介绍完了何为hybrid,我们来思考下面几个场景。

场景1,前端那边的页面有一个按钮,点击这个按钮需要显示一个native的组件(比如一个toast),或者点击这个按钮需要去在native端执行一个耗时的任务。

场景2,还是前端页面有一个按钮,点击这个按钮的逻辑是:如果登录了,则跳转到相应的界面,如果没有登录,则跳转到登录界面。而这个登录界面是我们native维护的。

看完上面两个场景,相信大家也发现了一个问题,hybrid这样的开发方式有一个问题需要解决,那就是前端和本地的通信。

下面让我带大家了解一下几种常见的通信方式吧。

前言

在看这篇文章之前你要确保你有那么一点点的js知识,没错只需要一点点,能看懂最简单的代码就可以。如果你之前没接触过js的话。。也没关系,我会把其中对应的逻辑用语言表达出来。

为什么需要用到js呢,因为前端体系中,像我们说的点击按钮这样的逻辑都是放在js脚本中执行的,有点像我们Android中的model层。(由于本人对前端的知识也只是略知一二,这个比方可能不太恰当,见谅见谅)。所以说到hybrid通信,主要就是前端的js和我们Android端的通信。

传统的JSInterface

首先先介绍一下最普通的一种通信方式,就是使用Android原生的JavascriptInterface来进行js和java的通信。具体方式如下:

首先先看一段html代码


<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN" dir="ltr">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    <script type="text/javascript">
        function showToast(toast) {
             javascript:control.showToast(toast);
        }
        function log(msg){
             console.log(msg);
        }
    </script>

</head>

<body>
    <input type="button" value="toast"
        onClick="showToast('Hello world')" />
</body>
</html>

很简单,一个button,点击这个button就执行js脚本中的showToast方法。

1460940835128109.png

而这个showToast方法做了什么呢?


function showToast(toast) {
     javascript:control.showToast(toast);
}

可以看到control.showToast,这个是什么我们等下再说,下面看我们java的代码。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fitsSystemWindows="true"
     tools:context="zjutkz.com.tranditionaljsdemo.MainActivity">

     <WebView
           android:id="@+id/webView"
           android:layout_width="match_parent"
           android:layout_height="match_parent">

     </WebView>

</LinearLayout>


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);

          WebSettings webSettings = webView.getSettings();

          webSettings.setJavaScriptEnabled(true);

          webView.addJavascriptInterface(new JsInterface(), "control");

          webView.loadUrl("file:///android_asset/interact.html");
     }

     public class JsInterface {

         @JavascriptInterface
         public void showToast(String toast) {
              Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
              log("show toast success");
         }

         public void log(final String msg){
              webView.post(new Runnable() {
                   @Override
                   public void run() {
                         webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
                   }
              });
         }
    }
}

首先界面很简单,一个WebView。在对应的activity中做的事也就几件,首先打开js通道。


WebSettings webSettings = webView.getSettings();

webSettings.setJavaScriptEnabled(true);

然后通过WebView的addJavascriptInterface方法去注入一个我们自己写的interface。


webView.addJavascriptInterface(new JsInterface(), "control");

public class JsInterface {

     @JavascriptInterface
     public void showToast(String toast) {
          Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
          log("show toast success");
     }

     public void log(final String msg){
          webView.post(new Runnable() {
               @Override
               public void run() {
                    webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
               }
          });
     }
 }

可以看到这个interface我们给它取名叫control。

最后loadUrl。


webView.loadUrl("file:///android_asset/interact.html");

好了,让我们再看看js脚本中的那个showToast()方法。


function showToast(toast) {
      javascript:control.showToast(toast);
}

这里的control就是我们的那个interface,调用了interface的showToast方法


@JavascriptInterface
public void showToast(String toast) {
     Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
     log("show toast success");
}

可以看到先显示一个toast,然后调用log()方法,log()方法里调用了js脚本的log()方法。


function log(msg){
     console.log(msg);
}

js的log()方法做的事就是在控制台输出msg。

这样我们就完成了js和java的互调,是不是很简单。但是大家想过这样有什么问题吗?如果你使用的是AndroidStudio,在你的webSettings.setJavaScriptEnabled(true);这句函数中,AndroidStudio会给你一个提示。

1460941090480837.png

这个提示的意思呢,就是如果你使用了这种方式去开启js通道,你就要小心XSS攻击了,具体的大家可以参考wooyun上的这篇文章

虽然这个漏洞已经在Android 4.2上修复了,就是使用@JavascriptInterface这个注解。但是你得考虑兼容性啊,你不能保证,尤其在中国这样碎片化严重的地方,每个用户使用的都是4.2+的系统。所以基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现js和java的通信了。那怎么办呢?方法都是人想出来的嘛,下面让我们看解决方案。

JSBridge

JSBridge,顾名思义,就是和js沟通的桥梁。其实这个技术在Android中已经不算新了,相信有些同学也看到过不少实现方案,这里说一种我的想法吧。其实说是我的想法,实际是公司里的大牛实现的,我现在做的就是维护并且扩展,不过这里还是拿出来和大家分享一下。

思路

首先先说思路,有经验的同学可能都知道Android的WebView中有一个WebChromeClient类,这个类其实就是用来监听一些WebView中的事件的,我们发现其中有三个这样的方法。


@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
      return super.onJsPrompt(view, url, message, defaultValue, result);
}

@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
     return super.onJsAlert(view, url, message, result);
}

@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
      return super.onJsConfirm(view, url, message, result);
}

这三个方法其实就对应于js中的alert(警告框),comfirm(确认框)和prompt(提示框)方法,那这三个方法有什么用呢?前面我们说了JSBridge的作用是提供一种js和java通信的框架,其实我们可以利用这三个方法去完成这样的事。比如我们可以在js脚本中调用alert方法,这样对应的就会走到WebChromeClient类的onJsAlert()方法中,我们就可以拿到其中的信息去解析,并且做java层的事情。那是不是这三个方法随便选一个就可以呢?其实不是的,因为我们知道,在js中,alert和confirm的使用概率还是很高的,特别是alert,所以我们最好不要使用这两个通道,以免出现不必要的问题。

好了,说到这里我们前期的准备工作也就做好了,其实就是通过重写WebView中WebChromeClient类的onJsPrompt()方法来进行js和java的通信。

有了实现方案,下面就是一些具体的细节了,大家有没有想过,怎么样才能让java层知道js脚本需要调用的哪一个方法呢?怎么把js脚本的参数传递进来呢?同步异步的方式又该怎么实现呢?下面提供一种我的思路。

首先大家都知道http是什么,其实我们的JSBridge也可以效仿一下http,定义一个自己的协议。比如规定sheme,path等等。下面来看一下一些的具体内容:


hybrid://JSBridge:1538351/method?{“message”:”msg”}

是不是和http协议有一点像,其实我们可以通过js脚本把这段协议文本传递到onPropmt()方法中并且进行解析。比如,sheme是hyrid://开头的就表示是一个hybrid方法,需要进行解析。后面的method表示方法名,message表示传递的参数等等。

有了这样一套协议,我们就可以去进行我们的通信了。

代码

先看一下我们html和js的代码


<!DOCTYPE HTML>

<html>
<head>
    <meta charset="utf-8">
    <script src="file:///android_asset/jsBridge.js" type="text/javascript"></script>
</head>

<body>
     <div class="blog-header">
          <h3>JSBridge</h3>
     </div>
     <ul class="entry">

     <br/>
     <li>
         toast展示<br/>
        <button onclick="JsBridge.call('JSBridge','toast',{'message':'我是气泡','isShowLong':0},function(res){});">toast</button>
     </li>

     <br/>
     <li>
         异步任务<br/>
         <button onclick="JsBridge.call('JSBridge','plus',{'data':1},function(res){console.log(JSON.stringify(res))});">plus</button>
     </li>

     <br/>
     <br/>
     </ul>

</body>
</html>


(function (win, lib) {
     var doc = win.document;
     var hasOwnProperty = Object.prototype.hasOwnProperty;
     var JsBridge = win.JsBridge || (win.JsBridge = {});
     var inc = 1;
     var LOCAL_PROTOCOL = 'hybrid';
     var CB_PROTOCOL = 'cb_hybrid';
     var CALLBACK_PREFIX = 'callback_';

     //核心功能,对外暴露
     var Core = {

         call: function (obj, method, params, callback, timeout) {
               var sid;

             if (typeof callback !== 'function') {
                   callback = null;
             }

             sid = Private.getSid();

             Private.registerCall(sid, callback);
             Private.callMethod(obj, method, params, sid);

       },

      //native代码处理 成功/失败 后,调用该方法来通知js
      onComplete: function (sid, data) {
           Private.onComplete(sid, data);
      }
 };

//私有功能集合
var Private = {
     params: {},
     chunks: {},
     calls: {},

     getSid: function () {
          return Math.floor(Math.random() * (1 << 50)) + '' + inc++;
     },

     buildParam: function (obj) {
          if (obj && typeof obj === 'object') {
                return JSON.stringify(obj);
          } else {
                return obj || '';
          }
     },

     parseData: function (str) {
          var rst;
          if (str && typeof str === 'string') {
                try {
                       rst = JSON.parse(str);
                } catch (e) {
                       rst = {
                           status: {
                           code: 1,
                           msg: 'PARAM_PARSE_ERROR'
                       }
                };
          }
          } else {
               rst = str || {};
          }

          return rst;
     },

     //根据sid注册calls的回调函数
     registerCall: function (sid, callback) {
         if (callback) {
               this.calls[CALLBACK_PREFIX + sid] = callback;
          }
     },

     //根据sid删除calls对应的回调函数,并返回call对象
     unregisterCall: function (sid) {
           var callbackId = CALLBACK_PREFIX + sid;
           var call = {};

           if (this.calls[callbackId]) {
                 call.callback = this.calls[callbackId];
                 delete this.calls[callbackId];
           }

           return call;
     },

     //生成URI,调用native功能
     callMethod: function (obj, method, params, sid) {
          // hybrid://objectName:sid/methodName?params
          params = Private.buildParam(params);

          var uri = LOCAL_PROTOCOL + '://' + obj + ':' + sid + '/' + method + '?' + params;

          var value = CB_PROTOCOL + ':';
          window.prompt(uri, value);
     },

     onComplete: function (sid, data) {
           var callObj = this.unregisterCall(sid);
           var callback = callObj.callback;

           data = this.parseData(data);

           callback && callback(data);
     }
 };

for (var key in Core) {
     if (!hasOwnProperty.call(JsBridge, key)) {
           JsBridge[key] = Core[key];
     }
}
})(window);

有前端经验的同学应该能很轻松的看懂这样的代码,对于看不懂的同学我来解释一下,首先看界面。

1460941200116357.png

可以看到有两个按钮,对应着html的这段代码

<li>
      toast展示<br/>
      <button onclick="JsBridge.call('JSBridge','toast',{'message':'我是气泡','isShowLong':0},function(res){});">toast</button>
</li>

<br/>
<li>
      异步任务<br/>
      <button onclick="JsBridge.call('JSBridge','plus',{'data':1},function(res){console.log(JSON.stringify(res))});">toast</button>
</li>

点击按钮会执行js脚本的这段代码


call: function (obj, method, params, callback, timeout) {
      var sid;

      if (typeof callback !== 'function') {
            callback = null;
      }

      sid = Private.getSid();

      Private.registerCall(sid, callback);
      Private.callMethod(obj, method, params, sid);

}

它其实就是一个函数,名字叫call,括号里的是它的参数(obj, method, params, callback, timeout)。那这几个参数是怎么传递的呢?回过头看我们的html代码,点击第一个按钮,会执行这个语句


<button onclick="JsBridge.call('JSBridge','toast',{'message':'我是气泡','isShowLong':0},function(res){});">toast</button>

其中括号(‘JSBridge’,’toast’,{‘message’:’我是气泡’,’isShowLong’:0},function(res){})里的第一个参数’JSBridge’对应着前面的obj,’toast’对应着method,以此类推。第二个按钮也是一样。

然后在call这个方法内,会执行Private类的registerCall和callMethod,我们来看callMehod()。


//生成URI,调用native功能
callMethod: function (obj, method, params, sid) {
      // hybrid://objectName:sid/methodName?params
      params = Private.buildParam(params);

      var uri = LOCAL_PROTOCOL + '://' + obj + ':' + sid + '/' + method + '?' + params;

      var value = CB_PROTOCOL + ':';
      window.prompt(uri, value);
}

注释说的很清楚了,就是通过传递进来的参数生成uri,并且调用window.prompt()方法,这个方法大家应该很眼熟吧,没错,在调用这个方法之后,程序就会相应的走到java代码的onJsPrompt()方法中。而生成的uri则是我们上面说过的那个我们自己定义的协议格式。

好了,我们总结一下这两个前端的代码。其实很简单,以界面的第一个按钮toast为例,点击这个按钮,它会执行相应的js脚本代码,然后就会像我们前面所讲的那样,走到onJsPrompt()方法中,下面让我们看看对应的java代码。


public class InjectedChromeClient extends WebChromeClient {
     private final String TAG = "InjectedChromeClient";

     private JsCallJava mJsCallJava;

     public InjectedChromeClient() {
          mJsCallJava = new JsCallJava();
     }

     @Override
     public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
          result.confirm(mJsCallJava.call(view, message));
          return true;
      }
}

这是对应的WebChromeClient类,可以看到在onJsPrompt()方法中我们只做了一件事,就是丢给JsCallJava类去解析,再看JsCallJava类之前,我们可以先看看onJsPrompt()这个方法到底传进来了什么。

1460941346959608.png

可以看到,我们传给JsCallJava类的那个message,就像我们前面定义的协议一样。sheme是hybrid://,表示这是一个hybrid方法,host是JSBridge,方法名字是toast,传递的参数是以json格式传递的,具体内容如图。不知道大家有没有发现,这里我有一个东西没有讲,就是JSBridge:后面的那串数字,这串数字是干什么用的呢?大家应该知道,现在我们整个调用过程都是同步的,这意味着我们没有办法在里面做一些异步的操作,为了满足异步的需求,我们就需要定义这样的port,有了这串数字,我们在java层就可以做异步的操作,等操作完成以后回调给js脚本,js脚本就通过这串数字去得到对应的callback,有点像startActivity中的那个requestCode。大家没听懂也没关系,后面我会在代码中具体讲解。

好了,下面我们可以来看JsCallJava这个类的具体代码了。


public class JsCallJava {
     private final static String TAG = "JsCallJava";

     private static final String BRIDGE_NAME = "JSBridge";

     private static final String SCHEME="hybrid";

     private static final int RESULT_SUCCESS=200;
     private static final int RESULT_FAIL=500;
     private ArrayMap<String, ArrayMap<String, Method>> mInjectNameMethods = new ArrayMap<>();

     private JSBridge mWDJSBridge = JSBridge.getInstance();

     public JsCallJava() {
          try {
                 ArrayMap<String, Class<? extends IInject>> externals = mWDJSBridge.getInjectPair();
                 if (externals.size() > 0) {
                       Iterator<String> iterator = externals.keySet().iterator();
                       while (iterator.hasNext()) {
                             String key = iterator.next();
                             Class clazz = externals.get(key);
                             if (!mInjectNameMethods.containsKey(key)) {
                                    mInjectNameMethods.put(key, getAllMethod(clazz));
                              }
                       }
                 }
          } catch (Exception e) {
                 Log.e(TAG, "init js error:" + e.getMessage());
          }
     }

     private ArrayMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
           ArrayMap<String, Method> mMethodsMap = new ArrayMap<>();
           //获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法
           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]==JsCallback.class){
                                 mMethodsMap.put(name, method);
                        }
                 }
            }
            return mMethodsMap;
      }
      public String call(WebView webView, String jsonStr) {
           String methodName = "";
           String name = BRIDGE_NAME;
           String param = "{}";
           String result = "";
           String sid="";
           if (!TextUtils.isEmpty(jsonStr) && jsonStr.startsWith(SCHEME)) {
                 Uri uri = Uri.parse(jsonStr);
                 name = uri.getHost();
                 param = uri.getQuery();
                 sid = getPort(jsonStr);
                 String path = uri.getPath();
                 if (!TextUtils.isEmpty(path)) {
                       methodName = path.replace("/", "");
                 }
           }

           if (!TextUtils.isEmpty(jsonStr)) {
                 try {
                        ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);

                        Object[] values = new Object[3];
                        values[0] = webView;
                        values[1] = new JSONObject(param);
                        values[2]=new JsCallback(webView,sid);
                        Method currMethod = null;
                        if (null != methodMap && !TextUtils.isEmpty(methodName)) {
                              currMethod = methodMap.get(methodName);
                        }
                        // 方法匹配失败
                        if (currMethod == null) {
                              result = getReturn(jsonStr, RESULT_FAIL, "not found method(" + methodName + ") with valid parameters");
                        }else{
                              result = getReturn(jsonStr, RESULT_SUCCESS, currMethod.invoke(null, values));
                        }
                  } catch (Exception e) {
                        e.printStackTrace();
                  }
            } else {
                  result = getReturn(jsonStr, RESULT_FAIL, "call data empty");
            }

            return result;
      }

      private String getPort(String url) {
           if (!TextUtils.isEmpty(url)) {
                 String[] arrays = url.split(":");
                 if (null != arrays && arrays.length >= 3) {
                       String portWithQuery = arrays[2];
                       arrays = portWithQuery.split("/");
                       if (null != arrays && arrays.length > 1) {
                             return arrays[0];
                       }
                 }
            }
            return null;
      }

      private String getReturn(String reqJson, int stateCode, Object result) {
            String insertRes;
            if (result == null) {
                  insertRes = "null";
            } else if (result instanceof String) {
                  //result = ((String) result).replace("\"", "\\\"");
                  insertRes = String.valueOf(result);
            } else if (!(result instanceof Integer)
                    && !(result instanceof Long)
                    && !(result instanceof Boolean)
                    && !(result instanceof Float)
                    && !(result instanceof Double)
                    && !(result instanceof JSONObject)) { // 非数字或者非字符串的构造对象类型都要序列化后再拼接
                    insertRes = result.toString();//mGson.toJson(result);
             } else { //数字直接转化
                    insertRes = String.valueOf(result);
             }
             //String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes);
             Log.d(TAG, " call json: " + reqJson + " result:" + insertRes);
             return insertRes;
      }
}

有点长,不过其实逻辑很好理解。首先我们调用的是call这个方法。它里面做了什么呢


public String call(WebView webView, String jsonStr) {
     String methodName = "";
     String name = BRIDGE_NAME;
     String param = "{}";
     String result = "";
     String sid="";
     if (!TextUtils.isEmpty(jsonStr) && jsonStr.startsWith(SCHEME)) {
           Uri uri = Uri.parse(jsonStr);
           name = uri.getHost();
           param = uri.getQuery();
           sid = getPort(jsonStr);
           String path = uri.getPath();
           if (!TextUtils.isEmpty(path)) {
                 methodName = path.replace("/", "");
           }
     }

     if (!TextUtils.isEmpty(jsonStr)) {
           try {
                 ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);

                 Object[] values = new Object[3];
                 values[0] = webView;
                 values[1] = new JSONObject(param);
                 values[2]=new JsCallback(webView,sid);
                 Method currMethod = null;
                 if (null != methodMap && !TextUtils.isEmpty(methodName)) {
                       currMethod = methodMap.get(methodName);
                 }
                 // 方法匹配失败
                 if (currMethod == null) {
                       result = getReturn(jsonStr, RESULT_FAIL, "not found method(" + methodName + ") with valid parameters");
                 }else{
                       result = getReturn(jsonStr, RESULT_SUCCESS, currMethod.invoke(null, values));
                 }
           } catch (Exception e) {
                 e.printStackTrace();
           }
     } else {
           result = getReturn(jsonStr, RESULT_FAIL, "call data empty");
     }

     return result;
}

可以看到其实就是通过js脚本传递过来的参数得到了方法名字,sid(前面说的那串数字)等等内容。下面看这段代码


ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);

通过name去得到一个map,这里的name是我们刚刚解析得到了,对应实际情况就是JSBridge,那这个mInjectNameMethods又是什么呢?


private ArrayMap<String, ArrayMap<String, Method>> mInjectNameMethods = new ArrayMap<>();

private JSBridge mJSBridge = JSBridge.getInstance();

public JsCallJava() {
      try {
            ArrayMap<String, Class<? extends IInject>> externals = mJSBridge.getInjectPair();
            if (externals.size() > 0) {
                  Iterator<String> iterator = externals.keySet().iterator();
                  while (iterator.hasNext()) {
                       String key = iterator.next();
                       Class clazz = externals.get(key);
                       if (!mInjectNameMethods.containsKey(key)) {
                              mInjectNameMethods.put(key, getAllMethod(clazz));
                       }
                  }
             }
       } catch (Exception e) {
             Log.e(TAG, "init js error:" + e.getMessage());
       }
}

private ArrayMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
      ArrayMap<String, Method> mMethodsMap = new ArrayMap<>();
     //获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法
     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]==JsCallback.class){
                         mMethodsMap.put(name, method);
                 }
           }
     }
     return mMethodsMap;
}

可以看到我们有一个JSBridge类,在JsCallJava的构造函数中,我们通过JSBridge这个类的getInjectPair()方法得到了一个String和class的映射关系,并且把class中符合标准的方法拿出来存放到mInjectNameMethods中,以便我们在call方法中调用。下面来看看JSBridge类。


public class JSBridge {
     public static final String BRIDGE_NAME = "JSBridge";

     private static JSBridge INSTANCE = new JSBridge();

     private boolean isEnable=true;

     private ArrayMap<String, Class<? extends IInject>> mClassMap = new ArrayMap<>();

     private JSBridge() {
           mClassMap.put(BRIDGE_NAME, JSLogical.class);
     }

     public static JSBridge getInstance() {
          return INSTANCE;
     }

     public boolean addInjectPair(String name, Class<? extends IInject> clazz) {
          if (!mClassMap.containsKey(name)) {
                mClassMap.put(name, clazz);
                return true;
          }
          return false;
     }

     public boolean removeInjectPair(String name,Class<? extends IInject> clazz) {
          if (TextUtils.equals(name,BRIDGE_NAME)) {
                return false;
          }
          Class clazzValue=mClassMap.get(name);
          if(null!=clazzValue && (clazzValue == clazz)){
                mClassMap.remove(name);
                return true;
           }
           return false;

     }
     public ArrayMap<String, Class<? extends IInject>> getInjectPair() {
           return mClassMap;
     }
}

它的getInjectPair方法其实就是得到了mClassMap,这个map在JSBridge类初始化的时候就有一个默认的值了。


public static final String BRIDGE_NAME = "JSBridge";

private JSBridge() {
      mClassMap.put(BRIDGE_NAME, JSLogical.class);
}

key是”JSBridge”,value是我们的JSLogincal类。


public class JSLogical implements IInject {

/**
 * toast
 *
 * @param webView 浏览器
 * @param param 提示信息
 */
 public static void toast(WebView webView, JSONObject param, final JsCallback callback) {
      String message = param.optString("message");
      int isShowLong = param.optInt("isShowLong");
      Toast.makeText(webView.getContext(), message, isShowLong == 0 ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
      if (null != callback) {
           try {
                 JSONObject object = new JSONObject();
                 object.put("result", true);
                 invokeJSCallback(callback, object);
           } catch (Exception e) {
                 e.printStackTrace();
           }
      }
 }

/**
 * 加一
 *
 * @param webView
 * @param param
 * @param callback
 */
 public static void plus(WebView webView, final JSONObject param, final JsCallback callback) {
      new Thread(new Runnable() {
            @Override
            public void run() {
                 try {
                       Thread.sleep(2000);
                       int original = param.optInt("data");
                       original = original + 1;
                       if (null != callback) {
                             JSONObject object = new JSONObject();
                             object.put("after plussing", original);
                             invokeJSCallback(callback, object);
                       }
                 } catch (Exception e) {
                       e.printStackTrace();
                 }
            }
      }).start();
 }

private static void invokeJSCallback(JsCallback callback, JSONObject objects) {
      invokeJSCallback(callback, true, null, objects);
}

public static void invokeJSCallback(JsCallback callback, boolean isSuccess, String message, JSONObject objects) {
     try {
           callback.apply(isSuccess, message, objects);
     } catch (JsCallback.JsCallbackException e) {
           e.printStackTrace();
     }
}

对这个类上面的两个方法有没有很眼熟?名字和js脚本中的那两个方法一样有木有。我们调用链最后就会走到相应的同名方法中!

上面就是js调js的整个过程了,其实吧,不应该放这么多的代码的,搞得像是源码分析一样,不过我觉得这样还是有一定好处的,至少跟着代码走一遍能加深印象嘛。

我们还是来捋一捋整个过程。

(1) 在js脚本中把对应的方法名,参数等写成一个符合协议的uri,并且通过window.prompt方法发送给java层。
(2) 在java层的onJsPrompt方法中接受到对应的message之后,通过JsCallJava类进行具体的解析。
(3) 在JsCallJava类中,我们解析得到对应的方法名,参数等信息,并且在map中查找出对应的类的方法。

这里多说一句,还记得我们定义的协议中的host是什么吗?

hybrid://JSBridge:875725/toast?{“message”:”我是气泡”,”isShowLong”:0}

是JSBridge,而我们在JsCallJava类中是通过这个host去查找对应的类的,我们可以看到在JSBridge类中


public static final String BRIDGE_NAME = "JSBridge";

private JSBridge() {
      mClassMap.put(BRIDGE_NAME, JSLogical.class);
}

这意味着,如果你可以更换你的host,叫aaa都没关系,只要你在对应的map中的key也是aaa就可以了。

可能有的同学会说何必这么麻烦,直接在JsCallJava类中定义方法不就好了,这样还省的去写那么多的逻辑。可是大家有想过如果你把所有js脚本想要调用的方法都写在JsCallJava类中,这个类会有多难扩展和维护吗?而像我这样,如果你的js脚本处理的是登录相关逻辑,你可以写一个LoginLogical.class,如果是业务相关,你可以写一个BizLogical.class,这样不仅清晰,而且解耦。

当然,如果你仔细的看过代码,会发现其实在native层的那些同名函数其实是有规范的。

首先必须要是public static的,因为这样调用会更方便。

其次参数也有要求,有且仅有三个参数,WebView,JsonObject和一个Callback。WebView用来提供可能需要的context,另外java执行js方法也需要WebView对象。JsonObject是js脚本传递过来的参数。而Callback则是java用于回调js脚本的。

可能你会发现JSBridge里处处都是规范,协议需要规范,参数需要规范。这些其实都是合理的,因为规范所以安全。

#####(4) 在得到对应的方法之后,就去调用它,以我们的toast为


/**
 * toast
 *
 * @param webView 浏览器
 * @param param 提示信息
 */
public static void toast(WebView webView, JSONObject param, final JsCallback callback) {
     String message = param.optString("message");
     int isShowLong = param.optInt("isShowLong");
     Toast.makeText(webView.getContext(), message, isShowLong == 0 ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
     if (null != callback) {
          try {
               JSONObject object = new JSONObject();
               object.put("result", true);
               invokeJSCallback(callback, object);
          } catch (Exception e) {
               e.printStackTrace();
          }
     }
}

拿到对应的信息,直接makeToast就好了。

以上就是全部js调用java的过程,那我们java执行完逻辑以后,怎么回调js呢?这里我们以另外一个按钮的例子来说。


<button onclick="JsBridge.call('JSBridge','plus',{'data':1},function(res){console.log(JSON.stringify(res))});">plus</button>

js脚本传递的一个json的参数,{“data”:1},从名字可以看出是先要java执行一个加逻辑。


/**
 * 加一
 *
 * @param webView
 * @param param
 * @param callback
 */
public static void plus(WebView webView, final JSONObject param, final JsCallback callback) {
     new Thread(new Runnable() {
         @Override
         public void run() {
              try {
                    Thread.sleep(2000);
                    int original = param.optInt("data");
                    original = original + 1;
                    if (null != callback) {
                         JSONObject object = new JSONObject();
                         object.put("after plussing", original);
                         invokeJSCallback(callback, object);
                     }
               } catch (Exception e) {
                     e.printStackTrace();
               }
          }
     }).start();
}

这里我们模拟一下耗时操作,可以帮助大家更好的理解JSBridge中的异步操作。对应java层的方法执行完+1的操作之后,把结果封装成一个jsonObject,并且调用invokeJSCallback方法。


public static void invokeJSCallback(JsCallback callback, boolean isSuccess, String message, JSONObject objects) {
     try {
           callback.apply(isSuccess, message, objects);
     } catch (JsCallback.JsCallbackException e) {
           e.printStackTrace();
     }
}

invokeJSCallback方法中直接调用了callback的apply方法。


private static final String CALLBACK_JS_FORMAT = "javascript:JsBridge.onComplete('%s', %s);";

public void apply(boolean isSuccess, String message, JSONObject object) throws JsCallbackException {
     if (mWebViewRef.get() == null) {
           throw new JsCallbackException("the WebView related to the JsCallback has been recycled");
     }
     if (!mCouldGoOn) {
           throw new JsCallbackException("the JsCallback isn't permanent,cannot be called more than once");
     }
     JSONObject result = new JSONObject();

     try {
          JSONObject code=new JSONObject();
          code.put("code", isSuccess ? 0 : 1);
          if(!isSuccess && !TextUtils.isEmpty(message)){
               code.putOpt("msg",message);
          }
          if(isSuccess){
               code.putOpt("msg", TextUtils.isEmpty(message)?"SUCCESS":message);
          }
          result.putOpt("status", code);
          if(null!=object){
                result.putOpt("data",object);
          }
     } catch (Exception e) {
           e.printStackTrace();
     }
     final String jsFunc = String.format(CALLBACK_JS_FORMAT, mSid, String.valueOf(result));

     if (mWebViewRef != null && mWebViewRef.get() != null) {
           mHandler.post(new Runnable() {
                @Override
                public void run() {
                     mWebViewRef.get().loadUrl(jsFunc);
                }
           });

     }
}

在apply方法中,我们直接拼装了一个jsonObject,里面包括了我们想要返回给js脚本的结果,并且直接调用了js的onComplete方法。


onComplete: function (sid, data) {
     var callObj = this.unregisterCall(sid);
     var callback = callObj.callback;

     data = this.parseData(data);

     callback && callback(data);
}

可以看到js的onComplete通过sid(那一串数字)拿到对应的callback并执行,而我们plus的callback里做了什么呢?


function(res){console.log(JSON.stringify(res))}

直接在控制台中输出结果。

所以当我们点击plug按钮以后,过两秒我们就可以在logcat中看到如下输出

1460941696136548.png

好了,至此所有和JSBridge相关的代码就分析完了。其实原理非常的简单,通过js的window.prompt方法将事先定义好的协议文本传输到java层,然后java层进行解析并调用相应的方法,最后通过callback将结果返回给js脚本。中间我们使用的那些类可以更好的解耦,如果你有心,甚至可以把所用逻辑相关代码抽离出来,把剩余的代码写成JSBridge.core作为库来使用。这样你想加什么功能直接写,不用改任何的源码。

UrlRouter

其实严格的说,UrlRouter不算是js和java的通信,它只是一个通过url来让前端唤起native页面的框架。不过千万不要小看它的作用,如果协议定义的合理,它可以让前端,Android和iOS三端有一个高度的统一,十分方便。

思路

其实吧,这个思路比JSBridge还要简单,就是我们通过自己实现的框架去拦截前端同学写的url,发现如果是符合我们UrlRouter的协议的话,就跳转到相应的页面。

至于怎么拦截呢?当然是通过WebViewClient类的shouldOverrideUrlLoading方法咯。

代码

首先我们还是先看一个html代码


<html>
     <title>Login</title>
     <input type="button" value="login" onclick="javascript:location.href='http://login.h5.zjutkz.net/'">
</html>

很简单,有一个按钮,通过点击这个按钮,会加载一个url,这个url是login.h5.zjutkz.net/。

这里多说一句,如果你也想用UrlRouter这样的形式的话,协议的sheme最好是http这样开头的,不要自己去重新定义,因为这样可以保证前端同学逻辑的清晰。如果你想着自己定义一个sheme叫shemeA,公司做别的app的同学也定义一个sheme叫shemeB,加上本来就要的http,前端的同学可能脑子都昏了。。。

下面来看看WebViewClient类。


public class NavWebViewClient extends WebViewClient {

     private Context context;

     public NavWebViewClient(Context context){
          this.context = context;
     }

     @Override
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
          if( Nav.from(context).toUri(url)){
               return true;
          }

          view.loadUrl(url);
               return true;
          }
     }
}

很简单,在shouldOverrideUrlLoading方法中先拦截url交给Nav类处理,如果返回true则表示需要拦截,直接return true,否则交给WebView去loadUrl。

接下去看看Nav。


public class Nav {

     private static final String TAG = "Nav";

     public static Nav from(final Context context) {
          return new Nav(context);
     }

     public boolean toUri(final String uri) {
          if(TextUtils.isEmpty(uri)) return false;
          return toUri(Uri.parse(uri));
     }

     public boolean toUri(final Uri uri) {

          Log.d(TAG, uri.toString());

          final Intent intent = to(uri);

          for (;;) try {

                intent.setPackage(mContext.getPackageName());

                PackageManager pm = mContext.getPackageManager();

                final ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
                if(info == null) {
                       throw new ActivityNotFoundException("No Activity found to handle " + intent);
                } else {
                       intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
                }

                mContext.startActivity(intent);
                return true;

         } catch (final ActivityNotFoundException e) {

                return false;
         }
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private void startActivities(final Intent[] intents) {
         mContext.startActivities(intents);
    }

    private Intent to(final Uri uri) {
         mIntent.setData(uri);

         return mIntent;
    }

     private Nav(final Context context) {
          mContext = context;
          mIntent = new Intent(Intent.ACTION_VIEW);
     }

     private final Context mContext;
           private final Intent mIntent;
     }
}

我们在NavWebViewClient类中是这样调用的


Nav.from(context).toUri(url)

from方法创建了一个Nav类的实例,下面来看看toUri方法


public boolean toUri(final String uri) {
     if(TextUtils.isEmpty(uri)) return false;
     return toUri(Uri.parse(uri));
}

public boolean toUri(final Uri uri) {

     Log.d(TAG, uri.toString());

     final Intent intent = to(uri);

     for (;;) try {

          intent.setPackage(mContext.getPackageName());

          PackageManager pm = mContext.getPackageManager();

          final ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
          if(info == null) {
                throw new ActivityNotFoundException("No Activity found to handle " + intent);
          } else {
                intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
          }

          mContext.startActivity(intent);
          return true;

    } catch (final ActivityNotFoundException e) {

          return false;
    }
}

private Intent to(final Uri uri) {
     mIntent.setData(uri);

     return mIntent;
}

在toUri方法中调用了to方法,to方法做的就是将uri以setData的方式注入到intent中。

接着通过一系列PackageManager的方法去判断有没有符合uri的activity,如果有则直接startActivity。

是不是很简单,下面我以文中最开头的场景2为例子。

我们native端需要一个LoginActivity,并且根据上面的代码我们知道,这个LoginActivity必须要配置上对应的data才行。

<activity android:name=".activity.LoginActivity">
       <intent-filter>
       <action android:name="android.intent.action.VIEW"/>
       <category android:name="android.intent.category.DEFAULT"/>
       <category android:name="android.intent.category.BROWSABLE"/>
       <category android:name="${NAV_CATEGORY}"/>
       <data android:scheme="${NAV_SCHEMA}"/>
       <data android:host="${NAV_HOST}"/>
       </intent-filter>
</activity>
defaultConfig {
      applicationId "zjutkz.com.navigationdemo"
      minSdkVersion 15
      targetSdkVersion 23
      versionCode 1
      versionName "1.0"
      manifestPlaceholders = ["NAV_SCHEMA": "http", "NAV_HOST": "login.h5.zjutkz.net","NAV_CATEGORY": "zjutkz.net"]
}

这是我们的manifest文件,可以看到已经通过gradle配置了对应的data。

这里我为什么要用grdle去配置呢?想象如果你有十几个页面,你难道要在manifest中都写一遍吗?用我这种方式,直接在build.gradle中写一遍就可以了。这里我是想给大家传递一个思想:

使用gradle我们可以做很多自动化的事,千万不要自己给自己找麻烦了。

看到这儿大家肯定会觉得,就这么简单?是的,大体的框架就这么简单,但是如果你想真正的用好它,还需要做很多工作。

比如在跳转到native页面,做完响应的逻辑之后,你怎么通知前端去更新呢?这里你可以使用startActivityForResult,也可以使用广播,甚至是eventBus。这需要你在你的框架内做好封装。

再比如,上面的例子是最简单的,但是如果前端的同学想在跳到对应的native页面的时候加上一些参数呢?你的intent该怎么处理?

还有,如果你想你的框架鲁棒性够强,是不是得提供一个hook工具呢?让调用者可以hook掉你内部的那个intent,从而添加自己想要添加的数据。

这些都是要解决的问题,这里我就不给大家上具体的代码了。毕竟只有你自己去实现了以后才会有更深的理解。

原文出处zjutkz’s blog

转载请注明:Android开发中文站 »