【Android】如何寫一個JsBridge


JsBridge

簡介

Android JsBridge 就是用來在 Android app的原生 java 代碼與 javascript 代碼中架設通信(調用)橋梁的輔助工具。

原文地址點這里

github點這里

使用方式戳這里

有問題請聯系 xesam

原理概述

Javascript 運行在 WebView 中,而 WebView 只是 Javascript 執行引擎與頁面渲染引擎的一個包裝而已。

由於這種天然的隔離效應,我們可以將這種情況與 IPC 進行類比,將 Java 與 Javascript 的每次互調都看做一次 IPC 調用。
如此一來,我們可以模仿各種已有的 IPC 方式來進行設計,比如 RPC。本文模仿 Android 的 Binder 機制來實現一個 JsBridge。

首先回顧一一下基於 Binder 的經典 RPC 調用:

Javascript-bridge-rpc

當然,client 與 server 只是用來區分通信雙方責任的叫法而已,並不是一成不變的。
對於 java 與 javascript 互調的情況,當 java 主動調用 javascript 的時候,java 充當 client 角色,javascript 則扮演 server 的角色,
javascript 中的函數執行完畢后回調 java 方法,這個時候,javascript 充當 client 角色,而 javascript 則承擔 server 的責任。

Javascript-bridge-circle

剩下的問題就是怎么來實現這個機制了,大致有這么幾個需要解決的問題:

  1. java 如何調用 Javascript
  2. Javascript 如何調用 java
  3. 方法參數以及回調如何處理
  4. 通信的數據格式是怎樣的

下面逐個討論這些問題:

1. java 如何調用 Javascript

要實現 Java 與 Javascript 的相互調用,有兩條途徑可以考慮:

  1. 集成一個定制化的 Javascript 與 Html 渲染引擎,java 通過引擎底層與 Javascript 交互。這樣可以獲得完全的控制權。
  2. 使用 Android Sdk 提供的交互方法。

對於第一種途徑,代價比較大,而且技術方案比較復雜,一般只有基於 Javascript 的跨平台開發方案才會這么做。
所以,現在着重考查第二種途徑。

Android 的默認 Sdk 中, Java 與 Javascript 的一切交互都是依托於 WebView 的,大致有以下幾個可用方法:

第一:

    webView.loadUrl("javascript:scriptString"); //其中 scriptString 為 Javascript 代碼

第二,在 KITKAT 之后,又新增了一個方法:

    webView.evaluateJavascript(scriptString, new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
    
        }
    });//其中 scriptString 為 Javascript 代碼,ValueCallback 的用來獲取 Javascript 的執行結果。這是一個異步掉用。

這個調用看起比上面的正常,而且更像是一個方法調用。

需要注意的是,ValueCallback 並不是在 UI 線程里面執行的。

2. Javascript 如何調用 java

要實現 Javascript 調用 java 方法,需要先在 Javascript 環境中注入一個 Java 代理:


    class JavaProxy{
        @JavascriptInterface //注意這里的注解。出於安全的考慮,4.2 之后強制要求,不然無法從 Javascript 中發起調用
        public void javaFn(){
            //xxxxxx
        };
    }

    webView.addJavascriptInterface(new JavaProxy();, "java_proxy");

然后在 Javascript 環境中直接調用 obj_proxy 代理上的方法即可。


    java_proxy.javaFn();

這里有兩個方面需要統一:

  1. Javascript 的執行方法比較怪異,所以,我們需要將概念統一化。
  2. 如果需要執行的方法比較多,那么,代理對象上也需要定義非常多的方法,我們需要將各種方法定義統一起來管理。

所以,我們先將 Javascript 的執行包裝成類似 java 一樣的代理對象,然后通過在各自的 stub 上注冊回調來增加功能支持。
比如,如果 java 想增加 getPackageName 方法,那么,直接在 JavaProxy 上注冊即可:


    javaProxy.register("getPackageName", new JavaHandler(){
        @Override
        public void handle(Object value){
            //xxxxx
        }
    })

如圖:

Javascript-bridge-register

3. 方法參數以及回調如何處理

很顯然,任何 IPC 通信都涉及到參數序列化的問題, 同理 java 與 Javascript 之間只能傳遞基礎類型(注意,不單純是基本類型),包括基本類型與字符串,不包括其他對象或者函數。
由於只涉及到簡單的相互調用,這里就可以考慮采用 JSON 格式來傳遞各種數據,輕量而簡潔。

Java 調用 Javascript 沒有返回值(這里指 loadUrl 形式的調用),因此如果 java 端想從 Javascript 中獲取返回值,只能使用回調的形式。
但是在執行完畢之后如何找到正確的回調方法信息,這是一個重要的問題。比如有下面的例子:

在 java 環境中,JavaProxy 對象有一個無參數的 getPackageName 方法用來獲取當前應用的 PackageName。
獲取到 packageName 之后,傳遞給 Javascript 調用者的對應回調中。

在 Javascript 環境中,獲取當前應用的 PackageName 的大致調用如下:

    bridge.invoke('getPackageName', null, function(packageName){
        console.log(packageName);
    });

顯然

    function(packageName){
        console.log(packageName);
    }
       

這個 Javascript 函數是無法傳遞到 java 環境中的,所以,可以采取的一個策略就是,
在 Javascript 環境中將所有回調統一管理起來,而只是將回調的 id 傳遞到 java 環境去,java 方法執行完畢之后,
將回調參數以及對應的回調 id 返回給 Javascript 環境,由 Javascript 來負責執行正確的回調。

這樣,我們就可以實現一個簡單的回調機制:

