幾種常見的hybrid通信方式


原文出處zjutkz's blog。 

說起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代碼

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,從而添加自己想要添加的數據。

這些都是要解決的問題,這里我就不給大家上具體的代碼了。畢竟只有你自己去實現了以后才會有更深的理解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM