几种常见的hybrid通讯方式

原文出处zjutkz's blog。 javascript

提及hybrid你们不会陌生,主要意思就是native和h5混合开发。为何要这样作呢?你们能够想象一下针对于同一个活动,若是使用纯native的开发方式,Android和iOS两边都要维护同一套界面甚至是逻辑,这样开发和维护的成本会很大,而使用hybrid的开发方式的话,让前端的同窗去写一套界面和逻辑,对于native端来讲只要使用对应的容器去展现就能够了(对于Android来讲这个容器固然就是WebView)。那为何不全部的页面都使用这种方式开发呢?由于使用h5来展现界面的话用户体验始终是不如native的,因此在这二者之间咱们须要一个权衡。html

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

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

 

场景2,仍是前端页面有一个按钮,点击这个按钮的逻辑是:若是登陆了,则跳转到相应的界面,若是没有登陆,则跳转到登陆界面。而这个登陆界面是咱们native维护的。android

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

下面让我带你们了解一下几种常见的通讯方式吧。web

 

前言

在看这篇文章以前你要确保你有那么一点点的js知识,没错只须要一点点,能看懂最简单的代码就能够。若是你以前没接触过js的话。。也不要紧,我会把其中对应的逻辑用语言表达出来。json

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

传统的JSInterface

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

首先先看一段html代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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方法作了什么呢?

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

能够看到control.showToast,这个是什么咱们等下再说,下面看咱们java的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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>
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
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通道。

1
2
3
WebSettings webSettings = webView.getSettings();
 
webSettings.setJavaScriptEnabled( true );

而后经过WebView的addJavascriptInterface方法去注入一个咱们本身写的interface。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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。

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

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

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

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

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

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

1
2
3
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中的事件的,咱们发现其中有三个这样的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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的代码

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
<!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>
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
( 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的这段代码

1
2
3
4
5
6
7
8
9
10
<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脚本的这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
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代码,点击第一个按钮,会执行这个语句

1
<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()。

1
2
3
4
5
6
7
8
9
10
//生成URI,调用native功能
callMethod:  function  (obj, method, params, sid) {
     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代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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这个类的具体代码了。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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这个方法。它里面作了什么呢

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
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(前面说的那串数字)等等内容。下面看这段代码

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

经过name去获得一个map,这里的name是咱们刚刚解析获得了,对应实际状况就是JSBridge,那这个mInjectNameMethods又是什么呢?

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
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类。

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
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类初始化的时候就有一个默认的值了。

1
2
3
4
5
public static final String BRIDGE_NAME =  "JSBridge" ;
 
private JSBridge() {
     mClassMap.put(BRIDGE_NAME, JSLogical.class);
}

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

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
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类中

1
2
3
4
5
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为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
  * 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呢?这里咱们以另一个按钮的例子来讲。

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

js脚本传递的一个json的参数,{“data”:1},从名字能够看出是先要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
/**
  * 加一
  *
  * @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方法。

1
2
3
4
5
6
7
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方法。

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
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方法。

1
2
3
4
5
6
7
8
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里作了什么呢?

1
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代码

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

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

这里多说一句,若是你也想用UrlRouter这样的形式的话,协议的sheme最好是http这样开头的,不要本身去从新定义,由于这样能够保证前端同窗逻辑的清晰。若是你想着本身定义一个sheme叫shemeA,公司作别的app的同窗也定义一个sheme叫shemeB,加上原本就要的http,前端的同窗可能脑子都昏了。。。

下面来看看WebViewClient类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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。

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
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类中是这样调用的

1
Nav.from(context).toUri(url)

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

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
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才行。

1
2
3
4
5
6
7
8
9
10
<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>
1
2
3
4
5
6
7
8
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,从而添加本身想要添加的数据。

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

相关文章
相关标签/搜索