Principles and implementation of Android JSBridge

Source: Internet
Author: User

Principles and implementation of Android JSBridge

In Android, JSBridge is no longer a new thing, And the implementation methods vary slightly. Most people know that WebView has a vulnerability. For more information, see WebView interface hidden danger and mobile phone disconnection. Although this vulnerability has been fixed on Android 4.2@ JavascriptInterfaceReplaceAddJavascriptInterfaceHowever, due to compatibility and security issues, we will not use the Android system to provideAddJavascriptInterfaceMethod or@ JavascriptInterfaceAnnotations, so we can only find another way to find a solution that is both secure and compatible with various Android versions.

First, let's take a look at why JSBridge is used. In the development process, we tend to use h5 for some highly explicit pages in pursuit of development efficiency and convenience of porting, for highly functional pages, we prefer native. Once h5 is used, in order to get the native experience as much as possible in h5, we need to expose some methods to js calls on the native layer, such as the Toast reminder, Dialog, and sharing. Sometimes we even put the h5 network request in native to complete it, A typical example of JSBridge isThe SDK provides developers with JSSDK, which exposes many native layer methods, such as payment and positioning.

So how can we implement a JSBridge that is compatible with various Android versions and has certain security? We know that in WebView, it is very easy to use java to call js methods.WebView. loadUrl ("javascript: function ()")In this way, the native layer of JSBridge calls one-way communication on the h5 layer. But how can we call the native layer on the h5 layer? We need to find such a channel and recall it carefully, webView has a method calledSetWebChromeClient, You can setWebChromeClientObject, which has three methods:OnJsAlert,OnJsConfirm,OnJsPromptWhen js callsWindowThe corresponding method of the object, that isWindow. alert,Window. confirm,Window. prompt, The three methods in the WebChromeClient object will be triggered. Can we use this mechanism to do some processing on our own? The answer is yes.

For the differences between the three methods of js, see the w3c JavaScript message box. Generally, we will not use onJsAlert. Why? Because alert is frequently used in js, once we use this channel, normal use of alert will be affected, and the usage frequency of confirm and prompt is relative to that of alert, A little lower. So whether to choose confirm or prompt? In fact, the usage frequency of confirm is not low. For example, if you click a link to download a file, a prompt will pop up to confirm, click "OK" to download the image. Click "cancel" to cancel the image. There are many similar scenarios, so you cannot use confirm. The prompt is different. In Android, this method is almost never used. It is used or customized. Therefore, we can use this method completely. This method is to pop up an input box and let you enter it. After the input is complete, the content in the input box is returned. Therefore, it is perfect to use prompt.

In this step, we have found a channel for two-way JSBridge communication, and the next step is how to implement it. The implementation in this article is just a simple demo. If you want to use it in the production environment, you also need to encapsulate it yourself.

Communication protocols are essential for normal communication. Let's look back at the components of the familiar http request url. Such as http: // host: port/path? Param = value. Let's refer to http to define the components of JSBridge. What information does our JSBridge need to pass to native so that the native layer can complete the corresponding functions and then return the results? Obviously, to complete a function on the native layer, we need to call a method of a class. We need to pass this class name and method name. In addition, we need to call the required parameters for the method, for convenience of communication, the parameters required by the native method are defined as json objects. we pass this json object in js, and the native layer obtains this object and then parses it. To distinguish it from the http protocol, our jsbridge uses the jsbridge protocol. For the sake of simplicity, the question mark does not apply to key-value pairs. We keep up with our json string directly, so we have the uri shown below

jsbridge://className:port/methodName?jsonObj

Someone may ask why this port is used. After the js layer calls the native layer method, native needs to return the execution result to the js layer, however, you will feel that it is not good to return the returned value to js through the onJsPrompt method of the WebChromeClient object. Otherwise, if so, the process is synchronous. If native executes asynchronous operations, how can I return the returned value? At this time, port plays its due role. When we call the native method in js, register a callback in js, and then cache the callback at the specified position, then, the native layer executes the corresponding method and uses WebView. loadUrl calls the method in js and calls back the corresponding callback. So how does js know which callback to call? So we need to pass a storage location of callback to the past, so we need the native layer to call the method in js to send the storage location back to js, and js then calls the callback at the corresponding storage location,. Therefore, the complete Protocol definition is as follows:

