Hybrid(在Android中的實現(xiàn))

Native(以Android為例)和H5通訊,基本原理:

  • Android調(diào)用H5:通過webview類的loadUrl方法可以直接執(zhí)行js代碼型宙,類似瀏覽器地址欄輸入一段js一樣的效果
String javascriptCommand = String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');", messageJson);
webview.loadUrl(javascriptCommand);
  • H5調(diào)用Android:webview可以攔截H5發(fā)起的任意url請求均芽,webview通過約定的規(guī)則對攔截到的url進行處理(消費)贬派,即可實現(xiàn)H5調(diào)用Android
  var WVJBIframe = document.createElement('iframe');
  WVJBIframe.style.display = 'none';
  WVJBIframe.src = 'ddjsscheme://***';
  document.documentElement.appendChild(WVJBIframe);

實現(xiàn)步驟

1. H5調(diào)用原生方法實現(xiàn)步驟

h5調(diào)用原生方法有4個關(guān)鍵點:

1. 通訊媒介——原生通訊協(xié)議
ddjsscheme://return/_fetchQueue/[{"handlerName":"dd.native.call","data":{"handlerName":"biz.navigation.setTitle","params":{"title":"標題"}},"callbackId":"cb_1_1504142561851"}]

通過webview攔截到自定義約定的協(xié)議俐芯,就可以做相應(yīng)的處理篮绰。

2. 通訊行為——觸發(fā)

能被原生監(jiān)聽并捕獲截攔的H5行為肄渗,都可以作為原生通訊協(xié)議的觸發(fā)行為镇眷。Hybrid H5 的這類行為有 console.log、alert翎嫡、confirm欠动、prompt、location.href 等。將原生協(xié)議內(nèi)容通過其中的某一行為觸發(fā)具伍,即可被原生正確捕獲并解析翅雏。本文通過:WVJBIframe.src = 'ddjsscheme://';觸發(fā)。

觸發(fā)詳細流程

當h5頁面調(diào)用如下接口時

dd.biz.navigation.setTitle({
    title: '郵箱正文',
    onSuccess: function(data) {},
    onFail: function(error) {}
})

接著會調(diào)用此方法

function _executeFuction (methodName, options) {
  //alert('_executeFuction' + methodName)
  if (dd.isInit) {
    !!window['_' + methodName] && window['_' + methodName](options)
  } else {
    !!dd && setupWebViewJavascriptBridge(function () {
      if (window.WebViewJavascriptBridge.init) {
        if (!dd.isInit) {
          //alert('WebViewJavascriptBridge init')
          window.WebViewJavascriptBridge.init(function(message, responseCallback) {})
        }
      }
      dd.isInit = true;
      setTimeout(function() { 
        !!window['_' + methodName] && window['_' + methodName](options) 
      }, 100);
    });
  }
}

以本人的半杯水js水平來看人芽,先調(diào)用setupWebViewJavascriptBridge方法:

function setupWebViewJavascriptBridge(callback) {
  if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
  if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
  window.WVJBCallbacks = [callback];
  var WVJBIframe = document.createElement('iframe');
  WVJBIframe.style.display = 'none';
  WVJBIframe.src = 'ddjsscheme://__bridge_loaded__';
  document.documentElement.appendChild(WVJBIframe);
  setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

如果WebViewJavascriptBridge加載完成就調(diào)用window.WebViewJavascriptBridge.init(function(message, responseCallback) {});如果沒有加載完成WVJBIframe.src = 'ddjsscheme://__bridge_loaded__';然后native攔截加載本地的WebViewJavascriptBridge

if (url.equals("ddjsscheme://__bridge_loaded__")) {
    //加載bridge
    BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
}

接著回來說window.WebViewJavascriptBridge.init(function(message, responseCallback) {})

   function init(messageHandler) {
        if (WebViewJavascriptBridge._messageHandler) {
            throw new Error('WebViewJavascriptBridge.init called twice');
        }
        WebViewJavascriptBridge._messageHandler = messageHandler;
        var receivedMessages = receiveMessageQueue;
        receiveMessageQueue = null;
        for (var i = 0; i < receivedMessages.length; i++) {
            _dispatchMessageFromNative(receivedMessages[i]);
        }
    }

//提供給native使用,
    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                //直接發(fā)送
                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.data, message.data, exception);
                    }
                }
            }
        });
    }

  //sendMessage add message, 觸發(fā)native處理 sendMessage
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message.callbackId = callbackId;
        }
        sendMessageQueue.push(message);
        messagingIframe.src ='ddjsscheme://__WVJB_QUEUE_MESSAGE__';
    }

