Android native和h5混合開發幾種常見的hybrid通信方式

前言

在看這篇文章之前你要確保你有那麼一點點的js知識,沒錯只需要一點點,能看懂最簡單的代碼就可以。如果你之前沒接觸過js的話。。也沒關係,我會把其中對應的邏輯用語言表達出來。

爲什麼需要用到js呢,因爲前端體系中,像我們說的點擊按鈕這樣的邏輯都是放在js腳本中執行的,有點像我們Android中的model層。(由於本人對前端的知識也只是略知一二,這個比方可能不太恰當,見諒見諒)。所以說到hybrid通信,主要就是前端的js和我們Android端的通信。

傳統的JSInterface

首先先介紹一下最普通的一種通信方式,就是使用Android原生的JavascriptInterface來進行js和Java的通信。具體方式如下:

首先先看一段html代碼

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

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

</head>

<body>
<input type="button" value="toast"
       onClick="showToast('Hello world')" />
</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

很簡單,一個button,點擊這個button就執行js腳本中的showToast方法。 
這裏寫圖片描述 
而這個showToast方法做了什麼呢?

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

可以看到control.showToast,這個是什麼我們等下再說,下面看我們java的代碼。

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

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

    </WebView>

</LinearLayout>
public class MainActivity extends AppCompatActivity {

    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = (WebView)findViewById(R.id.webView);

        WebSettings webSettings = webView.getSettings();

        webSettings.setJavaScriptEnabled(true);

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

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

    public class JsInterface {

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

        public void log(final String msg){
            webView.post(new Runnable() {
                @Override
                public void run() {
                    webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
                }
            });
        }
    }
}
  • 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
  • 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

首先界面很簡單,一個WebView。在對應的activity中做的事也就幾件,首先打開js通道。

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

然後通過WebView的addJavascriptInterface方法去注入一個我們自己寫的interface。

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

public class JsInterface {

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

        public void log(final String msg){
            webView.post(new Runnable() {
                @Override
                public void run() {
                    webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
                }
            });
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

可以看到這個interface我們給它取名叫control。

最後loadUrl。

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

好了,讓我們再看看js腳本中的那個showToast()方法。

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

這裏的control就是我們的那個interface,調用了interface的showToast方法

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

可以看到先顯示一個toast,然後調用log()方法,log()方法裏調用了js腳本的log()方法。

function log(msg){
    console.log(msg);
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

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

這樣我們就完成了js和java的互調,是不是很簡單。但是大家想過這樣有什麼問題嗎?如果你使用的是AndroidStudio,在你的webSettings.setJavaScriptEnabled(true);這句函數中,AndroidStudio會給你一個warning。 
這裏寫圖片描述 
這個提示的意思呢,就是如果你使用了這種方式去開啓js通道,你就要小心XSS攻擊了,具體的大家可以參考wooyun上的這篇文章。

雖然這個漏洞已經在Android 4.2上修復了,就是使用@JavascriptInterface這個註解。但是你得考慮兼容性啊,你不能保證,尤其在中國這樣碎片化嚴重的地方,每個用戶使用的都是4.2+的系統。所以基本上我們不會再利用Android系統爲我們提供的addJavascriptInterface方法或者@JavascriptInterface註解來實現js和java的通信了。那怎麼辦呢?方法都是人想出來的嘛,下面讓我們看解決方案。

JSBridge

JSBridge,顧名思義,就是和js溝通的橋樑。其實這個技術在Android中已經不算新了,相信有些同學也看到過不少實現方案,這裏說一種我的想法吧。其實說是我的想法,實際是公司裏的大牛實現的,我現在做的就是維護並且擴展,不過這裏還是拿出來和大家分享一下。

思路

首先先說思路,有經驗的同學可能都知道Android的WebView中有一個WebChromeClient類,這個類其實就是用來監聽一些WebView中的事件的,我們發現其中有三個這樣的方法。

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

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

@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
    return super.onJsConfirm(view, url, message, result);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

這三個方法其實就對應於js中的alert(警告框),comfirm(確認框)和prompt(提示框)方法,那這三個方法有什麼用呢?前面我們說了JSBridge的作用是提供一種js和java通信的框架,其實我們可以利用這三個方法去完成這樣的事。比如我們可以在js腳本中調用alert方法,這樣對應的就會走到WebChromeClient類的onJsAlert()方法中,我們就可以拿到其中的信息去解析,並且做java層的事情。那是不是這三個方法隨便選一個就可以呢?其實不是的,因爲我們知道,在js中,alert和confirm的使用概率還是很高的,特別是alert,所以我們最好不要使用這兩個通道,以免出現不必要的問題。

好了,說到這裏我們前期的準備工作也就做好了,其實就是通過重寫WebView中WebChromeClient類的onJsPrompt()方法來進行js和java的通信。

有了實現方案,下面就是一些具體的細節了,大家有沒有想過,怎麼樣才能讓java層知道js腳本需要調用的哪一個方法呢?怎麼把js腳本的參數傳遞進來呢?同步異步的方式又該怎麼實現呢?下面提供一種我的思路。

首先大家都知道http是什麼,其實我們的JSBridge也可以效仿一下http,定義一個自己的協議。比如規定sheme,path等等。下面來看一下一些的具體內容:

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

是不是和http協議有一點像,其實我們可以通過js腳本把這段協議文本傳遞到onPropmt()方法中並且進行解析。比如,sheme是hyrid://開頭的就表示是一個hybrid方法,需要進行解析。後面的method表示方法名,message表示傳遞的參數等等。

有了這樣一套協議,我們就可以去進行我們的通信了。

代碼

先看一下我們html和js的代碼

<!DOCTYPE HTML>

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

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

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

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

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

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

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

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

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

            sid = Private.getSid();

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

        },

        //native代碼處理 成功/失敗 後,調用該方法來通知js
        onComplete: function (sid, data) {
            Private.onComplete(sid, data);
        }
    };

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

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

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

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

