幾種常見的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,從而添加本身想要添加的數據。

這些都是要解決的問題,這裏我就不給你們上具體的代碼了。畢竟只有你本身去實現了之後纔會有更深的理解。

相關文章
相關標籤/搜索