本系列文章主要分析 JsBridge 框架的原理,学习 H5 和原生 WebView 的交互方式,框架选自 GitHub 上的很火的 H5 + WebView 三方库:lzyzsd/JsBridge,作者是大鬼头;
1 初步分析
下面分析下 jsBridge 框架的通信协议,他是实际上是一个 js 文件,位于 assets 目录下:
1
| WebViewJavascriptBridge.js
|
这个 js 文件作为协议,决定了 H5 和 Native 代码通信方式和通信数据!
这里就有一个问题了,他是如何被加载并生效的呢,有两种方式:
- 第一种方式:通过 H5 直接加载;
- 第二种方式:通过动态注入的方式:
1
| BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
|
我们的 jsBridge 框架也是用的第二种方式,具体的逻辑我们后面再分析;
2 协议代码分析
下面我们分析下 js 协议代码的逻辑:
1 2 3 4 5 6 7
| (function() { if (window.WebViewJavascriptBridge) { return; } ... ... ... })();
|
这里来看的话,其实他是一个 js function,当我们将 js 动态注入到 H5 中时,这么这个 function 就会执行;
2.1 内部关键变量
js 文件中定义了一些关键的变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var messagingIframe; var bizMessagingIframe; var sendMessageQueue = []; var receiveMessageQueue = []; var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'yy'; var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/';
var responseCallbacks = {}; var uniqueId = 1;
... ... ...
var WebViewJavascriptBridge = window.WebViewJavascriptBridge = { init: init, send: send, registerHandler: registerHandler, callHandler: callHandler, _fetchQueue: _fetchQueue, _handleMessageFromNative: _handleMessageFromNative };
|
上面最关键的一个对象就是 WebViewJavascriptBridge,H5 和 Native 都会通过它。
这个 window.WebViewJavascriptBridge 内部包含了一些函数对象,这些 function 都定义在 js 内部!
2.2 动态注入初始化
这里是很关键的地方:
1 2 3 4 5 6 7 8 9 10
| var doc = document;
_createQueueReadyIframe(doc); _createQueueReadyIframe4biz(doc);
var readyEvent = doc.createEvent('Events'); readyEvent.initEvent('WebViewJavascriptBridgeReady'); readyEvent.bridge = WebViewJavascriptBridge; doc.dispatchEvent(readyEvent);
|
在动态注入的时候,会执行初始化的操作:
- 创建了一个 event;
- 初始化 event,事件类型为 ‘WebViewJavascriptBridgeReady’;
- readyEvent.bridge 设置为我们上面创建的 ‘WebViewJavascriptBridgeReady’ 对象;
- doc.dispatchEvent 分发 event;
这个 event 是在哪里做响应的呢?
是在 H5 里面,这个 H5 在加载时候,会执行内部 js 脚本,并通过 document.addEventListener 方法设置该 event 的监听器;
2.2.1 H5 加载启动 event 监听
H5 的页面里面,是有下面的一段 js 脚本,在 webview.loadUrl 后会直接加载该 js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <script> ... ... ... function connectWebViewJavascriptBridge(callback) { if (window.WebViewJavascriptBridge) { callback(WebViewJavascriptBridge) } else { document.addEventListener( 'WebViewJavascriptBridgeReady' , function() { callback(WebViewJavascriptBridge) }, false ); } }
connectWebViewJavascriptBridge(function(bridge) { ... ... ... ... }) </script>
|
默认情况下,window.WebViewJavascriptBridge 不存在,那么会注册一个 EventListener!
等待 event 触发后,执行 callback!
2.2.2 event 出发点后下一步初始化
callback 实际上就是闭包,参数 bridge 就是 js 协议中创建的 var WebViewJavascriptBridge:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
bridge.init(function(message, responseCallback) { console.log('JS got a message', message); var data = { 'Javascript Responds': '测试中文!' };
if (responseCallback) { console.log('JS responding with', data); responseCallback(data); } });
bridge.registerHandler("functionInJs", function(data, responseCallback) { document.getElementById("show").innerHTML = ("data from Java: = " + data); if (responseCallback) { var responseData = "Javascript Says Right back aka!"; responseCallback(responseData); } });
|
关于 init 和 registerHandler 我们会在下面分析:
2.3 核心函数
下面来分析下关键的协议函数:
2.3.1 init
init 方法用于设置 js 处理 native 消息的默认 handler:
同时也会分发已经被添加到 receiveMessageQueue 接受队列中的 native 的消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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]); } }
|
参数 messageHandler 就是【*2.2.2】中的函数闭包;
2.3.2 registerHandler
注册特定的消息处理 handler:
1 2 3 4
| function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; }
|
messageHandlers 之前有说过,是 js 处理 native 消息的 handler 数组!
- index 是 handler 的名称,根据前面代码,名称是 “functionInJs”;
- value 是一个函数闭包;
2.3.3 _dispatchMessageFromNative
这个方法是 js 层调用的,分发来自 native 的消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| function _dispatchMessageFromNative(messageJSON) { setTimeout(function() { var message = JSON.parse(messageJSON); var responseCallback; 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]; } try { handler(message.data, responseCallback); } catch (exception) { if (typeof console != 'undefined') { console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception); } } } }); }
|
到这里看起来,似乎很清晰呢;
2.3.4 _doSend
这个方法是 js 层调用,用于发送消息给 native 端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); responseCallbacks[callbackId] = responseCallback; message.callbackId = callbackId; } sendMessageQueue.push(message); messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
|
这里要注意第二个参数 responseCallback:
- 如果 responseCallback 不为 null,说明本次消息需要回调通知;
- 如果 responseCallback 为 null,说明不需要回调通知;
该方法创建了一个动态的 url,这会被 Webview.shouldOverrideUrlLoading 拦截到,这是该库 android 获得 js 数据的方式;
但是这里并不是真正获取数据的地方,该 url 会触发一次 Webview.shouldOverrideUrlLoading;
然后 android 又会调用 js 的 _fetchQueue 方法,这时,又会生成一个 url,这个 url 才会保存了要传递给 android 的消息;
具体可以看 2.3.8 的 _fetchQueue 方法;
2.3.5 callHandler
这个方法是 js 层调用的,通过这个接口来调用 native 方法:
- handlerName:js 处理消息的 handler 名称,这个 handler 是 native 层注册到 js 的;
- data:native 层传递的数据;
- responseCallback:接受回调的接口,native 层处理完数据会回调;
1 2 3 4 5 6 7 8
| function callHandler(handlerName, data, responseCallback) { _doSend({ handlerName: handlerName, data: data }, responseCallback); }
|
这里第二个参数不为 null,因为 js 短需要收到回调;
该方法设置 handlerName,所以 native 会使用指定 handlerName 的 handler 去处理;
2.3.6 send
这个方法也是 js 层调用的,通过这个接口来调用 native 方法:
1 2 3 4 5 6
| function send(data, responseCallback) { _doSend({ data: data }, responseCallback); }
|
这里我们看到,他并没有设置 handlerName,所以 native 会使用默认的 handler 去处理;
2.3.7 _handleMessageFromNative
这个方法是 native 层调用的,以 json string 的形式发送数据给 js:
1 2 3 4 5 6 7 8 9
| function _handleMessageFromNative(messageJSON) { console.log(messageJSON); if (receiveMessageQueue) { receiveMessageQueue.push(messageJSON); } _dispatchMessageFromNative(messageJSON); }
|
这里很简单,就不多说了;
2.3.8 _fetchQueue
这个方法是 native 层调用的,用于获取 sendMessageQueue 队列中的消息:
1 2 3 4 5 6 7 8 9
| function _fetchQueue() { var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = []; if (messageQueueString !== '[]') { bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString); } }
|
逻辑很简单,不多说了,关于 H5 和 Native 通信的流程,后续再分析!
3 总结
关于 js 通信协议的相关分析到这里就结束了。
这里我自己也有点疑惑,对于 android 获取 js 数据的方式,该库并没有使用 @JavascriptInterface 注解,通过如下方式实现:
1
| WebView.addJavascriptInterface(new WebData(), "webdata");
|
通过查阅相关资料,可能有如下的原因:
- 安全隐患:这是因为同源规则 (SOP) 不适用与该方法,加上第三方 JavaScript 库或来自一个陌生域名的 iframe 可能在 Java 层访问这些被暴露的方法。因此,攻击者可通过一个 XSS 漏洞执行原生代码或者注入病毒代码到应用程序中。
- 兼容性:JavaScript 层中暴露的 Java 对象的所有公有方法在 Android 版本低于 JerryBean MRI(API Level 17) 以下时可访问。而在 Google API 17 (4.2)以上,暴露的函数必须通过 @JavaScriptInterface 注释来防止方法的暴露