jsbridge://className:callbackAddress/methodName?jsonObj

Suppose we need to call the log method of the Logger class at the native layer. Of course, this class and method must follow certain specifications. Not all java classes can call it, otherwise, it will be the same as the WebView vulnerability at the beginning of the article. The parameter is msg. After the execution is complete, the js layer must have a callback, so the address is as follows:

jsbridge://Logger:callbackAddress/log?{"msg":"native log"}

The address of this callback object can be stored in the window object in js. As for how to store the data, the following article will be slow down.

The above is the communication protocol from js to native. On the other hand, the communication protocol from native to js also needs to be developed. An essential element is the return value. The return value is the same as that of js parameters, passed through the json object, which containsStatus code,Message msg, AndReturned resultIf the code is not 0, an error occurs during execution. The error message is in msg and the returned result is null. If the execution is successful, the returned json object is in result. The following are two examples: one successful call and one failed call.

{    "code":500,    "msg":"method is not exist",    "result":null}
{    "code":0,    "msg":"ok",    "result":{        "key1":"returnValue1",        "key2":"returnValue2",        "key3":{            "nestedKey":"nestedValue"            "nestedArray":["value1","value2"]        }    }}

Then how can this result be returned? native can call the method exposed by js, and then attach the port from the js layer to the native layer for calling. The method of calling is through WebView. the loadUrl method is as follows.

mWebView.loadUrl("javascript:JSBridge.onFinish(port,jsonObj);");

The implementation of the JsBridge. onFinish method is described later. We mentioned earlier that the native layer method must follow certain specifications, otherwise it will be very insecure. In native, we need a JSBridge to centrally manage the classes and methods exposed to js and add them in real time. This method is required at this time.

JSBridge.register("jsName",javaClass.class)

This javaClass is a class that meets certain specifications. There are methods that meet the specifications in this class. We stipulate that this class needs to implement an empty interface. Why? The main function is to avoid errors when obfuscation occurs. The second parameter of the JSBridge. register method must be the implementation class of this interface. Then we define this interface

public interface IBridge{}

The class is well defined. We also need to specify the methods in the class. For the convenience of calling, we stipulate that the methods in the class must be static, in this way, the class is called directly without creating a new object (or public), and then the method does not have a return value, because the return value is returned in the callback. Since there is a callback, in the parameter list, there must be a callback. Besides callback, there are also the parameters required for calling methods from js mentioned above. They are a json object. In the java layer, we define them as JSONObject objects; the execution result of the method needs to be passed back through callback, while the java js method requires a WebView object. Therefore, a method prototype that meets certain specifications will come out.

public static void methodName(WebView web view,JSONObject jsonObj,Callback callback){}

In addition to the JSBridge. onFinish (port, jsonObj) mentioned above in the js layer; the method is used for callback. There should be another method to call the native method. The prototype of this function is as follows:

JSBridge.call(className,methodName,params,callback)

In the call method, parameters are combined to form a uri in the following format.

jsbridge://className:callbackAddress/methodName?jsonObj

Then, call the window. prompt method to pass the uri. At this time, the java layer will receive the uri and parse it further.

Everything is ready. I only need to code it. Don't worry. Let's implement it step by step. We will first complete two js methods. Create a new file named JSBridge. js

