Overview
Developers who have been involved in hybrid development are familiar with frameworks such as Ionic and PhoneGap. These frameworks cover a web-based app in a Native layer and then call the local library through the JSBridge technology.
Before diving into the details of JSBridge technology, let us briefly explore the traditional implementation process.
Android Environment
Calling JS in Native
Calling JS in Native is relatively simple: you just need to observe the rule of "javascript: method name ('parameter, needed to be converted to string')
".
Call method before Android 4.4:
// mWebView = new WebView(this);
mWebView.loadUrl("javascript: method name ('parameter, needed to be converted to string')");
//Run in UI thread
runOnUiThread(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript: method name ('parameter, needed to be converted to string')");
Toast.makeText(Activity name.this, "call method...", Toast.LENGTH_SHORT).show();
}
});
Call method for Android 4.4 and newer:
mWebView.evaluateJavascript("javascript: method name ('parameter, needed to be converted to string')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//Here the value refers to the returned value of the corresponding JS method
}
});
Description:
- Before Android 4.4, Native calls JS method through loadUrl. In this approach, only a JS method is executed, but the returned value of this method cannot be obtained.
- For Android 4.4 and newer versions, the JS methods are called asynchronously through evaluateJavascript and the returned value is available in onReceiveValue.
- This method is not suitable for transmission of large amounts of data (the interface approach is recommended for a large amount of data).
- mWebView.loadUrl("javascript: The mWebView.loadUrl ("javascript:method name ('parameter, needed to be converted to string'")") function needs to be run in the UI thread because mWebView is a UI control.
Calling Native in JS
The call of Native in JS requires the @JavascriptInterface annotation to the WebView. There is a loophole, which will be explained later. To make JS native, you need to set the following attributes for WebView:
WebSettings webSettings = mWebView.getSettings();
//Android container allows JS scripts
webSettings.setJavaScriptEnabled(true);
//Android container sets the bridge object
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
Here we see getJSBridge()
. In Native, addJavascriptInterface
is used to add an exposed JS bridge object, and then the corresponding API method is declared inside the object.
private Object getJSBridge(){
Object insertObj = new Object(){
@JavascriptInterface
public String foo(){
return "foo";
}
@JavascriptInterface
public String foo2(final String param){
return "foo2:" + param;
}
};
return insertObj;
}
Calling the Native method in HTML.
//Call method 1
window.JSBridge.foo(); //Returned: 'foo'
//Call method 2
window.JSBridge.foo2('test');//Returned: 'foo2:test'
Description:
- In Android 4.2 (API17) and newer versions, the exposed API should carry an
@JavascriptInterface
annotation. Otherwise, the method will not be found. - Before API17,
addJavascriptInterface
has a potential security risk. Hackers can get the Native-registered JS object by decompiling and obtaining sensitive information, and then launching attacks by reflecting Java's built-in static classes on the page. - JS calls Native-exposed API and gets the corresponding returned value.
iOS Environment
Calling JS in Native
The method to call JS in Native is relatively simple. Native calls the function that HTML binds to the window through stringByEvaluatingJavaScriptFromString. Note the OC and Swift writing styles.
//Swift
webview.stringByEvaluatingJavaScriptFromString("method name (parameter)")
//OC
[webView stringByEvaluatingJavaScriptFromString:@"method name (parameter);"];
Description:
- When you call JS methods in Native, you can get the returned value of the JS method.
- This method is not suitable for transmission of large amounts of data (the interface approach is recommended for a large amount of data).
Calling Native in JS
In Native, the API can be bound to JSContext through the introduction of the official JavaScriptCore library (iOS7 and above) (and then JS can be called in HTML through window.top. *).
Introduce the official library file
#import <JavaScriptCore/JavaScriptCore.h>
Native registers the API function (OC)
-(void)webViewDidFinishLoad:(UIWebView *)webView{
[self hideProgress];
[self setJSInterface];
}
-(void)setJSInterface{
JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// API method registered with a name of "foo"
context[@"foo"] = ^() {
//Get the parameter
NSArray *args = [JSContext currentArguments];
NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
//Do some logic of your own
//A value is returned: 'foo:'+title
return [NSString stringWithFormat:@"foo:%@", title];
};
}
Calling Native methods in JS in HTML
window.top.foo('test');
Description:
- This approach was not available until iOS 7. Prior to iOS7, JS could not call Native directly. JS can call Native indirectly only through JSBridge.
- JS can call the exposed API and get the corresponding returned value.
- Native iOS cannot be called by JS, but APIs can be made open to JS calls by introducing the official third-party "JavaScriptCore".
What is JSBridge?
As its name implies, JSBridge serves as a bridge between JS and Native. In fact, JSBridge, also known as the Hybrid app technology, is a communication method between JS and Native. In JSBridge, Native calls JS only through a fixed bridge object, and vice versa.
JSBridge Implementation Process
The process of JSBridge implementation can be summarized as follows. An H5 page triggers a URL in some way and Native captures the URL for analysis. Native then performs the processing and calls the H5 JSBridge object for transfer and callback.
Since the native WebView/UIWebView controls have already achieved data communication with JS, why do we still need JSBridge?
The following list describes the motivations for using JSBridge:
- The addJavascriptInterface method has security loopholes in the versions lower than Android 4.2.
- JS is unable to call Native in versions lower than iOS 7.
- The URL scheme interaction is a set of existing mature solutions that are perfectly compatible with various versions and with the technology of older versions.
URL Scheme
URL scheme is a link, like the URL, designed to facilitate direct mutual calls between apps. You can open the system application for a system URL scheme. Otherwise, you can try to find whether an app has registered such a scheme and open that corresponding app.
Note: A scheme will be valid only after native app registration is completed.
However, in an actual development scenario, the app does not register the corresponding scheme. Instead, the front-end page triggers the scheme (such as iframe.src) in some way. Native captures the corresponding URL to trigger the event and gets the current triggering URL, and then Native checks if the method is triggered according to the defined protocol.
JSBridge: Technical Implementation
To implement JSBridge, we need to analyze the following steps:
- Step 1: Design a global bridge object for Native to interact with JS
- Step 2: How JS calls Native
- Step 3: How Native knows that the API is called
- Step 4: URL-parameter and callback format
- Step 5: How Native calls JS
- Step 6: Registration and format of the API method in H5
The figure below shows the complete process of JSBridge:
Designing Global Bridge Objects for Interaction Between Native and JS
We stipulate that the communication between JS and Native must be implemented through an H5 global object – JSBridge. The object has the following characteristics:
The object name is "JSBridge", which is an attribute of the global object window in the H5 page. It is elaborated as:
var JSBridge = window.JSBridge || (window.JSBridge = {});
This object has the following methods:
- registerHandler (String, Function) H5 call. It registers the local JS method, after which Native can make the call through JSBridge. After the call, the method is registered in the local variable messageHandlers.
- callHandler (String, JSON, Function) H5 call. It calls the Native open API, after which the URL scheme is triggered locally. During the call, the callback ID is stored in the local variable responseCallbacks.
- _handleMessageFromNative (JSON) Native call. Native calls the method registered on the H5 page, or notifies the H5 page of the execution of the callback method.
Calling Native in JS
After we define the global bridge object, we can use its callHandler method to call the Native API.
Internal implementation process of the callHandler function
The callHandler execution includes the following internal steps:
1.Determine whether a callback function exists. If yes, a callback function ID is generated, and the ID and the corresponding callback is added to the callback function set responseCallbacks.
2.The input data and method name are spliced into a URL scheme through a specific parameter conversion method.
//The URL scheme format
//Basically the useful information is the callbackId, handlerName and data at the end
//Native will analyze the scheme after it captures it
var uri = CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data
3.A internal hidden which has long been ready iframe is used for triggering the scheme
//Process of creating the hidden iframe
var messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
document.documentElement.appendChild(messagingIframe);
//Trigger the scheme
messagingIframe.src = uri;
Note: Normally it is possible to initiate a network request through window.location.href. However, there is a very serious problem when we change the value of window.location.href several times in succession: the Native layer can only receive the last request, while ignoring all the previous ones. To avoid this problem, we need iframe to initiate network requests at the JS end.
Native Notifying the API to be Called
In our previous step, we successfully triggered the scheme on the H5 page.
Our next step is to explore how Native captures the scheme triggering event.
Android and iOS have their respective handling approaches.
Android
In Android (in WebViewClient), the shouldoverrideurlloading
helps to capture the URL scheme triggering event.
public boolean shouldOverrideUrlLoading(WebView view, String url){
//If false is returned, WebView handles the link URL. If true is returned, WebView executes the URL following the procedures
return true;
}
iOS
iOS, UIWebView has a unique feature: all the network requests initiated in the UIWebView can be notified to the Native layer through the delegate function. In this way, we can capture the URL scheme triggering event in WebView (the principle is to use shouldStartLoadWithRequest
).
- (BOOL)webView:(UIWebView )webView shouldStartLoadWithRequest:(NSURLRequest )request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];
NSString *requestString = [[request URL] absoluteString];
//Get the URL scheme for self-processing
Analyzing URL-parameter and Callback Format
In previous steps, Native has received a JS call method. In the next step, Native should parse the data according to the defined data formats. Native can extract the callback parameter ID, API name, and parameters following this format after receiving the URL, and then follow the steps below.
1. Search for the corresponding API method locally according to API name, and record the callback function ID after the method is executed.
2. Convert the extracted parameters according to the defined parameters.
3. Native executes the corresponding API function method locally.
4. After function execution, find the corresponding callback function ID of this API call, and then assemble the information with the parameter information to be passed into a JSON format parameter.
5. Notify the H5 page for callback via JSBridge.
Calling JS in Native
In this step, Native calls H5 JS methods through JSBridge or notify H5 for callback. The messageJSON data format in it has two different types.
JSBridge._handleMessageFromNative(messageJSON);
Native notifies H5 page for callback:
As per the data format, Native notifies H5 about the callback JSON format.
Native takes the initiative to call the H5 method:
When Native takes the initiative to call H5 methods, the data format is: {handlerName: the API name, data: data, callbackId: the callback ID}:
- handlerName String type: the open API name to be called in H5
- data JSON type: the data to be passed. Its format is fixed to be JSON (because we fix the format of the first parameter received by methods registered in H5 to JSON, and the second to a callback function)
- callbackId String type: the callback function ID generated by Native. After H5 execution, the URL scheme is used to notify Native about the successful execution of the API and to pass the parameter.
Registration and Format of the API Method in H5
Previously we mentioned that Native takes the initiative to call the API methods registered in H5.
Let us now study, how we can register API methods in H5 for the Native to call.
JSBridge.registerHandler('testH5Func',function(data,callback){
alert('Test data received by the function:'+JSON.stringify(data));
callback&&callback('Test the postback data...');
});
As indicated in the code above, the first data is the data passed by the Native. The second callback is encapsulated once internally. After the callback is executed, it triggers the URL scheme and notifies the Native to get the callback information.
Improving the JSBridge Scheme
The diagram below shows a JSBridge object.
The following diagram illustrates the full process of JSBridge implementation.
JSBridge in iOS and Android
In an actual development, how can we develop a unified scheme for different situations in both Android and iOS?
The JSBridge mentioned above is based on the URL scheme. However, if you do not plan to support versions lower than Android 4.2 and iOS 7, you must consider another scheme.
- Native calls of JS methods remain unchanged
- JS calls of Native are no longer through triggering the URL scheme, but through the built-in interaction. Specifically, in Android, Native opens a unified API for JS to call through addJavascriptInterface. Then the URL scheme triggering step is changed to call of the API while the other steps remain the same.
In iOS, Native registers a unified API through methods in JavaScriptCore, and the other steps are the same with those in Android.
Conclusion
Javascript Bridge is a unified messaging system that enables easy building of cross-language services that allow you to share data and real-time updates among your servers and clients. This article explores in detail the profiling of JSBridge and its implementation process in both iOS and Android respectively.