欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android JsBridge源码学习

程序员文章站 2022-05-18 15:33:33
Android JsBridge源码学习 众所周知Android 4.2以下的WebView存在addJavascriptInterface漏洞的问题,不太了解的同学可参考 "Android4.2下 WebView的addJavascriptInterface漏洞解决方案" "@Javascript ......

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源码

lzyzsd/jsbridge

我注释的jsbridge

native调用js代码,向js端传递数据

以下是**“ native向js端传递数据,并接受js回调数据 ”**的时序图

sequencediagram participant bridgewebview.java as clienta participant webviewjavascriptbridge.js as servera participant demo.html as serverb note over clienta: native向js端传递数据 clienta-->>clienta: bridgewebview.callhandler\n("functioninjs", \n"native向js问好",\n mcallbackfunction); clienta-->>clienta: dosend(handlername, data, responsecallback) clienta-->>clienta: queuemessage(m) clienta-->>clienta: dispatchmessage(m) clienta->>servera: bridgewebview.loadurl(javascriptcommand)\n调用js的_handlemessagefromnative方法 servera-->>servera: _handlemessagefromnative(messagejson) servera-->>servera: _dispatchmessagefromnative(messagejson) servera->>serverb: handler(message.data, responsecallback) serverb-->>serverb: bridge.registerhandler\n("functioninjs", \nfunction(data, responsecallback)) serverb-->>servera: responsecallback(responsedata) servera-->>servera: _dosend({responseid,responsedata}); servera-->>clienta: reload iframe "yy://__queue_message__/" clienta-->>clienta: shouldoverrideurlloading(view, url) clienta-->>clienta: flushmessagequeue() clienta->>servera: bridgewebview.loadurl(javascriptcommand)\n调用js的_fetchqueue()方法 servera-->>servera: _fetchqueue() servera-->>clienta: reload iframe "yy://return/_fetchqueue/[{"data"}]" clienta-->>clienta: handlerreturndata(string url) clienta-->>clienta: flushmessagequeue中oncallback clienta-->>clienta: mcallbackfunction.oncallback(responsedata)

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 ==========

Android JsBridge源码学习