(function (win) {    var hasOwnProperty = Object.prototype.hasOwnProperty;    var JSBridge = win.JSBridge || (win.JSBridge = {});    var JSBRIDGE_PROTOCOL = 'JSBridge';    var Inner = {        callbacks: {},        call: function (obj, method, params, callback) {            console.log(obj+" "+method+" "+params+" "+callback);            var port = Util.getPort();            console.log(port);            this.callbacks[port] = callback;            var uri=Util.getUri(obj,method,params,port);            console.log(uri);            window.prompt(uri, "");        },        onFinish: function (port, jsonObj){            var callback = this.callbacks[port];            callback && callback(jsonObj);            delete this.callbacks[port];        },    };    var Util = {        getPort: function () {            return Math.floor(Math.random() * (1 << 30));        },        getUri:function(obj, method, params, port){            params = this.getParam(params);            var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;            return uri;        },        getParam:function(obj){            if (obj && typeof obj === 'object') {                return JSON.stringify(obj);            } else {                return obj || '';            }        }    };    for (var key in Inner) {        if (!hasOwnProperty.call(JSBridge, key)) {            JSBridge[key] = Inner[key];        }    }})(window);

We can see that there is an Util class in it, which has three methods, getPort () is used to generate a random port, getParam () is used to generate a json string, getUri () used to generate the protocol uri required by native, which is mainly used for String concatenation. Then there is an Inner class which contains our call and onFinish methods. In the call method, we call Util. getPort () obtains the port value, stores the callback object in the port position of callbacks, and then calls Util. getUri () passes the parameter over, assigns the returned result to uri, and CALLS window. prompt (uri, "") transmits the uri to the native layer. The onFinish () method accepts the port value and execution result returned by native, obtains the original callback function from callbacks Based on the port value, executes the callback function, and then deletes it from callbacks. Finally, expose the functions in the Inner class to the external JSBrige object and assign values one by one through a for loop.

Of course, this implementation is the most simple implementation. There are too many factors to consider in the actual situation. Because I am not very proficient in js, I can only write js with the idea of java, ignore the factors that are not taken into account, such as memory collection and other mechanisms.

In this way, the js layer encoding is complete, and the java layer encoding is implemented.

As mentioned above, the java layer has an empty interface to expose constraints to js classes and methods, which is also easy to confuse.

public interface IBridge {}

First, we need to get the uri from js and compile a WebChromeClient subclass.

public class JSBridgeWebChromeClient extends WebChromeClient {    @Override    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {        result.confirm(JSBridge.callJava(view, message));        return true;    }}

Do not forget to set this object to WebView later

WebView mWebView = (WebView) findViewById(R.id.webview);WebSettings settings = mWebView.getSettings();settings.setJavaScriptEnabled(true);mWebView.setWebChromeClient(new JSBridgeWebChromeClient());mWebView.loadUrl("file:///android_asset/index.html");

The core content is the implementation of the JSBridge class called in JSBridgeWebChromeClient. As mentioned above, this class provides classes and methods for registration and exposure to js.

JSBridge.register("jsName",javaClass.class)

The implementation of this method is actually very simple. If the key exists from a Map and does not exist, the system returns all the methods in the corresponding Class. The method is of the public static void type, the parameters are of the Webview, JSONObject, and Callback types. If the conditions are met, all methods that meet the conditions are put. The entire implementation is as follows:

public class JSBridge {    private static Map
  
   > exposedMethods = new HashMap<>();    public static void register(String exposedName, Class
    clazz) {        if (!exposedMethods.containsKey(exposedName)) {            try {                exposedMethods.put(exposedName, getAllMethod(clazz));            } catch (Exception e) {                e.printStackTrace();            }        }    }    private static HashMap
   
     getAllMethod(Class injectedCls) throws Exception {        HashMap
    
      mMethodsMap = new HashMap<>();        Method[] methods = injectedCls.getDeclaredMethods();        for (Method method : methods) {            String name;            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {                continue;            }            Class[] parameters = method.getParameterTypes();            if (null != parameters && parameters.length == 3) {                if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == JSCallback.class) {                    mMethodsMap.put(name, method);                }            }        }        return mMethodsMap;    }}
    
   
  

As for the callJava method in the JSBridge class, the uri sent from js is parsed, and then the called class name alias is searched from the map to see if it exists, if it exists, get the methodMap of all the methods of this class, then get the method from methodMap according to the method name, reflect the call, and pass the parameter in. The parameter is the three parameters that meet the conditions described above, webView, JSONObject, and Callback.