            return rst;
        },

        //根據sid註冊calls的回調函數
        registerCall: function (sid, callback) {
            if (callback) {
                this.calls[CALLBACK_PREFIX + sid] = callback;
            }
        },

        //根據sid刪除calls對應的回調函數,並返回call對象
        unregisterCall: function (sid) {
            var callbackId = CALLBACK_PREFIX + sid;
            var call = {};

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

            return call;
        },

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

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

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

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

            data = this.parseData(data);

            callback && callback(data);
        }
    };

    for (var key in Core) {
        if (!hasOwnProperty.call(JsBridge, key)) {
            JsBridge[key] = Core[key];
        }
    }
})(window);
  • 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
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 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
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149

有前端經驗的同學應該能很輕鬆的看懂這樣的代碼,對於看不懂的同學我來解釋一下,首先看界面。 
這裏寫圖片描述 
可以看到有兩個按鈕,對應着html的這段代碼

<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))});">toast</button>
</li>

<br/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

點擊按鈕會執行js腳本的這段代碼

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

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

    sid = Private.getSid();

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

它其實就是一個函數,名字叫call,括號裏的是它的參數(obj, method, params, callback, timeout)。那這幾個參數是怎麼傳遞的呢?回過頭看我們的html代碼,點擊第一個按鈕,會執行這個語句

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

其中括號(‘JSBridge’,’toast’,{‘message’:’我是氣泡’,’isShowLong’:0},function(res){})裏的第一個參數’JSBridge’對應着前面的obj,’toast’對應着method,以此類推。第二個按鈕也是一樣。

然後在call這個方法內,會執行Private類的registerCall和callMethod,我們來看callMehod()。

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

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

    var value = CB_PROTOCOL + ':';
    window.prompt(uri, value);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

註釋說的很清楚了,就是通過傳遞進來的參數生成uri,並且調用window.prompt()方法,這個方法大家應該很眼熟吧,沒錯,在調用這個方法之後,程序就會相應的走到java代碼的onJsPrompt()方法中。而生成的uri則是我們上面說過的那個我們自己定義的協議格式。

好了,我們總結一下這兩個前端的代碼。其實很簡單,以界面的第一個按鈕toast爲例,點擊這個按鈕,它會執行相應的js腳本代碼,然後就會像我們前面所講的那樣,走到onJsPrompt()方法中,下面讓我們看看對應的java代碼。

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

    private JsCallJava mJsCallJava;

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

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        result.confirm(mJsCallJava.call(view, message));
        return true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

