In e-commerce or content apps, H5 usually occupies a place, and communication between Native and H5 is essential. For example, H5 notifies Native to share some scenes, and Native notifies H5 to partially refresh. Android itself also provides such interfaces. Such as addJavascriptInterface, loadUrl(“javascript:…”) ), and the ability to require support is also duplex.
- 1: H5 notifies Native(which may need to handle callbacks),
- 2: Native notification H5 (may also need to handle callbacks)
There is no unique way to implement this mechanism, but improper use often introduces many problems, such as: H5 and Native require an intermediate JS file to implement a simple communication protocol. Some products use this js file to let the front-end load itself, and others use client injection, that is, loadUrl(“javascript:…”). ) injection. The client-side injection approach is somewhat problematic because there is no good time to ensure that the injection is both successful and timely. If you inject onPageStarted, many phones will fail to do so, and if you inject onPageFinished too late, many features will be compromised. Such as: Some people use Prompt to implement H5 notification to Native. Prompt is a synchronization method that may cause problems. Once it fails to return, the whole JS environment will hang and all H5 pages cannot be opened. The other is through prompt.
Solution a: with the help of the WebView. AddJavascriptInterface implementation to communicate with Native H5
WebView’s addJavascriptInterface method allows Natvive to inject Java objects into Web pages, which can then be accessed directly in JS using the @javascriptInterface annotated method. For example, inject a Java object named mJsMethodApi to the front end with the following code
*/ @javascriptInterface public void callNative(String jsonString) {... class JsMethodApi {/ @javascriptInterface public void callNative(String jsonString) {... } } webView.addJavascriptInterface(new JsMethodApi(), "mJsMethodApi");Copy the code
In the front-end JS code, Native can be notified directly via mJsMethodApi. CallNative (jsonString), and objects injected by addJavascriptInterface can be called anywhere in H5. There is no problem with injection timing or injection failure, it is ok to call in H5 head.
<head> <script type="text/javascript" > JsMethodApi. CallNative (' headers can be called back '); </script> </head>Copy the code
However, it should be noted that callNative is executed in the JavaBridge thread. Although the relationship between it and JS thread is not mentioned clearly, JS will block and wait for the completion of callNative function before proceeding. Therefore, it is best not to do time-consuming operations in the @javascriptInterface annotation method. It is best to use Handler encapsulation, let each task handle itself, if it is time-consuming to open the thread to handle itself.
What if the front end notifies Native with a callback? You can extract an intermediate JS, set an ID for each task, and temporarily store the callback function. After the Native processing is finished, you can go through this intermediate JS first and find the corresponding JS callback function to execute.
var _callbacks = {}; function callNative(method, params, success_cb, error_cb) { var request = { version: jsRPCVer, method: method, params: params, id: _current_id++ }; <! --> if (Typeof Success_cb! == 'undefined') { _callbacks[request.id] = { success_cb: success_cb, error_cb: error_cb }; } <! JsMethodApi --> JsMethodApi. CallNative (json.stringify (request)); };Copy the code
The above JS code completes the temporary storage of the callback and notifies Native of its execution. Native will receive the JS message, which contains the ID. After the execution of Native is completed, the execution result and message ID will be notified to this middle-layer JS, and the corresponding callback function can be found and executed as follows:
jsRPC.onJsCallFinished = function(message) { var response = message; <! Var success_cb = _callbacks[response.id].success_cb; <! --> delete _callbacks[response.id]; <! --> Success_cb (response.result); };Copy the code
In this way, H5 notifies Native, and Native sends the result back to H5 and completes the callback pathway. Native tells H5, what about this road? The process is similar, and the callback can also be completed based on a message ID. However, it is more flexible, because the interface of Native notification front end is not very uniform, so you can use your own specific control.
Reference project https://github.com/happylishang/CMJsBridge
Be careful not to confuse
If it is, the @javascriptInterface annotated method may be lost, and as a result, JS will not be able to call the corresponding method, resulting in communication failure.
About vulnerabilities
After 4.2, WebView will prohibit JS calls without adding @javascriptInterface method, which solves the security vulnerability, and few apps are compatible before 4.2, security issues can be ignored.
On the blocking problem
When a method injected by JavascriptInterface is called by JS, it can be regarded as a synchronous call. Although they are located in different threads, there should be a waiting notification mechanism to ensure that time-consuming operations should not be processed in the callback method in Native, otherwise JS will block and wait for a long time, as shown in the following figure
Scheme 2: Realize the communication between H5 and Native through Prompt
In addition, WebChromeClient also provides several js callback entrances, such as onJsPrompt, onJsAlert, etc. When window.alert, window.confirm, window.prompt are called in the front end,
public boolean onJsAlert(WebView view, String url, String message,
JsResult result) {
return false;
}
public boolean onJsConfirm(WebView view, String url, String message,
JsResult result) {
return false;
}
public boolean onJsPrompt(WebView view, String url, String message,
String defaultValue, JsPromptResult result) {
return false;
}
Copy the code
When js calls window.alert, window.confirm and window.prompt, the corresponding WebChromeClient methods will be called, which can be the entry and the message passing channel. In consideration of development habits, alert and confirm are generally not selected. Promopt is usually selected as the entry point, which in the App is onJsPrompt as the entry point for jsBridge calls. Since onJsPrompt is executed in the UI thread, do not do time-consuming operations and can be handled flexibly with the help of handlers. Use prompt(json.stringify (request)) to handle the callback in the same way as addJavascriptInterface. Notify Native as follows:
function callNative(method, params, success_cb, error_cb) { var request = { version: jsRPCVer, method: method, params: params, id: _current_id++ }; if (typeof success_cb ! == 'undefined') { _callbacks[request.id] = { success_cb: success_cb, error_cb: error_cb }; } prompt(JSON.stringify(request)); };Copy the code
Like the JavaBridge thread before, the JS thread of the prompt must wait for the onJsPrompt to return from the UI thread before waking up, which can be considered a synchronous blocking call (which should be done by thread waiting).
public class JsWebChromeClient extends WebChromeClient { JsBridgeApi mJsBridgeApi; public JsWebChromeClient(JsBridgeApi jsBridgeApi) { mJsBridgeApi = jsBridgeApi; } @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { try { if (mJsBridgeApi.handleJsCall(message)) { <! // Thread. Sleep (10000); result.confirm("sdf"); return true; } } catch (Exception e) { return true; } return super.onJsPrompt(view, URL, message, defaultValue, result); }}Copy the code
If we sleep onJsPrompt for 10 seconds, js prompt will block and wait for 10 seconds to return. This design requires that we cannot do time-consuming operations in onJsPrompt, which can be verified in systrace.
In the figure above, chrome_iothreads are js threads.
A pit in Prompt caused JS to hang
From a presentation point of view, the onJsPrompt must be executed before the prompt returns, otherwise the JS thread will remain blocked. This does happen in practice, especially if there are many threads in the APP. Suspect scenario:
- Step 1: the JS thread is suspended during prompt execution,
- The second part: The UI thread is scheduled to destroy the Webview, call detroy (Webview detroy), cause onJsPrompt will not be called back, prompt will wait, js thread will keep blocking, causing all webViews cannot open, Once there may need to kill process to resolve.
If you do not destroy webView actively, you can largely avoid this problem. The specific implementation of Chrome has not been analyzed, but it is only based on the phenomenon. The WebView. AddJavascriptInterface does not have this problem, whether or not take the initiative to destroy the WebView, are not the problem, may be chrome did additional processing of addJavascriptInterface this way, It actively invokes the JS thread during its own destruction, but the UI thread where the onJsPrompt is located clearly does not handle this scenario.
Reference project https://github.com/happylishang/CMJsBridge
conclusion
- It is best to use front-end injection to avoid injection failures and injection timing problems
- Recommended WebView. AddJavascriptInterface implementation, can avoid the prompt hang up js environment problems
- Methods via @javascriptInterface do not handle time-consuming operations synchronously, and methods that return values need to block calls (to minimize)
- If you do use prompt, try not to destroy the webView yourself, otherwise the JS environment will hang and all webViews will not open the web page
- As with any implementation, do not directly handle time-consuming operations that block the JS thread.
JsBridge for Android Hybrid development
For reference only, welcome correction