Android jsbridge source code learning

Time:2020-2-19

Android jsbridge source code learning

As we all know, WebView under Android 4.2 has the problem of addjavascriptinterface vulnerability. For those who don’t know much, please refer to
Solution to addJavascriptInterface vulnerability of WebView under Android4.2
@JavascriptInterface
As a result, it is used early in company projectsJsBridgeImplement “JS and native communication”.

Communication principle of native and JS

When WebView on Android is started, a section will be loadedWebViewJavascriptBridge.jsJS script code for.

  • Native calls JS code:

When native needs to pass data to JS, it is directly used in Android WebViewWebView.loadURL(javascript:WebViewJavascriptBridge.xxxxx)Call inWebViewJavascriptBridge.jsDefined in advancexxxxxMethod to transfer the data to JS;

  • JS calls native code:

When JS needs to pass the data to native, it uses JSreload iframeTransfer data to native’sshouldOverrideUrlLoading(WebView view, String url)In the URL parameter of the method, Android gets the parameter passed by JS by intercepting the URL.

This is used to realize the communication between native and JS.

GitHub source code

lzyzsd/JsBridge

My annotated jsbridge

Native calls JS code to pass data to JS end

Below is“Native passes data to JS and accepts JS callback data”Sequence diagram

sequenceDiagram
participant BridgeWebView.java as clientA
participant WebViewJavascriptBridge.js as serverA
participant demo.html as serverB

Note over clienta: native transfers data to JS
Clienta -- > > clienta: bridgewebview. Callhandler \ n ("functioninjs", \ n "native greetings to JS", \ n mcallbackfunction);
clientA-->>clientA: doSend(handlerName, data, responseCallback)
clientA-->>clientA: queueMessage(m) 
clientA-->>clientA: dispatchMessage(m)
Clienta - >
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-->>serverA: _fetchQueue()
serverA-->>clientA: reload iframe "yy://return/_fetchQueue/[{"data"}]"
clientA-->>clientA: handlerReturnData(String url)
Clienta -- > > clienta: oncallback in flushmessagequeue
clientA-->>clientA: mCallBackFunction.onCallBack(responseData)

BridgeWebView.java

Callhandler (“functioninjs”, “native greetings to JS”, mcallbackfunction);

/**
     *Native calls JS
     * <p>
     * call javascript registered handler
     *Call JavaScript handler registration
     *
     *Handlername registered in @ param handlername JS
     *@ param data native data passed to JS
     *After processing @ param callback JS, call back to native
     */
    public void callHandler(String handlerName, String data, CallBackFunction callBack) {
        doSend(handlerName, data, callBack);
    }

The notes are complete. Let’s look at the notes instead of explaining them

BridgeWebView.java

doSend(handlerName, data, responseCallback)

/**
     *Native calls JS
     * <p>
     *Save message to message queue
     *
     *Handlername registered in @ param handlername JS
     *@ param data native data passed to JS
     *@ param responsecallback JS after processing, callback to native
     */
    private void doSend(String handlerName, String data, CallBackFunction responseCallback) {
        LogUtils.e(TAG, "doSend——>data: " + data);
        LogUtils.e(TAG, "doSend——>handlerName: " + handlerName);
        //Create a message body
        Message m = new Message();
        //Add data
        if (!TextUtils.isEmpty(data)) {
            m.setData(data);
        }
        //
        if (responseCallback != null) {
            //Create callback ID
            String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));
            //1. Used when JS calls back native data; key: ID value: callback (the corresponding callback method can be found through the callback ID returned by JS)
            responseCallbacks.put(callbackStr, responseCallback);
            //1. Used when JS calls back native data; key: ID value: callback (the corresponding callback method can be found through the callback ID returned by JS)
            m.setCallbackId(callbackStr);
        }
        //Method name registered in JS
        if (!TextUtils.isEmpty(handlerName)) {
            m.setHandlerName(handlerName);
        }
        LogUtils.e(TAG, "doSend——>message: " + m.toJson());
        //Add message or distribute message to JS
        queueMessage(m);
    }
  • Made aMessageTo encapsulate data data
  • A callbackid is created and the corresponding reference is stored in theMap<String, CallBackFunction> responseCallbacksIn this way, when JS processing results are returned after JS processing, native can find the corresponding callbackfunction through the callbackid to complete the data callback.
