JSBridge is nothing new in Android, and implementations vary slightly. Most people know that there is a bug in WebView, see WebView Interface vulnerability and Mobile Mount Exploitation, although this bug has been fixed in Android 4.2, namely using @javascriptInterface instead of addJavascriptInterface. However, due to compatibility and security issues, we basically will not use the addJavascriptInterface method or @javascriptInterface annotation provided by Android system to implement, so we have to find a new way to find the security, And can achieve compatible Android each version of the scheme.

First of all, let’s understand why JSBridge should be used. In the development, in order to pursue the efficiency of development and the convenience of transplantation, we prefer to use H5 for some pages with strong display, and native for some pages with strong function. Once h5 is used, In order to get as much native experience as possible in H5, our native layer needs to expose some methods to JS calls, such as Toast reminders, Dialog and sharing, etc. Sometimes, we even leave H5 network requests to Native to complete. A typical example of JSBridge’s good performance is wechat. Wechat provides developers with JSSDK, which exposes many methods of wechat Native layer, such as payment, positioning, etc.

So, how to implement a compatible Android version of JSBridge with a certain degree of security? LoadUrl (” javascript:function() “). In this way, the native layer of JSBridge can call the H5 layer one-way communication. But how does h5 layer tune native layer, we need to look for such a channel, carefully recall, WebView has a method called setWebChromeClient, you can set the WebChromeClient object, and this object has three methods, OnJsAlert, onJsConfirm, onJsPrompt when js call window object corresponding method, namely the window. The alert, window. Confirm that Windows. Prompt, The three methods in the WebChromeClient object will be triggered. Can we use this mechanism to do some processing ourselves? The answer is yes.

See the W3C JavaScript message box for the differences between these three methods. In general, we don’t use onJsAlert, why? Because the frequency of using alert in JS is still very high, once we occupy this channel, the normal use of alert will be affected, while the frequency of using confirm and prompt is lower than that of alert. In fact, the frequency of use of confirm is not low. For example, when you click a link to download a file, if you need to pop up a prompt for confirmation, click OK and the file will be downloaded, while click cancel and the file will not be downloaded. There are many similar scenarios. Therefore, confirm cannot be used. Prompt, on the other hand, is almost never used in Android, and if it is, it is customized, so you can use it. The idea is to pop up an input box, ask you to type it, and then return what’s in the input box. Therefore, the occupation prompt is perfect.

At this point, we have found a channel for JSBridge two-way communication, and it is now up to us to implement it. The implementation in this article is a simple demo that needs to be wrapped if you want to use it in a production environment.

To carry on the normal communication, the communication protocol formulation is essential. Let’s recall the familiar components of an HTTP request URL. Like http://host:port/path? Param =value, we refer to HTTP to formulate the components of JSBridge. What information does our JSBridge need to transmit to Native so that the Native layer can complete corresponding functions and then return the results? Obvious we native layer to accomplish a function is a need to call a class method, we need to transfer the name of the class and method names in the past, in addition, you also need to method calls the required parameters, in order to convenient communication, native methods we stipulated as a json object, the parameters needed in our js pass this json object in the past, The Native layer takes this object and parses it. To differentiate ourselves from the HTTP protocol, our JsBridge uses the JsBridge protocol. For simplicity, we don’t apply key-value pairs after the question mark, so we just follow our JSON string, and we have this URI like this

jsbridge://className:port/methodName? jsonObjCopy the code

After the js layer calls the Native layer method, Native needs to return the result of the execution to the JS layer. However, you might think that the onJsPrompt method of the WebChromeClient object should return the return value to js. The process is synchronous. If native performs an asynchronous operation, how does the return value come back? At this time, port plays its due role. When we call the native method in JS, we register a callback in JS, and then cache the callback in the specified location. Then, after the native layer executes the corresponding method, it invokes the method in JS through webView. loadUrl and calls the corresponding callback. So how does JS know which callback to call? Therefore, we need to pass a storage location of callback, so the native layer needs to send the storage location back to JS when calling the method in JS, and THEN JS calls the callback at the corresponding storage location for callback. Thus, the complete protocol is defined as follows:

jsbridge://className:callbackAddress/methodName? jsonObjCopy the code

Suppose we need to call the log method of the Logger class in the native layer. Of course, this class and method must follow some specification, and 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, and the JS layer needs a callback after execution, so the address is as follows

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

The address of the callback object can be stored in the window object in js. As for how to store it, I will slowly reverse it later.

The above is the communication protocol from JS to Native, so on the other hand, the communication protocol from Native to JS also needs to be formulated. An essential element is the return value, which is transmitted through json object just like the parameter of JS. The JSON object contains the status code and the prompt message MSG, If code is non-0, an error occurs during execution. The error message is in MSG, and the returned result is null. If the execution succeeds, the returned JSON object is in result. Here are two examples, one successful and one failed.

{"code":500,"msg":"method is not exist","result":null
}Copy the code

{"code":0,"msg":"ok","result":{"key1":"returnValue1","key2":"returnValue2","key3":{"nestedKey":"nestedValue"
            "nestedArray": ["value1"."value2"]
        }
    }
}Copy the code

Then how to return this result? Native calls the method exposed by JS, and then passes the JS layer to the port of native layer along with it to make the call. The call method is completed through webView. loadUrl, as follows.

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

The implementation of the JsBridge. OnFinish method is described below. We mentioned earlier that native layer methods must conform to some specification or they are very insecure. In Native, we need a JSBridge to manage these classes and methods exposed to JS in a unified manner and add them in real time. At this time, such a method is needed

JSBridge.register("jsName",javaClass.class)Copy the code

A javaClass is a class that satisfies a specification. It has methods that satisfy the specification. We specify that this class needs to implement an empty interface. The main function is to avoid errors when obfuscating, and to constrain the jsbridle. register method that the second parameter must be the implementation class of the interface. So let’s define this interface

public interface IBridge{
}Copy the code

We also need to specify that the method in the class must be static, so that we can call directly from the class instead of creating a new object (and be public), and that the method does not have a return value, because the return value is returned in a callback, and since there is a callback, Callback is a json object. In the Java layer, we define it as a JSONObject. The result of a method execution needs to be passed back through a callback, whereas Java execution of a JS method requires a WebView object, so a method prototype that meets a specification is created.

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

}Copy the code

JSBridge. OnFinish (port,jsonObj); Method is used for callbacks. There should also be a method that provides the ability to call native methods, which is modeled as follows

JSBridge.call(className,methodName,params,callback)Copy the code

In the call method, the parameters are then combined into a URI in the form shown below

jsbridge://className:callbackAddress/methodName? jsonObjCopy the code

The window.prompt method is then called to pass in the URI, which is then received by the Java layer for further parsing.

Everything has, only owe how to code, don’t worry, the following we step by step to achieve, the first two methods to complete JS. Create a new file and name it 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(a);
            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 (a) {
            return Math.floor(Math.random(a) * (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);Copy the code

GetPort () is used to generate port randomly, getParam() is used to generate JSON string, and getUri() is used to generate protocol URIs required by Native. It is mainly used for string concatenation. Then we have an Inner class with our call and onFinish methods. In the call method, we call util.getPort () to get the port value and store the callback object at port in the Callbacks. Then call util.geturi () to pass the argument, assign the returned result to the URI, and call window.prompt(uri, “”) to pass the URI to the native layer. The onFinish() method, on the other hand, accepts the port value returned from Native and the result of execution. Based on the port value, the original callback function is retrieved from the callbacks, executed, and removed from the callbacks. Finally, the functions in the Inner class are exposed to the external JSBrige object through a for loop.

Of course, this implementation is the most simple implementation, the actual situation to consider too many factors, because I am not very proficient in JS, so can only write JS in Java thought, did not consider the factors let’s ignore it, such as memory recycling and so on mechanism.

This completes the coding of the JS layer and then implements the coding of the Java layer.

The Java layer has an empty interface to constrain the exposure of JS classes and methods, as well as to facilitate confusion

public interface IBridge {
}Copy the code

First we need to get the URI from JS and write 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; }}Copy the code