public static String callJava(WebView webView, String uriString) {        String methodName = "";        String className = "";        String param = "{}";        String port = "";        if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {            Uri uri = Uri.parse(uriString);            className = uri.getHost();            param = uri.getQuery();            port = uri.getPort() + "";            String path = uri.getPath();            if (!TextUtils.isEmpty(path)) {                methodName = path.replace("/", "");            }        }        if (exposedMethods.containsKey(className)) {            HashMap
  
    methodHashMap = exposedMethods.get(className);            if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {                Method method = methodHashMap.get(methodName);                if (method != null) {                    try {                        method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));                    } catch (Exception e) {                        e.printStackTrace();                    }                }            }        }        return null;    }
  

We can see that new Callback (webView, port) is used in this method to create a new object. This object is the java class used to call back the Callback method in js. In this class, you need to pass in the js port and the WebView reference, because you need to use the WebView loadUrl method to prevent memory leakage, weak references are used here. If you need to call back the js callback, call the callback. apply () method in the corresponding method to pass in the returned data,

public class Callback {    private static Handler mHandler = new Handler(Looper.getMainLooper());    private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";    private String mPort;    private WeakReference
  
    mWebViewRef;    public Callback(WebView view, String port) {        mWebViewRef = new WeakReference<>(view);        mPort = port;    }    public void apply(JSONObject jsonObject) {        final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));        if (mWebViewRef != null && mWebViewRef.get() != null) {            mHandler.post(new Runnable() {                @Override                public void run() {                    mWebViewRef.get().loadUrl(execJs);                }            });        }    }}
  

The only thing to note is that I threw the apply method into the main thread for execution. Why? Because the method exposed to js may call this callback in the Child thread, in this case, an error will be reported, so I will switch it back to the main thread in the method.

After coding is almost complete, we can implement IBridge. Let's just show Toast as an example. After displaying it to js callback, although this callback has no significance.

public class BridgeImpl implements IBridge {    public static void showToast(WebView webView, JSONObject param, final Callback callback) {        String message = param.optString("msg");        Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();        if (null != callback) {            try {                JSONObject object = new JSONObject();                object.put("key", "value");                object.put("key1", "value1");                callback.apply(getJSONObject(0, "ok", object));            } catch (Exception e) {                e.printStackTrace();            }        }    }    private static JSONObject getJSONObject(int code, String msg, JSONObject result) {        JSONObject object = new JSONObject();        try {            object.put("code", code);            object.put("msg", msg);            object.putOpt("result", result);            return object;        } catch (JSONException e) {            e.printStackTrace();        }        return null;    }}

You can throw the required method to the class, but the method must be public static void and the parameter list meets the conditions.

Do not forget to register this class

JSBridge.register("bridge", BridgeImpl.class);

Perform a simple test, and then drop the existing jsbridge.js file to the assetsdirectory. Then, create index.html and enter

<Script src = "file: // android_asset/JSBridge. js "type =" text/javascript "> </script> <script type =" text/javascript "> </script> JSBridge Test
  • Test showToast

It is easy to call the JSBridge. call () method when the button is clicked. The callback function is the result returned by alert.

The access handler uses webviewto load the index.html file for testing.

mWebView.loadUrl("file:///android_asset/index.html");

Shows the effect.

We can see that the entire process has passed. Then we test the subthread callback and add the test method to BridgeImpl.

public static void testThread(WebView webView, JSONObject param, final Callback callback) {        new Thread(new Runnable() {            @Override            public void run() {                try {                    Thread.sleep(3000);                    JSONObject object = new JSONObject();                    object.put("key", "value");                    callback.apply(getJSONObject(0, "ok", object));                } catch (InterruptedException e) {                    e.printStackTrace();                } catch (JSONException e) {                    e.printStackTrace();                }            }        }).start();    }

Add in index.html

 
  • Test subthread callback

The ideal effect is to display alert in the callback pop-up after 3 seconds.

It is perfect, and there are not many codes, so we can implement the function. If you need to use the generated environment, you must encapsulate the above Code, because I just implemented the function, and other factors are not considered too much.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.