Android JsBridge源码学习
android jsbridge源码学习
众所周知android 4.2以下的webview存在addjavascriptinterface漏洞的问题,不太了解的同学可参考
android4.2下 webview的addjavascriptinterface漏洞解决方案
@javascriptinterface
因此,公司项目中很早便使用 jsbridge
实现 “js与native的通信” 了。
native与js通信原理
android端webview启动时,会加载一段webviewjavascriptbridge.js
的js脚本代码。
-
native调用js代码:
当native需要向js端传递数据时,直接在android webview中使用
webview.loadurl(javascript:webviewjavascriptbridge.xxxxx)
调用在webviewjavascriptbridge.js
中提前定义好的xxxxx
方法,将数据传递到js端; -
js调用native代码:
当js需要将数据传递给native时,通过js
reload iframe
将数据传递到native的shouldoverrideurlloading(webview view, string url)
方法的url参数中,android端通过截获url获取js传递过来的参数。
以此来实现native与js的通信。
github源码
native调用js代码,向js端传递数据
以下是**“ native向js端传递数据,并接受js回调数据 ”**的时序图
bridgewebview.java
callhandler("functioninjs", "native向js问好", mcallbackfunction);
/** * native调用js * <p> * call javascript registered handler * 调用javascript处理程序注册 * * @param handlername js中注册的handlername * @param data native传递给js的数据 * @param callback js处理完成后,回调到native */ public void callhandler(string handlername, string data, callbackfunction callback) { dosend(handlername, data, callback); }
注释很全,看注释吧,不作讲解
bridgewebview.java
dosend(handlername, data, responsecallback)
/** * native 调用 js * <p> * 保存message到消息队列 * * @param handlername js中注册的handlername * @param data native传递给js的数据 * @param responsecallback js处理完成后,回调到native */ private void dosend(string handlername, string data, callbackfunction responsecallback) { logutils.e(tag, "dosend——>data: " + data); logutils.e(tag, "dosend——>handlername: " + handlername); // 创建一个消息体 message m = new message(); // 添加数据 if (!textutils.isempty(data)) { m.setdata(data); } // if (responsecallback != null) { // 创建回调id string callbackstr = string.format(bridgeutil.callback_id_format, ++uniqueid + (bridgeutil.underline_str + systemclock.currentthreadtimemillis())); // 1、js回调native数据时候使用;key: id value: callback (通过js返回的callbackid 可以找到相应的callback方法) responsecallbacks.put(callbackstr, responsecallback); // 1、js回调native数据时候使用;key: id value: callback (通过js返回的callbackid 可以找到相应的callback方法) m.setcallbackid(callbackstr); } // js中注册的方法名称 if (!textutils.isempty(handlername)) { m.sethandlername(handlername); } logutils.e(tag, "dosend——>message: " + m.tojson()); // 添加消息 或者 分发消息到js queuemessage(m); }
- 做了一个
message
来封装data数据 - 创建了一个callbackid,并将对应的引用存储在
map<string, callbackfunction> responsecallbacks
,这样js相应方法处理结束后,将js的处理结果返回来的时候,native可通过该callbackid找到对应的callbackfunction,从而完成数据回调。
/** * bridgewebview.java * list<message> != null 添加到消息集合否则分发消息 * * @param m message */ private void queuemessage(message m) { logutils.e(tag, "queuemessage——>message: " + m.tojson()); if (startupmessage != null) { startupmessage.add(m); } else { // 分发消息 dispatchmessage(m); } } /** * bridgewebview.java * 分发message 必须在主线程才分发成功 * * @param m message */ void dispatchmessage(message m) { logutils.e(tag, "dispatchmessage——>message: " + m.tojson()); // 转化为json字符串 string messagejson = m.tojson(); //escape special characters for json string 为json字符串转义特殊字符 messagejson = messagejson.replaceall("(\\\\)([^utrn])", "\\\\\\\\$1$2"); messagejson = messagejson.replaceall("(?<=[^\\\\])(\")", "\\\\\""); string javascriptcommand = string.format(bridgeutil.js_handle_message_from_java, messagejson); logutils.e(tag, "dispatchmessage——>javascriptcommand: " + javascriptcommand); // 必须要找主线程才会将数据传递出去 --- 划重点 if (thread.currentthread() == looper.getmainlooper().getthread()) { // 调用js中_handlemessagefromnative方法 this.loadurl(javascriptcommand); } }
- dispatchmessage中,通过load
javascript:webviewjavascriptbridge._handlemessagefromnative('%s');
将message数据传递到js方法的_handlemessagefromnative当中
// native通过loadurl(js_handle_message_from_java),调用js中_handlemessagefromnative方法,实现native向js传递数据 final static string js_handle_message_from_java = "javascript:webviewjavascriptbridge._handlemessagefromnative('%s');";
webviewjavascriptbridge.js
// 1、收到native的消息 // 提供给native调用,receivemessagequeue 在会在页面加载完后赋值为null,所以 function _handlemessagefromnative(messagejson) { // console.log(messagejson); // 添加到消息队列 if (receivemessagequeue) { receivemessagequeue.push(messagejson); } // 分发native消息 _dispatchmessagefromnative(messagejson); }
- 这里将native发送过来的消息添加到
receivemessagequeue
数组中。
//2、分发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) { ... } else { // 消息中有callbackid 说明需要将处理完成后,需要回调native端 //直接发送 if (message.callbackid) { // 回调消息的 回调id var callbackresponseid = message.callbackid; // responsecallback = function(responsedata) { // 发送js端的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); } } } }); }
demo.html
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); } });
- 这里调用到js的functioninjs注册方法,并将js的处理结果
javascript says right back aka!
返回,回调到webviewjavascriptbridge.js _dispatchmessagefromnative注册的responsecallback,从而调用到webviewjavascriptbridge.js 的_dosend方法之中。
一下为webviewjavascriptbridge.js 的_dosend
webviewjavascriptbridge.js
// 发送js端的responsedata _dosend({ responseid: callbackresponseid, responsedata: responsedata });
// 3、js将数据发送到native端 // sendmessage add message, 触发native的 shouldoverrideurlloading方法,使native主动向js取数据 // // 把消息队列数据放到shouldoverrideurlloading 的url中不就可以了吗? // 为什么还要native主动取一次,然后再放到shouldoverrideurlloading的url中返回? function _dosend(message, responsecallback) { // 发送的数据存在 if (responsecallback) { // var callbackid = 'cb_' + (uniqueid++) + '_' + new date().gettime(); responsecallbacks[callbackid] = responsecallback; message.callbackid = callbackid; } // 添加到消息队列中 sendmessagequeue.push(message); // 让native加载一个新的页面 messagingiframe.src = custom_protocol_scheme + '://' + queue_has_message; }
- 1、将native发送过来的message数据,存储到
sendmessagequeue
消息队列中 - 2、_dosend 中 reload iframe " yy://queue_message/ " 触发native的 shouldoverrideurlloading方法
bridgewebviewclient.java
@override public boolean shouldoverrideurlloading(webview view, string url) { logutils.d(tag, "shouldoverrideurlloading——>url: " + url); try { url = urldecoder.decode(url, "utf-8"); } 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); } }
- _dosend 中 reload iframe " yy://queue_message/ " 触发native的 shouldoverrideurlloading方法,最终调用到webview.flushmessagequeue();方法中
/** * 1、调用js的 _fetchqueue方法,获取js中处理后的消息队列。 * js 中_fetchqueue 方法 中将message数据返回到native的 {@link #bridgewebviewclient.shouldoverrideurlloading}中 * <p> * 2、等待{@link #handlerreturndata} 回调 callback方法 */ void flushmessagequeue() { logutils.d(tag, "flushmessagequeue"); if (thread.currentthread() == looper.getmainlooper().getthread()) { // 调用js的 _fetchqueue方法 bridgewebview.this.loadurl(bridgeutil.js_fetch_queue_from_java, new callbackfunction() { @override public void oncallback(string data) { // ... 此处暂时省略 } }); } }
- flushmessagequeue中加载了一段js脚本,js_fetch_queue_from_java,以下为js脚本的代码。
// 调用js的 _fetchqueue方法。_fetchqueue方法中将message数据返回到native的shouldoverrideurlloading中 final static string js_fetch_queue_from_java = "javascript:webviewjavascriptbridge._fetchqueue();";
- 这段js脚本代码调用到的是 webviewjavascriptbridge.js中的 _fetchqueue方法。
webviewjavascriptbridge.js
// 将数据返回给native // 提供给native调用,该函数作用:获取sendmessagequeue返回给native,由于android不能直接获取返回的内容,所以使用url shouldoverrideurlloading 的方式返回内容 function _fetchqueue() { // json数据 var messagequeuestring = json.stringify(sendmessagequeue); // message数据清空 sendmessagequeue = []; // 数据返回到shouldoverrideurlloading //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); }
- 这里通过 reload iframe " yy://return/_fetchqueue/ + encodeuricomponent(messagequeuestring)"将数据发送给native的shouldoverrideurlloading方法中。
/** * 1、获取到callbackfunction data执行调用并且从数据集移除 * <p> * 2、回调native{@link #flushmessagequeue()} callback方法 * * @param url */ void handlerreturndata(string url) { logutils.d(tag, "handlerreturndata——>url: " + url); // 获取js的方法名称 // _fetchqueue string functionname = bridgeutil.getfunctionfromreturnurl(url); // 获取_fetchqueue 对应的回调方法 callbackfunction f = responsecallbacks.get(functionname); // 获取body message消息体 string data = bridgeutil.getdatafromreturnurl(url); // 回调 native flushmessagequeue callback方法 if (f != null) { logutils.d(tag, "oncallback data" + data); f.oncallback(data); responsecallbacks.remove(functionname); return; } }
- 这里的callbackfunction 回调到了flushmessagequeue方法的oncallback中。
@override public void oncallback(string data) { logutils.d(tag, "flushmessagequeue——>data: " + data); // deserializemessage 反序列化消息 list<message> list = null; try { list = message.toarraylist(data); } catch (exception e) { e.printstacktrace(); return; } if (list == null || list.size() == 0) { logutils.e(tag, "flushmessagequeue——>list.size() == 0"); return; } for (int i = 0; i < list.size(); i++) { message m = list.get(i); string responseid = m.getresponseid(); /** * 完成native向js发送信息后的回调 */ // 是否是response callbackfunction if (!textutils.isempty(responseid)) { callbackfunction function = responsecallbacks.get(responseid); string responsedata = m.getresponsedata(); function.oncallback(responsedata); responsecallbacks.remove(responseid); } else { // ... 此处暂时省略 } } }
- 这里循环了从js端获取到的message队列,并将js端获取的数据,回调到了native中对应的callbackfunction中。
到这里,jsbridge中native调用js代码的通信,则完成了。
一个问题
webviewjavascriptbridge.js的_dosend(message, responsecallback)方法中,把message消息队列 放到shouldoverrideurlloading 的url
中直接返回给native不就可以了吗?
为什么还要用_dosend 中 reload iframe " yy://queue_message/ " 触发native的 shouldoverrideurlloading方法,让native主动向js请求一次message队列,然后再放到shouldoverrideurlloading的url中返回给native呢?
个人观点: 觉得,这样将message集中在一起,通过发送一个消息给native,让native主动将所有数据请求回来。避免了js与native的频繁交互。
js调用native代码,向native传递数据
不太想说了,就到这吧
========== the end ==========
推荐阅读