/**
     * BridgeWebView.java
     *List < message >! = null is added to the message collection or the message will be distributed
     *
     * @param m Message
     */
    private void queueMessage(Message m) {
        LogUtils.e(TAG, "queueMessage——>message: " + m.toJson());
        if (startupMessage != null) {
            startupMessage.add(m);
        } else {
            //Distribute messages
            dispatchMessage(m);
        }
    }

    /**
    * BridgeWebView.java
     *The distribution message must be in the main thread before it can be successfully distributed
     *
     * @param m Message
     */
    void dispatchMessage(Message m) {
        LogUtils.e(TAG, "dispatchMessage——>message: " + m.toJson());
        //Convert to JSON string
        String messageJson = m.toJson();
        //Escape special characters for JSON string 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);

        LogUtils.e(TAG, "dispatchMessage——>javascriptCommand: " + javascriptCommand);
        //It is necessary to find the main thread to transfer the data out
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            //Call the "handlemessagefromnative" method in JS
            this.loadUrl(javascriptCommand);
        }
    }
  • In dispatchmessage, through loadjavascript:WebViewJavascriptBridge._handleMessageFromNative('%s');Pass message data to the “handlemessagefromnative” of JS method
//Native calls the handlemessagefromnative method in JS through loadurl (JS ﹣ handle ﹣ message ﹣ from ﹣ Java) to realize the transfer of data from native to JS
final static String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";

WebViewJavascriptBridge.js

//1. Receive the message from native
    //It is provided to the native call, and the receivemessagequeue will be null after the page is loaded, so
    function _handleMessageFromNative(messageJSON) {
        //
        console.log(messageJSON);
        //Add to message queue
        if (receiveMessageQueue) {
            receiveMessageQueue.push(messageJSON);
        }
        //Distribute native messages
        _dispatchMessageFromNative(messageJSON);
       
    }
  • Here, add the message sent by native to thereceiveMessageQueueArray.
//2. Distribute native messages
    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            //Parse message
            var message = JSON.parse(messageJSON);
            //
            var responseCallback;
            //java call finished, now need to call js callback function
            if (message.responseId) {
                ...
            } else {
                //There is a callbackid in the message, indicating that after the processing is completed, it needs to call back to the native side
                //Direct delivery
                if (message.callbackId) {
                    //Callback ID of the callback message
                    var callbackResponseId = message.callbackId;
                    //
                    responseCallback = function(responseData) {
                        //Send the response data of JS
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }

                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName];
                }
                //Find the specified 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);
    }
});
  • Here, call the functioninjs registration method of JS, and process the results of JSJavascript Says Right back aka!Return, call back to the responsecallback registered by webviewjavascriptbridge.js [dispatchmessagefromnative] to call into the [dosend] method of webviewjavascriptbridge.js.

Here is the “dosend” of webviewjavascriptbridge.js

WebViewJavascriptBridge.js

//Send the response data of JS
_doSend({
    responseId: callbackResponseId,
    responseData: responseData
});
//3. JS sends data to native
//SendMessage add message, trigger the shouldoverrideurlloading method of native to enable native to take the initiative to retrieve data from JS
//
//Can't you just put the message queue data in the URL of shouldoverrideurlloading?
//Why do you want native to take the initiative once and return it in the URL of shouldoverrideurlloading?
function _doSend(message, responseCallback) {
    //Sent data exists
    if (responseCallback) {
        //
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message.callbackId = callbackId;
    }
    //Add to message queue
    sendMessageQueue.push(message);
    //Let native load a new page
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
  • 1. Store the message data sent by native tosendMessageQueueIn message queue
  • 2. Reload iframe “YY: / / \

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)) {// if the data is returned
            webView.handlerReturnData(url);
            return true;
        } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
            webView.flushMessageQueue();
            return true;
        } else {
            return super.shouldOverrideUrlLoading(view, url);
        }
    }
  • _In dosend, reload iframe “YY: / / \
