Enhancing the capabilities of WebView with AndroidX

In the application development process, in order to maintain a consistent user experience and improve development efficiency on multiple platforms, many applications choose to use H5 technology. On the Android platform, the WebView component is usually used to host H5 content for display.

Problems with WebView

Since Android Lollipop, the upgrade of the WebView component has been independent of the Android platform. However, the API (android.webkit) that controls the WebView is still relevant for platform upgrades. This means that application developers can only use the interfaces defined by the current platform, but cannot take full advantage of the full capabilities of WebView. For example: WebView.startSafeBrowsing API is added on Android 8.1, this Feature is provided by WebView, even if we update WebView to have this Feature in Android 7.0, since Android 7.0 does not have WebView.startSafeBrowsing API , we can’t use this feature either.

The implementation of WebView is based on the Chromium open source project, while Android is based on the AOSP project. These two projects have different release cycles. WebView can usually launch the next version in a month. Android takes one year, and we need one year at the latest to use the new features of WebView.

The emergence of AndroidX Webkit

In order to solve the above mismatch between platform capabilities and WebView, we can define a set of WebView API independently of the platform, and let it update the API with the WebView Feature, which solves the existing problem but introduces another problem- -How to connect the newly defined WebView API and WebView.

From the perspective of application development, the system WebView is difficult to modify. It is a good solution to compile and customize a WebView by yourself and provide it with the apk. At this time, we can easily solve the connection problem, and can add and change features arbitrarily according to the needs without waiting for the official update. At the same time, it solves the problem of compatibility and WebView kernel fragmentation. Tencent X5, UC U4, etc. are all this solution. Maintaining a WebView is not an easy task, and requires more manpower support, because packing WebView into a package is accompanied by a sharp increase in package size.

From an official Android point of view, WebView can be promoted upstream to support the WebView API, and this is the solution of AndroidX Webkit. Android officially puts the defined WebView API into the AndroidX Webkit library to support frequent updates, and adds a “glue layer” upstream of WebView to connect with AndroidX Webkit, so that on the old version of the Android platform, as long as it has “glue\ “The WebView of the layer code also has the functions of the new version of the platform.

The “glue layer” is only supported after a certain version, and the old version of the WebView kernel does not support it, which is why you should always check isFeatureSupported before calling it.

AndroidX Webkit features

After a preliminary understanding of the generation and implementation principles of AndroidX Webkit, let’s take a look at what new capabilities it provides to enhance our WebView.

Backward compatibility

As analyzed above, AndroidX Webkit provides downward compatibility, as shown in the code below, and WebViewCompat provides compatible interface calls.

It should be noted that the WebViewFeature is checked before calling. For each Feature, AndroidX Webkit will take the union of the platform and the Feature provided by WebView, and it must be checked before calling an API. If neither the platform nor the WebView If this API is supported, an UnsupportedOperationException will be thrown.

// Old code:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
   WebView.startSafeBrowsing(appContext, callback);
}

//New code:
if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
   WebViewCompat.startSafeBrowsing(appContext, callback);
}

If we peel off the coat of WebViewCompat and look at its source code (as shown below), we will find that if the current version of the Platform API provides an interface, the interface of the Platform API will be called directly, and for the lower version, it is provided by AndroidX Webkit and WebView “Channel” provides services.

// WebViewCompat#startSafeBrowsing
public static void startSafeBrowsing(@NonNull Context context,
        @Nullable ValueCallback<Boolean> callback) {
    ApiFeature.O_MR1 feature = WebViewFeatureInternal.START_SAFE_BROWSING;
    if (feature. isSupportedByFramework()) {
        ApiHelperForOMR1.startSafeBrowsing(context, callback);
    } else if (feature. isSupportedByWebView()) {
        getFactory().getStatics().initSafeBrowsing(context, callback);
    } else {
        throw WebViewFeatureInternal.getUnsupportedOperationException();
    }
}

Compared with the above code, only 90% of users can be supported when using platform API (old code), while about 99% of users can be covered when using AndroidX Webkit (new code).

image

Proxy function support

The proxy setting of WebView has always been extremely cumbersome, and there is nothing you can do when you encounter complex proxy rules. Added ProxyController API in AndroidX Webkit to set proxy for WebView. The ProxyConfig.Builder class provides methods such as setting proxy and configuring proxy bypass methods, which can meet complex proxy scenarios through combination.