這是對應的WebChromeClient類,可以看到在onJsPrompt()方法中我們只做了一件事,就是丟給JsCallJava類去解析,再看JsCallJava類之前,我們可以先看看onJsPrompt()這個方法到底傳進來了什麼。 
這裏寫圖片描述 
可以看到,我們傳給JsCallJava類的那個message,就像我們前面定義的協議一樣。sheme是hybrid://,表示這是一個hybrid方法,host是JSBridge,方法名字是toast,傳遞的參數是以json格式傳遞的,具體內容如圖。不知道大家有沒有發現,這裏我有一個東西沒有講,就是JSBridge:後面的那串數字,這串數字是幹什麼用的呢?大家應該知道,現在我們整個調用過程都是同步的,這意味着我們沒有辦法在裏面做一些異步的操作,爲了滿足異步的需求,我們就需要定義這樣的port,有了這串數字,我們在java層就可以做異步的操作,等操作完成以後回調給js腳本,js腳本就通過這串數字去得到對應的callback,有點像startActivity中的那個requestCode。大家沒聽懂也沒關係,後面我會在代碼中具體講解。

好了,下面我們可以來看JsCallJava這個類的具體代碼了。

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

    private static final String BRIDGE_NAME = "JSBridge";

    private static final String SCHEME="hybrid";

    private static final int RESULT_SUCCESS=200;
    private static final int RESULT_FAIL=500;


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

    private JSBridge mWDJSBridge = JSBridge.getInstance();

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

    private ArrayMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
        ArrayMap<String, Method> mMethodsMap = new ArrayMap<>();
        //獲取自身聲明的所有方法(包括public private protected), getMethods會獲得所有繼承與非繼承的方法
        Method[] methods = injectedCls.getDeclaredMethods();
        for (Method method : methods) {
            String name;
            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
                continue;
            }
           Class[] parameters=method.getParameterTypes();
           if(null!=parameters && parameters.length==3){
               if(parameters[0]==WebView.class && parameters[1]==JSONObject.class && parameters[2]==JsCallback.class){
                   mMethodsMap.put(name, method);
               }
           }
        }
        return mMethodsMap;
    }


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

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

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

        return result;
    }



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

    private String getReturn(String reqJson, int stateCode, Object result) {
        String insertRes;
        if (result == null) {
            insertRes = "null";
        } else if (result instanceof String) {
            //result = ((String) result).replace("\"", "\\\"");
            insertRes = String.valueOf(result);
        } else if (!(result instanceof Integer)
                && !(result instanceof Long)
                && !(result instanceof Boolean)
                && !(result instanceof Float)
                && !(result instanceof Double)
                && !(result instanceof JSONObject)) {    // 非數字或者非字符串的構造對象類型都要序列化後再拼接
            insertRes = result.toString();//mGson.toJson(result);
        } else {  //數字直接轉化
            insertRes = String.valueOf(result);
        }
        //String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes);
        Log.d(TAG, " call json: " + reqJson + " result:" + insertRes);
        return insertRes;
    }
}
  • 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
  • 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

有點長,不過其實邏輯很好理解。首先我們調用的是call這個方法。它裏面做了什麼呢

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

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

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

    return result;
}
  • 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
  • 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

可以看到其實就是通過js腳本傳遞過來的參數得到了方法名字,sid(前面說的那串數字)等等內容。下面看這段代碼

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

通過name去得到一個map,這裏的name是我們剛剛解析得到了,對應實際情況就是JSBridge,那這個mInjectNameMethods又是什麼呢?

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

private JSBridge mJSBridge = JSBridge.getInstance();

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

private ArrayMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
    ArrayMap<String, Method> mMethodsMap = new ArrayMap<>();
    //獲取自身聲明的所有方法(包括public private protected), getMethods會獲得所有繼承與非繼承的方法
    Method[] methods = injectedCls.getDeclaredMethods();
    for (Method method : methods) {
        String name;
        if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
            continue;
        }
       Class[] parameters=method.getParameterTypes();
       if(null!=parameters && parameters.length==3){
           if(parameters[0]==WebView.class && parameters[1]==JSONObject.class && parameters[2]==JsCallback.class){
               mMethodsMap.put(name, method);
           }
       }
    }
    return mMethodsMap;
}
  • 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
  • 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

可以看到我們有一個JSBridge類,在JsCallJava的構造函數中,我們通過JSBridge這個類的getInjectPair()方法得到了一個String和class的映射關係,並且把class中符合標準的方法拿出來存放到mInjectNameMethods中,以便我們在call方法中調用。下面來看看JSBridge類。

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

    private static JSBridge INSTANCE = new JSBridge();

    private boolean isEnable=true;

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

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

    public static JSBridge getInstance() {
        return INSTANCE;
    }

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

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

    }


    public ArrayMap<String, Class<? extends IInject>> getInjectPair() {
        return mClassMap;
    }
}
  • 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
  • 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