/**
     *1. Call the "fetchqueue" method of JS to get the message queue processed in JS.
     *In JS, the message data is returned to the native {@ link {bridgewebviewclient. Shouldoverrideurlloading} in the fetchqueue method
     * <p>
     *2. Wait for {@ link ා handlerreturndata} callback method
     */
    void flushMessageQueue() {
        LogUtils.d(TAG, "flushMessageQueue");
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            //Call the "fetchqueue" method of JS
            BridgeWebView.this.loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

                @Override
                public void onCallBack(String data) {
                    //... omitted here
                }
            });
        }
    }
  • flushMessageQueueA section of JS script is loaded in. JS fetch queue from Java. The following is the code of JS script.
//Call the "fetchqueue" method of JS. _The fetchqueue method returns message data to native's shouldoverrideurlloading
final static String JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();";
  • This JS script code callsWebViewJavascriptBridge.jsMedium_fetchQueueMethod.

WebViewJavascriptBridge.js

//Return data to native
//It is used to call native. The function is used to get sendmessagequeue and return it to native. Android can't get the returned content directly, so it uses the URL shouldouverrideurlloading to return the content
    function _fetchQueue() {
        //JSON data
        var messageQueueString = JSON.stringify(sendMessageQueue);
        //Message data clear
        sendMessageQueue = [];
        //Data returned to 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);
    }
  • Through reload iframe” yy://return/_fetchQueue/ + encodeURIComponent(messageQueueString)”Send data to native’sshouldOverrideUrlLoadingMethod.
/**
     *1. Get the callbackfunction data to call and remove it from the data set
     * <p>
     *2. Call back native {@ link ා flushmessagequeue()} callback method
     *
     * @param url
     */
    void handlerReturnData(String url) {
        LogUtils.d(TAG, "handlerReturnData——>url: " + url);
        //Get the method name of JS
        // _fetchQueue
        String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
        //Get the callback method corresponding to "fetchqueue"
        CallBackFunction f = responseCallbacks.get(functionName);
        //Get body message body
        String data = BridgeUtil.getDataFromReturnUrl(url);

        //Call back the native flushmessagequeue callback method
        if (f != null) {
            LogUtils.d(TAG, "onCallBack data" + data);
            f.onCallBack(data);
            responseCallbacks.remove(functionName);
            return;
        }
    }
  • The callbackfunction here calls back to oncallback in the flushmessagequeue method.
@Override
public void onCallBack(String data) {
    LogUtils.d(TAG, "flushMessageQueue——>data: " + data);
    //Deserializemessage deserialize message
    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();
        /**
         *Callback after native sends information to JS
         */
        //Response callbackfunction or not
        if (!TextUtils.isEmpty(responseId)) {
            CallBackFunction function = responseCallbacks.get(responseId);
            String responseData = m.getResponseData();
            function.onCallBack(responseData);
            responseCallbacks.remove(responseId);
        } else {
            //... omitted here
        }
    }
}
  • Here, the message queue obtained from JS is recycled, and the data obtained from JS is recalled to the corresponding message queue in nativeCallBackFunctionMedium.

Here, the communication of native calling JS code in jsbridge is completed.

One problem

In the “dosend (message, responsecallback) method of webviewjavascriptbridge.js, set theMessage message queueput toURL of shouldoverrideurlloadingCan’t we just go back to native?

Why use reload iframe “YY: / / \?

Personal opinion:
I feel that in this way, messages are gathered together. By sending a message to native, native actively requests all data back. The frequent interaction between JS and native is avoided.

JS calls native code to pass data to native

I don’t want to say much. Let’s get here