js不太懂望几,能看懂流程就可以啦,特別注意_doSend方法里的這行sendMessageQueue.push(message);代碼萤厅,message就是就是我們從h5調(diào)用接口傳過來的參數(shù)橄抹,后面會用到!這時又發(fā)出一條協(xié)議messagingIframe.src ='ddjsscheme://__WVJB_QUEUE_MESSAGE__';,我們再去本地看看做了什么操作:

 @Override
 public boolean shouldOverrideUrlLoading(WebView view, String url) {
   if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //攔截'ddjsscheme://__WVJB_QUEUE_MESSAGE__'
       webView.flushMessageQueue();
       return true;
   } else {
       return shouldOverrideUrl(view, url);
   }
 }
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 {
                        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++) {
                        Message m = list.get(i);
                        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
                            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);
                                    }
                                };
                            }
                            BridgeHandler handler;
                            if (!TextUtils.isEmpty(m.getHandlerName())) {
                                handler = messageHandlers.get(m.getHandlerName());
                            } else {
                                handler = defaultHandler;
                            }
                            if (handler != null){
                                handler.handle(m.getData(), responseFunction);
                            }
                        }
                    }
                }
            });
        }
    }

public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
        this.loadUrl(jsUrl);
        responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback)
}

這里的回調(diào)暫時先不關(guān)注惕味,后面我們在分析楼誓,注意responseCallbacks這個是個 map集合。它的value就是一個回調(diào)函數(shù)赦拘,同時還需知道responseFunction這個是native返回給h5的回調(diào)慌随,這里只需知道有這么個概念,后面我們會講解躺同。this.loadUrl(jsUrl);這里又加載了一段jsjavascript:WebViewJavascriptBridge._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);
}

這是重點阁猜! 這是重點! 這是重點蹋艺!重要的事說三遍剃袍!這里把sendMessageQueue轉(zhuǎn)換成json格式方便native解析,然后把sendMessageQueue回收掉捎谨,又到了我們熟悉的

messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);

再看一下本地做了什么操作:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回數(shù)據(jù)
            webView.handlerReturnData(url);
            return true;
        } else {
            return shouldOverrideUrl(view, url);
        }
 }

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

這里f.onCallBack(data);調(diào)用的就是flushMessageQueue()里的回調(diào)民效,data就是h5傳過來的參數(shù)

[{"handlerName":"dd.native.call","data":{"handlerName":"biz.navigation.setTitle","params":{"title":"標題"}},"callbackId":"cb_2_1504156072331"}]

第一個handlerName是作為messageHandlersmap的鍵;
第二個handlerName是native方法路徑
params是h5傳過來的設(shè)置標題的值
callbackId用來回調(diào)的在回調(diào)這節(jié)再去講解
至此涛救,h5與native通信的觸發(fā)過程講解完畢畏邢,下節(jié)我們一起看看flushMessageQueue()里的回調(diào)是如何做處理的。

3. 通訊行為——處理

flushMessageQueue()里有這么一段代碼

if (!TextUtils.isEmpty(m.getHandlerName())) {
      handler = messageHandlers.get(m.getHandlerName());
} else {
      handler = defaultHandler;
}
if (handler != null) {                          
      handler.handle(m.getData(), responseFunction);
}

這個handler意義在于處理h5傳過來的數(shù)據(jù)检吆,使用之前需要注冊:

public void registerHandler(String handlerName, BridgeHandler handler) {
        if (handler != null) {
            messageHandlers.put(handlerName, handler);
        }
}

webView.registerHandler(sFunction, new BridgeHandler() {
            @Override
            public void handle(String data, CallBackFunction function) {
                dispatchTask(data, function);
            }
});

注冊時需要鍵值對舒萎,鍵就是之前說的第一個handlerName,value是BridgeHandler,調(diào)用handle(String data, CallBackFunction function)時傳入就data是:

{"handlerName":"biz.navigation.setTitle","params":{"title":"標題"}}

function就是用于回調(diào)給h5的,下面再看一下dispatchTask(data, function);