if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
    ProxyConfig proxyConfig = new ProxyConfig. Builder()
            .addProxyRule("localhost:7890") //Add proxy to be used for all URLs
            .addProxyRule("localhost:1080") //The priority is lower than the first proxy, only applied when the previous one fails
            .addDirect() //When the previous proxy fails, direct connection without proxy
            .addBypassRule("www.baidu.com") //This URL does not use a proxy and directly connects to the service
            .addBypassRule("*.cn") // URLs ending in .cn do not use proxy
            .build();
    Executor executor = ?…
    Runnable listener = ?…
    ProxyController.getInstance().setProxyOverride(proxyConfig, executor, listener);
}

The above code defines a complex proxy scenario. We set up two proxy servers for WebView. localhost:1080 is only enabled when localhost:7890 fails. addDirect declares that if both servers fail, it will directly connect to the server. addBypassRule stipulates Domain names ending with www.baidu.com and .so should never use proxies.

Whitelist proxy

If only a small number of URLs need to configure a proxy, we can use the setReverseBypassEnabled(true) method to convert the URLs added by addBypassRule to use a proxy server, while other URLs are directly connected to the service.

Secure WebView and Native communication support

Establishing two-way communication between WebView and Native is the basis of using the Hybrid hybrid development model. Android has provided some mechanisms to complete basic communication before, but there are some security and performance problems in the existing interfaces. In AndroidX, a The powerful interface addWebMessageListener takes into account security and performance issues.

In the code example, the JavaSript object replyObject is injected into the context matching allowedOriginRules, so that this object can only be used in trusted websites, which also prevents network attackers from unknown sources from using the object.

// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    // do something about view, message, sourceOrigin and isMainFrame.
    replyProxy.postMessage("Got it!");
  }
};

HashSet<String> allowedOriginRules = new HashSet<>(Arrays. asList("[https://example.com](https://example.com/)"));
// Add WebMessageListeners.

WebViewCompat.addWebMessageListener(webView, "replyObject", allowedOriginRules,myListener);

After calling the above method, we can access myObject in the JavaScript context, and call postMessage to call back the onPostMessage method on the Native side and automatically switch to the main thread for execution. When the Native side needs to send a message to WebView, it can be sent to WebView through JavaScriptReplyProxy.postMessage , and pass the message to the onmessage closure.

// Web page (in JavaScript)
myObject.onmessage = function(event) {
  // prints "Got it!" when we receive the app's response.
  console.log(event.data);
}
myObject.postMessage("I'm ready!");

File transfer

In the previous communication mechanism, if we want to transmit a picture, we can only convert it to base64 for transmission. If we have used shouldOverrideUrlLoading to intercept the url, there is a high probability that we will encounter transmission bottlenecks. AndroidX Webkit provides byte stream transmission very intimately. mechanism.

Native passes files to WebView

// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    // Communication is setup, send file data to web.
    if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
      // Suppose readFileData method is to read content from file.
      byte[] fileData = readFileData("myFile.dat");
      replyProxy. postMessage(fileData);
    }
  }
}
// Web page (in JavaScript)
myObject.onmessage = function(event) {
  if (event. data instanceof ArrayBuffer) {
    const data = event.data; // Received file content from app.
    const dataView = new DataView(data);
    // Consume file content by using JavaScript DataView to access ArrayBuffer.
  }
}
myObject.postMessage("Setup!");

WebView passes files to Native

// Web page (in JavaScript)
const response = await fetch('example. jpg');
if (response. ok) {
    const imageData = await response. arrayBuffer();
    myObject. postMessage(imageData);
}
// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    if (message. getType() == WebMessageCompat. TYPE_ARRAY_BUFFER) {
      byte[] imageData = message.getArrayBuffer();
      // do something like draw image on ImageView.
    }
  }
};

Dark theme support Android 10 provides dark theme support, but the webpage displayed in WebView does not automatically display the dark theme, which shows a serious sense of fragmentation, developers can only achieve the goal by modifying css However, this is often time-consuming and labor-intensive, and there are compatibility issues. In order to improve this user experience, Android officials provide WebView with a dark theme adaptation.

How a web page behaves is interoperable with two web standards, prefers-color-scheme and color-scheme. Android officials provide a table illustrating the relationship between them.

image

The above picture is more complicated. Simply put, if you want the content of WebView to match the theme of the application, you should always define a dark theme and implement prefers-color-scheme, and for pages that do not define prefers-color-scheme , the system selects algorithms according to different strategies to generate or display the default page.