Then don’t forget to set that object to the WebView

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

Here comes the core, which is the implementation of the JSBridge class called in JSBridgeWebChromeClient. As mentioned earlier, there is a method in this class that provides registration of classes and methods exposed to JS

JSBridge.register("jsName",javaClass.class)Copy the code

Public static void public static void public static void public static void public static void public static void public static void public static void Callback, if the condition is met, all the methods that meet the condition are put in, the whole implementation is as follows

public class JSBridge {
    private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>();

    public static void register(String exposedName, Class<? extends IBridge> clazz) {
        if(! exposedMethods.containsKey(exposedName)) {try {
                exposedMethods.put(exposedName, getAllMethod(clazz));
            } catch(Exception e) { e.printStackTrace(); }}}private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
        HashMap<String, Method> 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] == Callback.class) { mMethodsMap.put(name, method); }}}returnmMethodsMap; }}Copy the code

As for the callJava method in the JSBridge class, it parses the URI from the JS class and looks for it in the map based on the class name alias. If it does exist, it gets a methodMap for all the methods in the class. Then it gets a method from the methodMap based on the method name and reflects the call. The WebView, JSONObject, and Callback parameters are the three parameters that meet the criteria mentioned above.

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<String, Method> 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;
    }Copy the code

This method uses new Callback(webView, port) to create a new object, which is the Java class that calls back and forth to the JS Callback method. In this class you need to pass in the port from js as well as the reference to the WebView, because you need to use the loadUrl method of the WebView. In order to prevent memory leaks, we use weak references here. If you need to call the js callback, call callback.apply() in the corresponding method and 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<WebView> 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); }}); }}}Copy the code

The only thing to notice is that I left the apply method on the main thread. Why, because methods exposed to JS might call this callback in a child thread, which would give an error, so I cut it back to the main thread inside the method.

Code completion about, then implements IBridge will be left, we have a simple, just as well to show Toast as an example, shows the js callback, although there is no point in this callback.

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; }}Copy the code

You can throw any method you want into the class, but it must be public static void and the argument list meets the criteria to find the method.

Don’t forget to register the class

JSBridge.register("bridge", BridgeImpl.class);Copy the code

To do a simple test, throw the jsbridge. js file you implemented into assets, create a new index.html file, and type


       
<html>
<head>
    <meta charset="utf-8">
    <title>JSBridge</title>
    <meta name="viewport"
          content="Width =device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"/>
    <script src="file:///android_asset/JSBridge.js" type="text/javascript"></script>
    <script type="text/javascript">

    </script>
    <style>

    </style>
</head>

<body>
<div>
    <h3>JSBridge test</h3>
</div>
<ul class="list">
    <li>
        <div>
            <button onclick="JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})">Test showToast</button>
        </div>
    </li>
    <br/>
</ul>
</body>
</html>
Copy the code

The jsbridge.call () method is called when the button is clicked, and the callback function is the result of an alert.

The next step is to test the index.html file load using the WebView

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

The effect is shown below

You can see that the whole process is working, and then we test the subthread callback, adding 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();
    }Copy the code

Add to index.html

<ul class="list">
    <li>
        <div>
            <button onclick="JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})">Test the child thread callback</button>
        </div>
    </li>
    <br/>
</ul>Copy the code

Ideally, the callback should bring up an Alert 3 seconds later

It’s perfect, it doesn’t have much code, and it does the job. If you need to use the build environment, you must wrap the above code, because I simply implemented the function, other factors did not consider too much.

Of course, you can also refer to the Safe Java-JS WebView Bridge, an open source implementation

And finally, the convention, the code

Download.csdn.net/detail/sbsu…