它的getInjectPair方法其實就是得到了mClassMap,這個map在JSBridge類初始化的時候就有一個默認的值了。

public static final String BRIDGE_NAME = "JSBridge";

private JSBridge() {
    mClassMap.put(BRIDGE_NAME, JSLogical.class);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

key是”JSBridge”,value是我們的JSLogincal類。

public class JSLogical implements IInject {

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

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

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

    public static void invokeJSCallback(JsCallback callback, boolean isSuccess, String message, JSONObject objects) {
        try {
            callback.apply(isSuccess, message, objects);
        } catch (JsCallback.JsCallbackException e) {
            e.printStackTrace();
        }
    }
  • 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
  • 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

對這個類上面的兩個方法有沒有很眼熟?名字和js腳本中的那兩個方法一樣有木有。我們調用鏈最後就會走到相應的同名方法中!

上面就是js調js的整個過程了,其實吧,不應該放這麼多的代碼的,搞得像是源碼分析一樣,不過我覺得這樣還是有一定好處的,至少跟着代碼走一遍能加深印象嘛。

我們還是來捋一捋整個過程。

在js腳本中把對應的方法名,參數等寫成一個符合協議的uri,並且通過window.prompt方法發送給java層。 
在java層的onJsPrompt方法中接受到對應的message之後,通過JsCallJava類進行具體的解析。 
在JsCallJava類中,我們解析得到對應的方法名,參數等信息,並且在map中查找出對應的類的方法。 
這裏多說一句,還記得我們定義的協議中的host是什麼嗎?

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

是JSBridge,而我們在JsCallJava類中是通過這個host去查找對應的類的,我們可以看到在JSBridge類中

public static final String BRIDGE_NAME = "JSBridge";

private JSBridge() {
    mClassMap.put(BRIDGE_NAME, JSLogical.class);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

這意味着,如果你可以更換你的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裏處處都是規範,協議需要規範,參數需要規範。這些其實都是合理的,因爲規範所以安全。

在得到對應的方法之後,就去調用它,以我們的toast爲

/**
 * toast
 *
 * @param webView 瀏覽器
 * @param param   提示信息
 */
public static void toast(WebView webView, JSONObject param, final JsCallback callback) {
    String message = param.optString("message");
    int isShowLong = param.optInt("isShowLong");
    Toast.makeText(webView.getContext(), message, isShowLong == 0 ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
    if (null != callback) {
        try {
            JSONObject object = new JSONObject();
            object.put("result", true);
            invokeJSCallback(callback, object);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

拿到對應的信息,直接makeToast就好了。

以上就是全部js調用java的過程,那我們java執行完邏輯以後,怎麼回調js呢?這裏我們以另外一個按鈕的例子來說。

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

js腳本傳遞的一個json的參數,{“data”:1},從名字可以看出是先要java執行一個加邏輯。

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

這裏我們模擬一下耗時操作,可以幫助大家更好的理解JSBridge中的異步操作。對應java層的方法執行完+1的操作之後,把結果封裝成一個jsonObject,並且調用invokeJSCallback方法。

public static void invokeJSCallback(JsCallback callback, boolean isSuccess, String message, JSONObject objects) {
    try {
        callback.apply(isSuccess, message, objects);
    } catch (JsCallback.JsCallbackException e) {
        e.printStackTrace();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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

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

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

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

    }
}
  • 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
  • 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

在apply方法中,我們直接拼裝了一個jsonObject,裏面包括了我們想要返回給js腳本的結果,並且直接調用了js的onComplete方法。

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

    data = this.parseData(data);

    callback && callback(data);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

可以看到js的onComplete通過sid(那一串數字)拿到對應的callback並執行,而我們plus的callback裏做了什麼呢?

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

直接在控制檯中輸出結果。

所以當我們點擊plug按鈕以後,過兩秒我們就可以在logcat中看到如下輸出 
這裏寫圖片描述 
好了,至此所有和JSBridge相關的代碼就分析完了。其實原理非常的簡單,通過js的window.prompt方法將事先定義好的協議文本傳輸到java層,然後java層進行解析並調用相應的方法,最後通過callback將結果返回給js腳本。中間我們使用的那些類可以更好的解耦,如果你有心,甚至可以把所用邏輯相關代碼抽離出來,把剩餘的代碼寫成JSBridge.core作爲庫來使用。這樣你想加什麼功能直接寫,不用改任何的源碼。

UrlRouter

其實嚴格的說,UrlRouter不算是js和java的通信,它只是一個通過url來讓前端喚起native頁面的框架。不過千萬不要小看它的作用,如果協議定義的合理,它可以讓前端,Android和iOS三端有一個高度的統一,十分方便。

思路

其實吧,這個思路比JSBridge還要簡單,就是我們通過自己實現的框架去攔截前端同學寫的url,發現如果是符合我們UrlRouter的協議的話,就跳轉到相應的頁面。

至於怎麼攔截呢?當然是通過WebViewClient類的shouldOverrideUrlLoading方法咯。

代碼

首先我們還是先看一個html代碼

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

很簡單,有一個按鈕,通過點擊這個按鈕,會加載一個url,這個url是http://login.h5.zjutkz.net/

這裏多說一句,如果你也想用UrlRouter這樣的形式的話,協議的sheme最好是http這樣開頭的,不要自己去重新定義,因爲這樣可以保證前端同學邏輯的清晰。如果你想着自己定義一個sheme叫shemeA,公司做別的app的同學也定義一個sheme叫shemeB,加上本來就要的http,前端的同學可能腦子都昏了。。。

下面來看看WebViewClient類。

public class NavWebViewClient extends WebViewClient {

    private Context context;

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

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

        view.loadUrl(url);
        return true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

很簡單,在shouldOverrideUrlLoading方法中先攔截url交給Nav類處理,如果返回true則表示需要攔截,直接return true,否則交給WebView去loadUrl。

接下去看看Nav。

public class Nav {

    private static final String TAG = "Nav";

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

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

    public boolean toUri(final Uri uri) {

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

        final Intent intent = to(uri);

        for (;;) try {

            intent.setPackage(mContext.getPackageName());

            PackageManager pm = mContext.getPackageManager();

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

            mContext.startActivity(intent);
            return true;

        } catch (final ActivityNotFoundException e) {

            return false;
        }
    }

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

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

        return mIntent;
    }

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

    private final Context mContext;
    private final Intent mIntent;
}
  • 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
  • 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

我們在NavWebViewClient類中是這樣調用的

Nav.from(context).toUri(url)

from方法創建了一個Nav類的實例,下面來看看toUri方法

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

public boolean toUri(final Uri uri) {

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

    final Intent intent = to(uri);

    for (;;) try {

        intent.setPackage(mContext.getPackageName());

        PackageManager pm = mContext.getPackageManager();

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

        mContext.startActivity(intent);
        return true;

    } catch (final ActivityNotFoundException e) {

        return false;
    }
}

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

    return mIntent;
}
  • 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
  • 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

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

接着通過一系列PackageManager的方法去判斷有沒有符合uri的activity,如果有則直接startActivity。

是不是很簡單,下面我以文中最開頭的場景2爲例子。

我們native端需要一個LoginActivity,並且根據上面的代碼我們知道,這個LoginActivity必須要配置上對應的data才行。

<activity android:name=".activity.LoginActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <category android:name="${NAV_CATEGORY}"/>
        <data android:scheme="${NAV_SCHEMA}"/>
        <data android:host="${NAV_HOST}"/>
    </intent-filter>
</activity>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
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"]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這是我們的manifest文件,可以看到已經通過gradle配置了對應的data。

這裏我爲什麼要用grdle去配置呢?想象如果你有十幾個頁面,你難道要在manifest中都寫一遍嗎?用我這種方式,直接在build.gradle中寫一遍就可以了。這裏我是想給大家傳遞一個思想:

使用gradle我們可以做很多自動化的事,千萬不要自己給自己找麻煩了。

看到這兒大家肯定會覺得,就這麼簡單?是的,大體的框架就這麼簡單,但是如果你想真正的用好它,還需要做很多工作。

比如在跳轉到native頁面,做完響應的邏輯之後,你怎麼通知前端去更新呢?這裏你可以使用startActivityForResult,也可以使用廣播,甚至是eventBus。這需要你在你的框架內做好封裝。

再比如,上面的例子是最簡單的,但是如果前端的同學想在跳到對應的native頁面的時候加上一些參數呢?你的intent該怎麼處理?

還有,如果你想你的框架魯棒性夠強,是不是得提供一個hook工具呢?讓調用者可以hook掉你內部的那個intent,從而添加自己想要添加的數據。

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

轉載:http://zjutkz.net/2016/04/17/好好和h5溝通!幾種常見的hybrid通信方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章