1. 程式人生 > >Android與JS之JsBridge使用與原始碼分析

Android與JS之JsBridge使用與原始碼分析

在Android開發中,由於Native開發的成本較高,H5頁面的開發更靈活,修改成本更低,因此前端網頁JavaScript(下面簡稱JS)與Java之間的互相呼叫越來越常見。

JsBridge就是一個簡化Android與JS通訊的框架,原始碼:https://github.com/lzyzsd/JsBridge
我們今天通過一個簡單栗子來分析下開源框架JsBridge的原始碼。栗子的程式碼我也放在Github,有需要的可以seesee:
https://github.com/juexingzhe/Android_JS
栗子很簡單,隨便輸入資訊登陸,會載入一個H5頁面,在H5介面點選按鈕,Java執行getUserInfo()然後將UserInfo回傳給JS,H5頁面再顯示UserInfo。

 

JS呼叫Android基本有下面三種方式

webView.addJavascriptInterface()
WebViewClient.shouldOverrideUrlLoading()
WebChromeClient.onJsAlert()/onJsConfirm()/onJsPrompt() 方法分別回撥攔截JS對話方塊alert()、confirm()、prompt()訊息

Android呼叫JS

webView.loadUrl();
webView.evaluateJavascript()

常用方法的使用後面栗子中會用到,更細節的介紹各位同學可以去網上搜搜看看。

1.JsBridge使用

我們先來看下Java層的程式碼
首先引入依賴和倉庫

dependencies {
   ……
    compile 'com.github.lzyzsd:jsbridge:1.0.4'
    compile 'com.google.code.gson:gson:2.7'
}
repositories {
    jcenter()
    maven { url "https://jitpack.io" }
}

準備工作就是這樣,下面可以開始擼程式碼,首先就是點選按鈕登陸,這個簡單:

Intent intent = new Intent(LoginActivity.this, WebActivity.class);
intent.putExtra("email", mEmailView.getText().toString());
startActivity(intent);

佈局檔案中要使用BridgeWebView:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.github.lzyzsd.jsbridge.BridgeWebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

在跳轉後的頁面,獲取登陸資訊並存儲,再通過loadUrl載入H5頁面:

Intent intent = this.getIntent();
String email = intent.getStringExtra("email");

 mUserInfo = new UserInfo(email);

mBridgeWebView = (BridgeWebView) findViewById(R.id.web_view);
mBridgeWebView.setDefaultHandler(new DefaultHandler());
mBridgeWebView.loadUrl("file:///android_asset/getuserinfo.html");

registerHandler();

主要是要註冊Handler,供JS呼叫,

getUserInfo就是註冊供JS呼叫的Handler的id
data是JS傳過來的引數
CallBackFunction 函式中需要把JS需要的response返回給JS

private void registerHandler() {
        mBridgeWebView.registerHandler("getUserInfo", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                Log.i(TAG, "handler = getUserInfo, data from web = " + data);
                function.onCallBack(new Gson().toJson(mUserInfo));
            }
        });
}

Java層的程式碼就這麼簡單,下面看下JS層工作:
首先需要一個js檔案,我們寫一個getuserinfo.html檔案放在assets目錄下,檔案內容,不建議把js程式碼直接放在html檔案中,我為了方便直接就寫在這了。程式碼放了兩個段落,一個類似於TextView用來顯示使用者資訊,一個Button。點選按鈕會呼叫callHandler,三個引數和Java層一一對應,在Java層返回的時候,會呼叫function(responseData)函式,顯示用於資訊。

<html>
<head>
    <meta content="text/html; charset=utf-8" http-equiv="content-type">
    <title>
        js呼叫java
    </title>
</head>
<body>
<p>
    <xmp id="show">
    </xmp>
</p>
<div align="center">
    <p>
        <input type="button" id="enter" value="獲取使用者資訊" onclick="getUserInfo();"
        />
    </p>
</div>
</body>
<script>
    function getUserInfo(){
        window.WebViewJavascriptBridge.callHandler(
            'getUserInfo',
            {'info': 'I am JS, want to get UserInfo from Java'},
            function(responseData) {
                document.getElementById("show").innerHTML = "repsonseData from java,\ndata = " + responseData;
            }
        )
    }
</script>
</html>

使用基本就是這樣了,可以看出來JsBridge通過封裝,JS和Java之間的通訊只需要實現兩個步驟,使用起來很方便。

我們來看下原始碼是怎麼個玩法,先來個華麗麗的分割線