在 java 環境中


    class JavaProxy{
        public void onTransact(String jsonInvoke, String jsonParam){
            json = new Json(jsonInvoke);
            invokeName = json.getInvokeName(); // getPackageName
            callbackId = json.getCallbackId(); // 12345678xx
            invokeParam = new Param(jsonParam);// null
            
            ...
            ...
            
            JsProxy.invoke(callbackId, callbackParam); //發起 Javascript 調用,讓 Javascript 去執行對應的回調
        }
    }

在 javascript 環境中

    
    bridge.invoke = function(name, param, callback){
        var callbackId = new Date().getTime();
        _callbacks[callbackId] = callback;
        var invoke = {
            "invokeName" : name,
            "callbackId" : callbackId
        };
        JavaProxy.onTransact(JSON.stringify(invoke), JSON.stringify(param));
    }
    
     bridge.invoke('getPackageName', null, function(packageName){
         console.log(packageName);
     });  

反之亦然。

4. 通信的數據格式是怎樣的

問題都處理了,只需要設計對應的協議即可。
按照上面的討論,

在 client 端,我們使用:

    Proxy.transact(invoke, callback);

來調用 server 端注冊的方法。

在 server 端,我們使用:

    Stub.register(name, handler);

來注冊新功能,使用

    Stub.onTransact(invoke, handler);

來處理接收到的 client 端調用。

其中,invoke 包含所要執行的方法以及回調的信息,因此,invoke 的設計如下:

{
    _invoke_id : 1234,  
    _invoke_name : "xxx",  
    _callback_id : 5678,  
    _callback_name : "xxx"  
}

注意 _invoke_id 與 _invoke_name 的區別:

如果當前 invoke 是一個直接方法調用,那么 _invoke_id 應該是無效的。
如果當前 invoke 是一個回調,那么 _invoke_id + _invoke_name 共同決定回調的具體對象

需要注意的問題

1. 回調函數需要及時刪除,不然會引起內存泄漏。

由於我們使用一 Hash 來保存各自環境中的回調函數。如果某個回調由於某種原因沒有被觸發,那么,這個引用的對象就永遠不會被回收。
針對這種問題,處理方案如下:

在 Java 環境中:

如果 WebView 被銷毀了,應該手動移除所有的回調,然后禁用 javascript 。
另外,一個 WebView 可能加載多個 Html 頁面,如果頁面的 URL 發生了改變,這個時候也應該清理所有的回調,因為 Html 頁面是無狀態的,也不會傳遞相互數據。
這里有一點需要注意的是,如果 javascript 端是一個單頁面應用,應該忽略 url 中 fragment (也就是 # 后面的部分) 的變化,因為並沒有發生傳統意義上的頁面跳轉,
所有單應用的 Page 之間是可能有交互的。

在 javascript 環境中:

javascript 端情況好很多,因為 WebView 會自己管理每個頁面的資源回收問題。

使用

必要配置

請在對應的 html 頁面中引入

    <script src="js-bridge.js"></script>

Java 環境

初始化 JsBridge:

    jsBridge = new JsBridge(vWebView);

加入 url 監控:

    vWebView.setWebViewClient(new WebViewClient() {

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            Log.e("onPageFinished", url);
            jsBridge.monitor(url);
        }
    });

Java 注冊處理方法:

    jsBridge.register(new SimpleServerHandler("showPackageName") {
        @Override
        public void handle(String param, ServerCallback serverCallback) {
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    String packageName = getPackageName();
                    Tip.showTip(getApplicationContext(), "showPackageName:" + packageName);
                }
            });
        }
    });

Java 在處理方法中回調 Javascript:


    @Override
    public void handle(final String param, final ServerCallback serverCallback) {
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                User user = getUser();
                Map<String, String> map = new Gson().fromJson(param, Map.class);
                String prefix = map.get("name_prefix");
                Tip.showTip(mContext, "user.getName():" + prefix + "/" + user.getName());
                if ("standard_error".equals(prefix)) {
                    Map<String, String> map1 = new HashMap<>();
                    map1.put("msg", "get user failed");
                    String userMarshalling = new Gson().toJson(map1);
                    serverCallback.invoke("fail", new MarshallableObject(userMarshalling));
                } else {
                    String userMarshalling = new Gson().toJson(user);
                    serverCallback.invoke("success", new MarshallableObject(userMarshalling));
                }
            }
        });
    }

Java 執行 Js 函數:


    jsBridge.invoke("jsFn4", new MarshallableString("yellow"), new ClientCallback<String>() {
        @Override
        public void onReceiveResult(String invokeName, final String invokeParam) {
            if ("success".equals(invokeName)) {

                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        Tip.showTip(getApplicationContext(), invokeParam);
                    }
                });
            }
        }

        @Override
        public String getResult(String param) {
            return param;
        }
    });

銷毀 JsBridge

    @Override
    protected void onDestroy() {
        super.onDestroy();
        jsBridge.destroy();
    }

Javascript 環境

Javascript 的靈活性比較高,所以要簡單一些:

Javascript 注冊處理函數:

    window.JavaBridge.serverRegister('jsFn4', function (transactInfo, color) {
        log("jsFn4:" + color);
        title.style.background = color;
        log("jsFn4:callback");
        transactInfo.triggerCallback('success', 'background change to ' + color);
    });

Javascript 執行 Java 方法:


    var sdk = {
        getUser: function (params) {
            var _invokeName = 'getUser';
            var _invokeParam = params;
            var _clientCallback = params;
            window.JavaBridge.invoke(_invokeName, _invokeParam, _clientCallback);
        }
    };

    sdk.getUser({
        "name_prefix": "standard_error",
        "success": function (user) {
            log('sdk.getUser,success:' + user.name);
        },
        "fail": function (error) {
            log('sdk.getUser,fail:' + error.msg);
        }
    })

詳細 Demo 請參見 js-bridge-demo 工程


免責聲明!

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



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