Native JsBridge源码解析 深入理解JsBridge
来源:互联网 发布:库里杜兰特知乎 编辑:程序博客网 时间:2024/06/02 02:51
最近项目中使用了 HyBrid 框架,但是在使用过程中遇到了不少问题,因此花时间来研究了一下其中原理! 在平时开发过程中,不管是可复用性非常高,可以跨平台开发的 HyBrid ,还是半 Native 半 web 浅尝辄止的 HyBrid ,对 Android 而言,陌生的就是其中的通信——Android 与 Html 的互相通信。这里就不掉书包,直接阐明其中的使用方法。
- 引入JsBridge库
//at your porject gradlerepositories { // ... maven { url "https://jitpack.io" }}//at you app gradledependencies { compile 'com.github.lzyzsd:jsbridge:1.0.4'}
Android端收发消息
- 使用控件
- 向Html发送消息
- 接收Html发送的消息
//布局 <com.github.lzyzsd.jsbridge.BridgeWebView android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent" > </com.github.lzyzsd.jsbridge.BridgeWebView>//控件webView = (BridgeWebView) findViewById(R.id.webView);/** * 发送消息给html * @param value 字符串 * @param data 数据 * @param function 回调方法 */webView.callHandler("value", "data", new CallBackFunction() { @Override public void onCallBack(String response) { } }); /** * 发送消息给html * @param data 字符串 */ webView.send("hello");/** * register handler,so that javascript can call it * 注册handler,以便javascript可以调用它 * @param handlerName * @param handler */webView.registerHandler("value", new BridgeHandler() { @Override public void handler(String data, CallBackFunction function) { } });
- Html端收发消息
//注册 WebViewJavascriptBridgeReadyfunction connectWebViewJavascriptBridge(callback) { if (window.WebViewJavascriptBridge) { callback(WebViewJavascriptBridge) } else { document.addEventListener( 'WebViewJavascriptBridgeReady' , function() { callback(WebViewJavascriptBridge) }, false ); }}connectWebViewJavascriptBridge(function(bridge) { bridge.init(function(message, responseCallback) { console.log('JS got a message', message); //默认返回值 var data = { 'Javascript Responds': '测试中文!' }; console.log('JS responding with', data); responseCallback(data); }); /** * 接收消息 * "functionInJs" 字符串标签 * data 收到数据 * responseCallback 回调接口 */ bridge.registerHandler("functionInJs", function(data, responseCallback) { document.getElementById("show").innerHTML = ("data from Java: = " + data); var responseData = "Javascript Says Right back aka!"; responseCallback(responseData); }); })
通过库文件demo可以仔细看到这些方法,还是比较容易理解的。但是具体是怎么一个原理呢?跟踪代码看看,其实非常简单:
webView.send("hello");/** * 发送消息给html * @param value 字符串 * @param data 数据 * @param function 回调方法 */private void sendMessage(String value, String data, CallBackFunction function) { webView.callHandler(value, data, function); }
接下来看看BridgeWebView的源码:
@Override public void send(String data) { send(data, null); } @Override public void send(String data, CallBackFunction responseCallback) { doSend(null, data, responseCallback); }/** * call javascript registered handler * * @param handlerName * @param data * @param callBack */ public void callHandler(String handlerName, String data, CallBackFunction callBack) { doSend(handlerName, data, callBack); }
接下来仔细研究一下doSend这个方法,源码如下:
private void doSend(String handlerName, String data, CallBackFunction responseCallback) { Message m = new Message(); if (!TextUtils.isEmpty(data)) { m.setData(data); } if (responseCallback != null) { String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis())); responseCallbacks.put(callbackStr, responseCallback); m.setCallbackId(callbackStr); } if (!TextUtils.isEmpty(handlerName)) { m.setHandlerName(handlerName); } queueMessage(m); }
上面的代码其实就是封装了一个 Message,然后将 Message 添加到添加到队列里,这个结构类似于 Handler,接下来看看队列消息里面怎么将消息发送到 Js?
private void queueMessage(Message m) { if (startupMessage != null) { startupMessage.add(m); } else { dispatchMessage(m); } } void dispatchMessage(Message m) { String messageJson = m.toJson(); //escape special characters for json string messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2"); messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\""); String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson); if (Thread.currentThread() == Looper.getMainLooper().getThread()) { LUtil.e(javascriptCommand); this.loadUrl(javascriptCommand); } }
startupMessage 其实就是一个消息队列,这个对象的初始值肯定不为 null,但是这里的 startupMessage 确实是被置空了,至于在什么地方置空的,稍候再研究,我们可以看到在分发消息的时候,直接将 Message 转成了 Json 字符串,然后对字符串经过一系列处理便加载了这个方法,通过日志,我们可以看到这时候输出的内容如下:
问题是我们注册的时候,并没有这个方法,那么这个方法在什么地方被声明的呢?通过查看 BridgeWebView 的相关方法,最后发现在 BridgeWebViewClient 的 onPageFinished 方法里,这里将 assets 文件夹下的 WebViewJavascriptBridge.js 读取出来发送到了 html,而 WebViewJavascriptBridge.js 内容是什么呢?看代码:
//发送消息内容:view.loadUrl("javascript:" + jsContent);//消息内容如下//notation: js file can only use this kind of comments//since comments will cause error when use in webview.loadurl,//comments will be remove by java use regexp(function() { if (window.WebViewJavascriptBridge) { return; } var messagingIframe; var sendMessageQueue = []; var receiveMessageQueue = []; var messageHandlers = {}; var CUSTOM_PROTOCOL_SCHEME = 'yy'; var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/'; var responseCallbacks = {}; var uniqueId = 1; function _createQueueReadyIframe(doc) { messagingIframe = doc.createElement('iframe'); messagingIframe.style.display = 'none'; doc.documentElement.appendChild(messagingIframe); } //set default messageHandler 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]); } } function send(data, responseCallback) { _doSend({ data: data }, responseCallback); } function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; } function callHandler(handlerName, data, responseCallback) { _doSend({ handlerName: handlerName, data: data }, responseCallback); } //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 = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; } // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容 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); } //提供给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, exception); } } } }); } //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以 function _handleMessageFromNative(messageJSON) { console.log(messageJSON); if (receiveMessageQueue && receiveMessageQueue.length > 0) { receiveMessageQueue.push(messageJSON); } else { _dispatchMessageFromNative(messageJSON); } } var WebViewJavascriptBridge = window.WebViewJavascriptBridge = { init: init, send: send, registerHandler: registerHandler, callHandler: callHandler, _fetchQueue: _fetchQueue, _handleMessageFromNative: _handleMessageFromNative }; var doc = document; _createQueueReadyIframe(doc); var readyEvent = doc.createEvent('Events'); readyEvent.initEvent('WebViewJavascriptBridgeReady'); readyEvent.bridge = WebViewJavascriptBridge; doc.dispatchEvent(readyEvent);})();
前面讲的 startupMessage 置空,就是在上面的 onPageFinished 中被置空的。
//置空startupMessageif (webView.getStartupMessage() != null) { for (Message m : webView.getStartupMessage()) { webView.dispatchMessage(m); } webView.setStartupMessage(null);}
接下来分析,BridgeWebView 端发送的消息是怎么在 JS 被执行的呢?先是通过 _handleMessageFromNative 方法调用 _dispatchMessageFromNative 方法,最后执行如下代码:
try { handler(message.data, responseCallback);} catch (exception) { if (typeof console != 'undefined') { console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception); }}
handler(message.data, responseCallback) 方法具体实现是在下面实现的( js 不大熟悉,整理代码发现逻辑大致如下):
/** * 接收消息 * "functionInJs" 字符串标签 * data 收到数据 * responseCallback 回调接口 */ bridge.registerHandler("functionInJs", function(data, responseCallback) { document.getElementById("show").innerHTML = ("data from Java: = " + data); var responseData = "Javascript Says Right back aka!"; responseCallback(responseData); });
接着我们看看 BridgeWebView 端发送消息的回调接口,是怎么实现的?通过代码我们发现 _dispatchMessageFromNative 方法下回调方法 _doSend 方法:
//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 = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
_doSend 方法请求 url:yy://QUEUE_MESSAGE/
当 BridgeWebView 的 shouldOverrideUrlLoading 方法收到 messagingIframe 请求,继续检查这个方法:
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { try { url = URLDecoder.decode(url, "UTF-8"); LUtil.e("shouldOverrideUrlLoading:"+url); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { webView.handlerReturnData(url); return true; } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { // webView.flushMessageQueue(); return true; } else { return super.shouldOverrideUrlLoading(view, url); } }
这次我们查看 BridgeWebView 的 flushMessageQueue 方法,直接看代码:
/** * 刷新消息队列 */ void flushMessageQueue() { if (Thread.currentThread() == Looper.getMainLooper().getThread()) { loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() { *** }); } }public void loadUrl(String jsUrl, CallBackFunction returnCallback) { this.loadUrl(jsUrl); responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback); }
其实这里很清楚了,就是当 JS 需要返回数据的时候,先将需要返回的数据保存下来,然后发送请求到 BridgeWebView ,告诉 BridgeWebView 我要发送回调数据了,然后 BridgeWebView 将请求和接口以键值对的形式保存下来,并请求 js 的方法—— javascript:WebViewJavascriptBridge._fetchQueue();JS 收到消息后就发送请求,然后 shouldOverrideUrlLoading 收到请求判断消息头以后调用 BridgeWebView 的方法,代码如下:
//返回消息头 yy://return/_fetchQueue/+返回数据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; } }
最后将这个过程整理如下:
- 安卓发送消息给 Js 过程
- 当 WebView 发送数据给 Js 时: WebView 请求 js 方法—— javascript:WebViewJavascriptBridge._handleMessageFromNative( gson 字符串);
- js 执行相关方法: _handleMessageFromNative——_dispatchMessageFromNative——handler(message.data, responseCallback);,
- Js 通过接口返回数据
- js 执行 _doSend 方法;
- BridgeWebView 拦截请求并判断消息头,如果消息头标签是—— yy://,则将 _fetchQueue 和接口再次以键值对的形式保存下来,并通过 BridgeWebView 请求 js 的 _fetchQueue() 方法;
- js 的 _fetchQueue() 方法将值发送给 BridgeWebView;
- BridgeWebView 再次判断请求的消息头,如果消息头标签是—— yy://return/ ,则用 _fetchQueue 取出的接口对象;
- 在接口对象中,根据返回的接口对象 Id —— responseId 取出 BridgeWebView 存储的待处理数据的接口对象,并将请求里面包含的数据取出来,执行待处理数据的接口。
接下来 Js 发送消息给 BridgeWebView 的过程又如何呢?首先咱们得与 Js 约定一个 Handler,然后将这个 Handler 保存下来,代码如下:
Map<String, BridgeHandler> messageHandlers = new HashMap<String, BridgeHandler>();/** * register handler,so that javascript can call it * * @param handlerName * @param handler */public void registerHandler(String handlerName, BridgeHandler handler) { if (handler != null) { messageHandlers.put(handlerName, handler); } }
然后咱们继续看 Js 是如何发送消息给 BridgeWebView,还是直接看源码:
//call native method window.WebViewJavascriptBridge.callHandler( 'submitFromWeb' , {'param': '中文测试'} , function(responseData) { document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData } );
来来来,咱们理一下这个过程,首先 js 执行 callHandler 方法—— _doSend 方法,在这个方法 _doSend 方法中还是一样:将 Message 信息保存下来,并发送请求给 BridgeWebView,告诉他,我要给你发消息了,准备好!
然后过程就和上面类似了,过程如下:
- js 执行 _doSend 方法,保存消息并发送请求—— yy://QUEUE_MESSAGE/;
- BridgeWebView 拦截请求并判断消息头,如果消息头标签是—— yy://,则将 _fetchQueue 和接口以键值对的形式保存下来,并通过 BridgeWebView 请求 js 的 _fetchQueue() 方法;
- js 执行 _fetchQueue() 方法并发送请求——yy://return/_fetchQueue/+”消息”;
- BridgeWebView 在 shouldOverrideUrlLoading 方法拦截请求,如果消息头标签是—— yy://return/_fetchQueue/,则取出第2点保存的接口,并将请求包含的数据发送给接口;
- 在接口当中做两件事,第一件事是根据请求信息生成 Message 对象并保存下来(BridgeWebView的queueMessage方法),另一件事就是根据请求内容将 js 与 BridgeWebView 约定 BridgeHandler ,并执行 BridgeHandler 的 handler 方法;
- BridgeWebViewClient 在 onPageFinished 方法中分发消息(dispatchMessage方法)—— BridgeWebView 请求 js 的 _handleMessageFromNative 方法;
- Js 执行 _handleMessageFromNative 方法—— _dispatchMessageFromNative 方法,在方法中根据 responseId 找出接口,并执行该接口,完毕。
上述已经描述了 BridgeWebView 发送消息并回调、Js 发送消息并回调,整个过程我们已经很清楚了,那么这些个过程有没有需要优化的地方呢?比如 BridgeWebView 与 js 优化的地方有很多,那么能不能把接口改一下呢?咱们试一试!由于整个 JsBridge 代码文件不多,我们可以直接下载下来,这样方便我们修改,因此我们可以直接修改 BridgeHandler.java ,然后修改部分代码,说干就干:
public interface BridgeHandler { void handler(String name, String data, CallBackFunction function);}//修改DefaultHandlerpublic class DefaultHandler implements BridgeHandler { @Override public void handler(String name,String data, CallBackFunction function) { if(function != null){ function.onCallBack("DefaultHandler response data"); } }}//通过Js信息查找HandlerJsBridgeHandler handler; if (!TextUtils.isEmpty(m.getHandlerName())) { handler = messageHandlers.get(m.getHandlerName()); } else { handler = defaultHandler; } if (handler != null){ handler.handler(m.getHandlerName(),m.getData(), responseFunction); }//修改注册方法@Override public void handler(String name, String data, final CallBackFunction function) { switch (name){ case "open"://打开后台维护界面 break; case "token"://获取token break; case "clientId"://获取clientId break; case "out"://退出账号 break; } }
以上已经分析完毕。JsBridge 的原理大家都知道了,接下来需要做的就是WebView 的优化,因为我们知道原生的 WebView 加载页面的时候渲染比较慢,一些诸如腾讯 X5、VasSonic 等据说渲染效果比 WebView 好很多,那么兼容通信和渲染效果良好的的 JsBridge 框架,应该怎么搭建呢?好好想一想?
腾讯 X5 WebView 接入
X5 WebView 比较简单,就是一个 jar 包,然后将原本的 android.webkit 包下的内容转为 com.tencent.smtt 下的相关类,并实现相关权限(运行时权限需要自己处理),具体如下:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.READ_PHONE_STATE" />
- VasSonic-Android 接入
VasSonic接入比较简单,但是使用就比较复杂,接入如下:
//Add VasSonic gradle plugin as a dependency in your module's build.gradlecompile 'com.tencent.sonic:sdk:1.0.0'
使用前需要实现 SonicRuntime 和 SonicSessionClient 接口,具体参考 VasSonic
腾讯 X5 WebView 、VasSonic 、原生 WebView 对比
通过对onPageStarted方法、onPageFinished方法查看各个 WebView 加载页面所需时间,发现 X5 、VasSonic 均比原生 WebView 快了近一倍,所以 JsBridge 有必要将第三方WebView 加载到项目中,但是加载 X5 还是 VasSonic 呢?通过查看文档发现 VasSonic 更新需要和后台服务器交互判断是否更新数据?也就是说 VasSonic 需要和后台配合才能发挥最大效果。我倾向于选择前者,因为使用起来简单得多,而后者的库也配置好了,地址如下:https://github.com/Vicent9920/JsBridge
JsBridge 注意事项:
App 首次就可以加载 x5 内核
App 在启动后(例如在 Application 的 onCreate 中)立刻调用 QbSdk 的预加载接口 initX5Environment ,可参考接入示例,第一个参数传入 context,第二个参数传入 callback,不需要 callback 的可以传入 null,initX5Environment 内部会创建一个线程向后台查询当前可用内核版本号,这个函数内是异步执行所以不会阻塞 App 主线程,这个函数内是轻量级执行所以对 App 启动性能没有影响,当 App 后续创建 webview 时就可以首次加载 x5 内核了。(也可以像LitePal那样在Application初始化,但是需要文件清单配置或者继承自该Application,或者在该类传入一个静态方法初始化 JsBridge )
获取系统内核的WebView或者 x5内核的WebView的宽高
com.tencent.smtt.sdk.WebView webView = new com.tencent.smtt.sdk.WebView(this);int width = webView.getView().getWidth();
- 避免输入法界面弹出后遮挡输入光标的问题
//方法一:在AndroidManifest.xml中设置android:windowSoftInputMode="stateHidden|adjustResize"//方法二:在代码中动态设置:getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
- 兼容视频播放
1)享受页面视频的完整播放体验需要做如下声明:
页面的Activity需要声明
android:configChanges="orientation|screenSize|keyboardHidden"
2)视频为了避免闪屏和透明问题,需要如下设置
a)网页中的视频,上屏幕的时候,可能出现闪烁的情况,需要如下设置:Activity在onCreate时需要设置:
getWindow().setFormat(PixelFormat.TRANSLUCENT);(这个对宿主没什么影响,建议声明)
在非硬绘手机和声明需要controller的网页上,视频切换全屏和全屏切换回页面内会出现视频窗口透明问题,需要如下设置
声明当前<item name="android:windowIsTranslucent">false为不透明。
特别说明:这个视各app情况所需,不强制需求,如果声明了,对体验更有利
c)以下接口禁止(直接或反射)调用,避免视频画面无法显示:
webview.setLayerType()webview.setDrawingCacheEnabled(true);
其它参考文档:X5 接入文档
WebView相关文章推荐:
腾讯浏览服务X5内核集成
Android中WebView的JavaScript代码和本地代码交互的三种方式
WebView详解与简单实现Android与H5互调
WebView写入数据到 localStorage总结
- Native JsBridge源码解析 深入理解JsBridge
- 理解JSBridge
- JsBridge 源码分析
- Android JsBridge源码分析
- JsBridge源码分析
- WebView Native与H5交互—jsbridge
- html和native使用JSBridge交互
- 【iOS开发】H5与Native交互之JSBridge技术
- 小悟:H5和native利用JsBridge交互
- Android webView与js 交互以及jsbridge框架源码分析
- JsBridge最详细的解析,高版本webview的evaluateJavascript
- Hybird App 之 JSBridge
- 框架使用系列--JSbridge
- HyBrid应用-JsBridge
- JsBridge与客户端交互
- Hybrid 开发:JsBridge
- JsBridge实现及原理
- JSBridge框架学习小结
- MySQL必知必会 学习笔记二
- Netfilter 和 iptables关系
- c++模板概述
- JavaScript对象总概
- IntelliJ IDEA的安装和使用
- Native JsBridge源码解析 深入理解JsBridge
- UVAlive 3890&Poj3525 半平面交+二分 解题报告
- 机房收费系统(三)组合查询
- Sqoop
- webhook开发环境部署
- 使用 Netty 搭建消息中心
- 组合数取模(-)
- linux运行级别
- c# 获取当前运行程序文件,函数,行号