美文网首页
Hybrid(在Android中的实现)

Hybrid(在Android中的实现)

作者: 武穆遗书 | 来源:发表于2017-08-31 14:10 被阅读0次

    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跨平台性思考

    相关文章

      网友评论

          本文标题:Hybrid(在Android中的实现)

          本文链接:https://www.haomeiwen.com/subject/izwtjxtx.html