2.JsBridge原始碼分析

分析之前我把JS呼叫Java畫了個簡易互動圖,Java呼叫JS的過程類似

是不是感覺反而更復雜了???其實只要捉住主要的三點,JsBridge就原形畢露:

1.Android呼叫JS是通過loadUrl(url),url中可以拼接要傳給JS的物件
2.JS呼叫Android是通過shouldOverrideUrlLoading
3.JsBridge將溝通資料封裝成Message,然後放進Queue,再將Queue進行傳輸

接下來我們來一步一步跟蹤上面栗子的呼叫過程:

  • JS層點選按鈕呼叫callHandler

handlerName,Java和JS要一致,
data是Java層handlerName函式執行的引數
responseCallback是Java執行完handlerName返回時,JS回撥的介面,是JS執行

onclick="getUserInfo();"

function getUserInfo(){
    window.WebViewJavascriptBridge.callHandler(
            'getUserInfo',
            {'info': 'I am JS, want to get UserInfo from Java'},
            function(responseData) {
                document.getElementById("show").innerHTML = "repsonseData from java,\ndata = " + responseData;
            }
    )
}

callHandler會呼叫_doSend

如果JS需要回調,就將回調的callbackId放進message中,Java執行完會傳回callbackId,這裡是cb_1_1495182409011
構造完message放進佇列sendMessageQueue
通過iframe屬性給Java傳送通知訊息,訊息結構yy://QUEUE_MESSAGE/

function callHandler(handlerName, data, responseCallback) {
    _doSend({
        handlerName: handlerName,
        data: data
    }, responseCallback);
}

function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message.callbackId = callbackId;
        }

        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
  • Java收到通知訊息
    WebView在shouldOverrideUrlLoading攔截到url:yy://QUEUE_MESSAGE/
    然後會執行webView.flushMessageQueue(),在主執行緒執行loadUrl通知JS層推送佇列到Java;

JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();"
呼叫JS層的_fetchQueue,通知JS層傳送佇列到Java層
在responseCallbacks中註冊回撥介面,介面id是函式名_fetchQueue,在JS推送訊息佇列時進行回撥

void flushMessageQueue() {
          if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
               loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
                    @Override
                    public void onCallBack(String data) {
                         //
                    });
          }
}

public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
          this.loadUrl(jsUrl);
          responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
  • JS 傳送Request Queue
    執行_fetchQueue

將sendMessageQueue轉化成JSON
通過iframe屬性給Java傳送通知訊息,訊息結構:yy://return/_fetchQueue/訊息佇列的內容

function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    //android can't read directly the return data, so we can reload iframe src to communicate with java
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
  • Java收到呼叫通知,進行處理併發送Response Queue到JS
    WebView在shouldOverrideUrlLoading會攔截到url:
yy://return/_fetchQueue/[{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]

執行webView.handlerReturnData(url);

根據函式名_fetchQueue拿到之前註冊的回撥函式CallBackFunction returnCallback
執行回撥函式,並且從註冊中移除

void handlerReturnData(String url) {
          String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
          CallBackFunction f = responseCallbacks.get(functionName);
          String data = BridgeUtil.getDataFromReturnUrl(url);
          if (f != null) {
               f.onCallBack(data);
               responseCallbacks.remove(functionName);
               return;
          }
}

接下來就是對Request Queue的解析然後找到JS希望呼叫Handler並且執行,程式碼中我寫了註釋,可以直接看:

//回撥介面執行onCallBack函式
//其中data [{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]
void flushMessageQueue() {
          if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
               loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

                    @Override
                    public void onCallBack(String data) {
                         // deserializeMessage
                         List<Message> list = null;
                         try {
                              //將JSON陣列轉化成Java list
                              list = Message.toArrayList(data);
                         } catch (Exception e) {
                        e.printStackTrace();
                              return;
                         }
                         if (list == null || list.size() == 0) {
                              return;
                         }
                         for (int i = 0; i < list.size(); i++) {
                              //從list中取出Message
                              Message m = list.get(i);
                              //在我們的栗子中沒有responseId,因此到else分支
                              String responseId = m.getResponseId();
                              // 是否是response
                              if (!TextUtils.isEmpty(responseId)) {
                                   CallBackFunction function = responseCallbacks.get(responseId);
                                   String responseData = m.getResponseData();
                                   function.onCallBack(responseData);
                                   responseCallbacks.remove(responseId);
                              } else {
                                   CallBackFunction responseFunction = null;
                                   // if had callbackId
                                   //如果有callbackId就說明JS需要回調,因此Java層需要構造responseMsg
                                   //從message中取出callbackId,放進responseMsg
                                   final String callbackId = m.getCallbackId();
                                   if (!TextUtils.isEmpty(callbackId)) {
                                        responseFunction = new CallBackFunction() {
                                             @Override
                                             public void onCallBack(String data) {
                                                  Message responseMsg = new Message();
                                                  responseMsg.setResponseId(callbackId);
                                                  responseMsg.setResponseData(data);
                                                  queueMessage(responseMsg);
                                             }
                                        };
                                   } else {
                                        responseFunction = new CallBackFunction() {
                                             @Override
                                             public void onCallBack(String data) {
                                                  // do nothing
                                             }
                                        };
                                   }
                                   BridgeHandler handler;
                                   //從message中取出Handler名字,再從messageHandlers中取
                                   //如果沒有就使用預設的Handler
                                   if (!TextUtils.isEmpty(m.getHandlerName())) {
                                        handler = messageHandlers.get(m.getHandlerName());
                                   } else {
                                        handler = defaultHandler;
                                   }
                                   if (handler != null){
                                        //執行handler
                                        handler.handler(m.getData(), responseFunction);
                                   }
                              }
                         }
                    }
               });
          }
}

那麼這個handler是什麼?就是Java呼叫registerHandler註冊的getUserInfo

private void registerHandler() {
        mBridgeWebView.registerHandler("getUserInfo", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                Log.i(TAG, "handler = getUserInfo, data from web = " + data);
                function.onCallBack(new Gson().toJson(mUserInfo));
            }
}

執行webView.handlerReturnData(url);

上面的function就是在flushMessageQueue 解析時構造的responseFunction,在message中包括JS層需要回調的函式Id,然後就是getUserInfo執行的結果
呼叫queueMessage

responseFunction = new CallBackFunction() {
          @Override
          public void onCallBack(String data) {
                    Message responseMsg = new Message();
                    responseMsg.setResponseId(callbackId);
                    responseMsg.setResponseData(data);
                    queueMessage(responseMsg);
          }
};

queueMessage呼叫dispatchMessage傳送message給JS

通過構造String指令,然後loadUrl執行JS程式碼,注意物件也是通過這樣方式傳遞過去的,就類似呼叫本地函式,不發起網路請求

void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
}

其中

BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA ="javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');"
javascriptCommand = javascript:WebViewJavascriptBridge._handleMessageFromNative('{\"responseData\":\"{\\\"email\\\":\\\"[email protected]\\\"}\",\"responseId\":\"cb_1_1495182558893\"}');
//data = {\"responseData\":\"{\\\"email\\\":\\\"[email protected]\\\"}\",\"responseId\":\"cb_1_1495182409011\"}
  • JS收到Response JSON
    來到_handleMessageFromNative,
function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue && receiveMessageQueue.length > 0) {
            receiveMessageQueue.push(messageJSON);
        } else {
            _dispatchMessageFromNative(messageJSON);
        }
}

最後都會呼叫到_dispatchMessageFromNative,由於是JS主動呼叫Java,因此有responseId,執行registerHandler時傳入的CallBack,也就是顯示使用者資訊。我在程式碼里加了註釋,很容易看懂。

function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
             //將資料解析成JSON
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            //根據responseId:cb_1_1495182409011拿到responseCallback,就是我們前門註冊的alert
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                //直接傳送
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }

                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName];
                }
                //查詢指定handler
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
}

原始碼的分析就到這結束了,程式碼不多,但是封裝的介面很是好用。最後再來個分割線~~



3.總結

最後總結下,使用上很方便主要兩個步驟

被呼叫方註冊Handler

registerHandler(String handlerName, BridgeHandler handler) 

呼叫方呼叫Handler

callHandler(String handlerName, String data, CallBackFunction callBack)

原理上還是那三句話,請原諒我從上面直接copy過來:

1.Android呼叫JS是通過loadUrl(url),url中可以拼接要傳給JS的物件
2.JS呼叫Android是通過shouldOverrideUrlLoading
3.JsBridge將溝通資料封裝成Message,然後放進Queue,再將Queue進行傳輸

好了,今天我們JsBridge的使用和原始碼分析就到這了,謝謝!

文中栗子的連結:
https://github.com/juexingzhe/Android_JS

原文地址