The API design of applications targeting Android 12 or lower is too complex, and the API is simplified for applications targeting Android 13 or higher. For specific changes, please refer to the official documentation.

JavaScript and WebAssembly execution engine support

We sometimes run JavaScript in the program without displaying any Web content, such as the logic layer of the applet. Using WebView can meet our requirements but waste too many resources. We all know that WebView is really responsible for executing JavaScript The engine is V8, but we can’t use it directly, so there are various engines in our installation package: Hermes, JSC, V8 and so on.

Android discovered this situation of “separation of heroes” and launched AndroidX JavascriptEngine. JavascriptEngine directly uses WebView’s V8 implementation. Since it does not need to allocate other WebView resources, resource allocation is lower, and multiple independent running environments can be opened. A large amount of data has been optimized.

The code demonstrates the use of executing JavaScript and WebAssembly code:

if(!JavaScriptSandbox.isSupported()){
return;
}
// connect to the engine
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
               JavaScriptSandbox.createConnectedInstanceAsync(context);
//Create context There is simple data isolation between contexts
JavaScriptIsolate jsIsolate = jsSandbox. createIsolate();
//Execute the function & amp; & amp; to get the result
final String code = "function sum(a, b) { let r = a + b; return r.toString(); }; sum(3, 4)";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture. get(5, TimeUnit. SECONDS);
Futures. addCallback(resultFuture,
       new FutureCallback<String>() {
           @Override
           public void onSuccess(String result) {
               text.append(result);
           }
           @Override
           public void onFailure(Throwable t) {
               text.append(t.getMessage());
           }
       },
       mainThreadExecutor); //Wasm running
final byte[] hello_world_wasm = {
   0x00 ,0x61 ,0x73 ,0x6d ,0x01 ,0x00 ,0x00 ,0x00 ,0x01 ,0x0a ,0x02 ,0x60 ,0x02 ,0x7f ,0x7f ,0x01,
   0x7f ,0x60 ,0x00 ,0x00 ,0x03 ,0x03 ,0x02 ,0x00 ,0x01 ,0x04 ,0x04 ,0x01 ,0x70 ,0x00 ,0x01 ,0x05,
   0x03 ,0x01 ,0x00 ,0x00 ,0x06 ,0x06 ,0x01 ,0x7f ,0x00 ,0x41 ,0x08 ,0x0b ,0x07 ,0x18 ,0x03 ,0x06,
   0x6d ,0x65 ,0x6d ,0x6f ,0x72 ,0x79 ,0x02 ,0x00 ,0x05 ,0x74 ,0x61 ,0x62 ,0x6c ,0x65 ,0x01 ,0x00,
   0x03 ,0x61 ,0x64 ,0x64 ,0x00 ,0x00 ,0x09 ,0x07 ,0x01 ,0x00 ,0x41 ,0x00 ,0x0b ,0x01 ,0x01 ,0x0a,
   0x0c ,0x02 ,0x07 ,0x00 ,0x20 ,0x00 ,0x20 ,0x01 ,0x6a ,0x0b ,0x02 ,0x00 ,0x0b,
};
final String jsCode = "android.consumeNamedDataAsArrayBuffer('wasm-1').then(" +
       "(value) => { return WebAssembly.compile(value).then(" +
       "(module) => { return new WebAssembly.Instance(module).exports.add(20, 22).toString(); }" +
       ")})";
boolean success = js.provideNamedData("wasm-1", hello_world_wasm);
if (success) {
    FluentFuture.from(js.evaluateJavaScriptAsync(jsCode))
           .transform(this::println, mainThreadExecutor)
           .catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
} else {
   // the data chunk name has been used before, use a different name
}

More support

AndroidX Webkit is a powerful library. Due to space reasons, the functions commonly used by developers are listed above. AndroidX also provides more fine-grained control over WebView, convenient access to cookies, convenient access to web resources, and The collection of WebView performance, as well as the support for large screens and other powerful APIs, you can check the release page to see the latest features.

Written at the end

Starting from the actual contradiction, this article leads everyone to think about the reasons and implementation principles of AndroidX Webkit, and briefly introduces several functions of AndroidX Webkit. I hope you can get some inspiration and help from this article.

Author: Jianhua Android
Link: https://juejin.cn/post/7259762775365320741
Source: Rare Earth Nuggets
Copyright belongs to the author. For commercial reprint, please contact the author for authorization, for non-commercial reprint, please indicate the source.