Hybrid(在Android中的实现)

Native(以Android为例)和H5通讯,基本原理:

  • Android调用H5:通过webview类的loadUrl方法可以直接执行js代码,类似浏览器地址栏输入一段js一样的效果
String javascriptCommand = String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');", messageJson);
webview.loadUrl(javascriptCommand);
  • H5调用Android:webview可以拦截H5发起的任意url请求,webview通过约定的规则对拦截到的url进行处理(消费),即可实现H5调用Android
  var WVJBIframe = document.createElement('iframe');
  WVJBIframe.style.display = 'none';
  WVJBIframe.src = 'ddjsscheme://***';
  document.documentElement.appendChild(WVJBIframe);

实现步骤

1. H5调用原生方法实现步骤

h5调用原生方法有4个关键点:

1. 通讯媒介——原生通讯协议
ddjsscheme://return/_fetchQueue/[{"handlerName":"dd.native.call","data":{"handlerName":"biz.navigation.setTitle","params":{"title":"标题"}},"callbackId":"cb_1_1504142561851"}]

通过webview拦截到自定义约定的协议,就可以做相应的处理。

2. 通讯行为——触发

能被原生监听并捕获截拦的H5行为,都可以作为原生通讯协议的触发行为。Hybrid H5 的这类行为有 console.log、alert、confirm、prompt、location.href 等。将原生协议内容通过其中的某一行为触发,即可被原生正确捕获并解析。本文通过:WVJBIframe.src = 'ddjsscheme://';触发。

触发详细流程

当h5页面调用如下接口时

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

接着会调用此方法

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水平来看,先调用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加载完成就调用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 {
                //直接发送
                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, 触发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调用接口传过来的参数,后面会用到!这时又发出一条协议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)
}

这里的回调暂时先不关注,后面我们在分析,注意responseCallbacks这个是个 map集合。它的value就是一个回调函数,同时还需知道responseFunction这个是native返回给h5的回调,这里只需知道有这么个概念,后面我们会讲解。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转换成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)) { // 如果是返回数据
            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);调用的就是flushMessageQueue()里的回调,data就是h5传过来的参数

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

第一个handlerName是作为messageHandlersmap的键;
第二个handlerName是native方法路径
params是h5传过来的设置标题的值
callbackId用来回调的在回调这节再去讲解
至此,h5与native通信的触发过程讲解完毕,下节我们一起看看flushMessageQueue()里的回调是如何做处理的。

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传过来的数据,使用之前需要注册:

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,调用handle(String data, CallBackFunction function)时传入就data是:

{"handlerName":"biz.navigation.setTitle","params":{"title":"标题"}}

function就是用于回调给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);

            // 接口调用每次都只执行一次,为了能多次执行,这里需要再重复调用原来的接口
            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();
        }
}

知道了方法路径,参数利用反射一切都水到渠成了,再看一下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调用native的处理步骤讲解完毕,下面通过CallBackFunction讲解如何回调给js的。

4. 通讯行为——回调

还记得在触发那个步骤中的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);
        }
    };
}

当调用这段callBackFunction.onCallBack(result);代码,就会回调到上面那段代码,在queueMessage(responseMsg);又会调用

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

js会把参数回调给h5,这样h5调用native的流程就走通了。

2. 原生调用H5方法实现步骤

未完待续

参考文章

Hybrid APP架构设计思路
解耦---Hybrid H5跨平台性思考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容