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 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);
再看一下本地做了什么操作:
@Overridepublic 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
是作为messageHandlers
map的键;
第二个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跨平台性思考
更多相关文章
- Android MediaPlayer 常用方法介绍
- Android 自定义组件 方法篇
- android中HttpURLConnection调用getResponseCode()时崩溃 解决方
- android studio调试c++代码
- Android自学笔记(Android x86模拟器Intel Atom x86 System Image
- android Button组件的属性和方法
- Android 中数据库查询方法 query() 中的 select
- Android空引用问题的解决方法——on a null object reference