private void dispatchTask(String data, CallBackFunction callBackFunction) {
        try {
            JSONObject jo = new JSONObject(data);
            final String handlerName = jo.getString("handlerName");
            String params = jo.getString("params");
            Log.d(TAG, "# ============ ");
            Log.d(TAG, "# request call ---> " + handlerName);
            Log.d(TAG, "# request params ---> " + params);

            // 接口調(diào)用每次都只執(zhí)行一次,為了能多次執(zhí)行,這里需要再重復(fù)調(diào)用原來的接口
            if (null == callBackFunction) {
                callBackFunction = new CallBackFunction() {
                    @Override
                    public void onCallBack(String data) {
                        mWebView.callHandler(handlerName, data, null);
                    }
                };
            }

            String[] split = handlerName.split("\\.");
            String packageName = "***";
            String className = split[split.length - 2];
            String methodName = split[split.length - 1];
            for (int i = 0; i < split.length - 2; i++) {
                packageName += "." + split[i];
            }
            packageName +=
                    "." + Character.toUpperCase(className.charAt(0)) + className.substring(1);

            Class<?> clazz = Class.forName(packageName);
            Constructor constructor = clazz.getConstructor(BridgeWebView.class, Context.class);
            Object obj = constructor.newInstance(mWebView, this);
            Method method = clazz.getMethod(methodName, String.class, CallBackFunction.class);
            method.invoke(obj, params, callBackFunction);
        } catch (JSONException | IllegalAccessException | InvocationTargetException
                | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
            e.printStackTrace();
        }
}

知道了方法路徑蹭沛,參數(shù)利用反射一切都水到渠成了臂寝,再看一下setTitle的處理:

public void setTitle(String data, CallBackFunction callBackFunction) {
        String result = null;
        JSONObject jo = new JSONObject();
        try {
            Log.d("setTitle==TTT===", data);
            JSONObject params = new JSONObject(data);
            String title = params.optString("title", "");
            if (mContext != null) {
                ((HybridActivity) mContext).setHeadTitle(title);
            }
            jo.put("result", true);
            result = new ResultCallBack().onSuccessResult(jo);
        } catch (JSONException e) {
            e.printStackTrace();
            try {
                jo.put("result", false);
            } catch (JSONException e1) {
                e1.printStackTrace();
            }
            result = new ResultCallBack()
                    .onFailResult(jo);
        }
        callBackFunction.onCallBack(result);
}

這樣h5調(diào)用native的處理步驟講解完畢,下面通過CallBackFunction講解如何回調(diào)給js的摊灭。

4. 通訊行為——回調(diào)

還記得在觸發(fā)那個步驟中的flushMessageQueue的這一段代碼嗎:

CallBackFunction responseFunction = null;
    // if had callbackId
    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);
        }
    };
}

當調(diào)用這段callBackFunction.onCallBack(result);代碼咆贬,就會回調(diào)到上面那段代碼,在queueMessage(responseMsg);又會調(diào)用

String javascriptCommand = String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');", messageJson);
webview.loadUrl(javascriptCommand);

js會把參數(shù)回調(diào)給h5帚呼,這樣h5調(diào)用native的流程就走通了掏缎。

2. 原生調(diào)用H5方法實現(xiàn)步驟

未完待續(xù)

參考文章

Hybrid APP架構(gòu)設(shè)計思路
解耦---Hybrid H5跨平臺性思考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子御毅,更是在濱河造成了極大的恐慌根欧,老刑警劉巖怜珍,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件端蛆,死亡現(xiàn)場離奇詭異,居然都是意外死亡酥泛,警方通過查閱死者的電腦和手機今豆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柔袁,“玉大人呆躲,你說我怎么就攤上這事〈匪鳎” “怎么了插掂?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長腥例。 經(jīng)常有香客問我辅甥,道長,這世上最難降的妖魔是什么燎竖? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任璃弄,我火速辦了婚禮,結(jié)果婚禮上构回,老公的妹妹穿的比我還像新娘夏块。我一直安慰自己,他們只是感情好纤掸,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布脐供。 她就那樣靜靜地躺著,像睡著了一般借跪。 火紅的嫁衣襯著肌膚如雪政己。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天垦梆,我揣著相機與錄音匹颤,去河邊找鬼。 笑死托猩,一個胖子當著我的面吹牛印蓖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播京腥,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼赦肃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起他宛,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤船侧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后厅各,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镜撩,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年队塘,在試婚紗的時候發(fā)現(xiàn)自己被綠了袁梗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡憔古,死狀恐怖遮怜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鸿市,我是刑警寧澤锯梁,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站焰情,受9級特大地震影響陌凳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烙样,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一冯遂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谒获,春花似錦蛤肌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至赔硫,卻和暖如春炒俱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爪膊。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工权悟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人推盛。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓峦阁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親耘成。 傳聞我的和親對象是個殘疾皇子榔昔,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容