在运用开发过程中,为了在多个渠道上保持一致的用户体会和进步开发效率,许多运用程序选择运用 H5 技术。在 Android 渠道上,一般运用 WebView 组件来承载 H5 内容以供展现。

WebView 存在的问题

自 Android Lollipop 起,WebView 组件的晋级现已独立于 Android 渠道。可是,操控 WebView 的 API(android.webkit) 仍然与渠道晋级相关。这意味着运用开发者只能运用当时渠道所界说的接口,而无法充分利用 WebView 的悉数才能。例如: WebView.startSafeBrowsing API 在 Android 8.1 上被增加,该 Feature 由 WebView 供给,即便咱们在 Android 7.0 更新 WebView 具有了该 Feature ,因为 Android 7.0 没有 WebView.startSafeBrowsing API ,咱们也没办法运用该功用。

WebView 的完成根据 Chromium 开源项目,而 Android 则根据 AOSP 项目,这两个项目有着不同的发布周期,WebView 往往一个月就能够推出下一个版别,而 Android 则需求一年的时间,关于 WebView 新增的 Feature 咱们最迟需求一年才能运用。

AndroidX Webkit 的出现

为了处理上面渠道才能和 WebView 不匹配的问题,咱们能够独立于渠道之外界说一套 WebView API ,并让它跟着 WebView 的 Feature 更新 API ,这样处理了现有的问题却导入了另一个问题——如何将新界说的 WebView API 和 WebView 进行衔接。

从运用开发的视点,体系 WebView 难以修正,自己编译定制一个 WebView 并跟着 apk 供给是一个很好计划。这时候,咱们能够轻松的处理衔接问题,并能够依照需求,任意增改 Feature 而不必等官方更新。同时处理了兼容问题和 WebView 内核碎片化的问题。腾讯 X5 ,UC U4 等都是这个计划。保护一份 WebView 并不是一件容易的事,需求投入更多的人力支撑,因为将 WebView 打入包中,还伴跟着包体积的急剧增加。

从 Android 官方的视点,能够推进 WebView 上游支撑该 WebView API , 而这正是 AndroidX Webkit 的处理计划。Android 官方将界说的 WebView API 放置到 AndroidX Webkit 库,以支撑频频的更新,并在 WebView 上游增加“胶水层”与 AndroidX Webkit 进行衔接,这样在旧版的 Android 渠道上,只要安装了具有”胶水”层代码的 WebView ,也就具有了新版渠道的功用。

“胶水层” 是在某个版别之后才后才支撑的,旧版别的 WebView 内核并不支撑,这也是为什么在调用之前一直应该检查 isFeatureSupported 的原因。

AndroidX Webkit 的功用

开始了解了 AndroidX Webkit 的发生和完成原理,下面带领我们看一下它都供给了哪些新才能能够增强咱们的 WebView 。

向下兼容

如上文剖析,AndroidX Webkit 供给了向下的兼容,如下面代码所示,由 WebViewCompat 供给兼容的接口调用。

需求留意的是在调用之前对 WebViewFeature 的检查,关于每个 Feature ,AndroidX Webkit 会取渠道和 WebView 所供给 Feature 的并集 ,在调用某个 API 之前有必要进行检查,假如渠道和 WebView 均不支撑该API则将抛出 UnsupportedOperationException 异常。

// 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);
}

假如咱们扒开 WebViewCompat 的外衣检查他的源码(如下所示),会发现假如在当时版别 Platform API 供给了接口,就会直接调用 Platform API 的接口,而关于低版别,则由 AndroidX Webkit 和 WebView 的”通道”供给服务。

// 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();
    }
}

比照上面的代码,运用渠道 API(old code)时仅能够支撑 90% 的用户,而运用 AndroidX Webkit(new code) 则能够覆盖大约 99% 的用户。

使用 AndroidX 增强 WebView 的能力

署理功用支撑

一直以来WebView 的署理设置异常繁琐,当遇到杂乱的署理规矩就力不从心了。在 AndroidX Webkit 中增加了 ProxyController API 用于为 WebView 设置署理。ProxyConfig.Builder 类供给了设置署理以及装备署理的绕过方式等办法,经过组合能够满足杂乱的署理场景。

if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
    ProxyConfig proxyConfig = new ProxyConfig.Builder()
            .addProxyRule("localhost:7890") //增加要用于一切 URL 的署理
            .addProxyRule("localhost:1080") //优先级低于第一个署理,仅在上一个失利时运用
            .addDirect()                    //当时面的署理失利时,不运用署理直连
            .addBypassRule("www.baidu.com") //该网址不运用署理,直连服务
            .addBypassRule("*.cn")          //以.cn结束的网址不运用署理
            .build();
    Executor executor = ...
    Runnable listener = ...
    ProxyController.getInstance().setProxyOverride(proxyConfig, executor, listener);
}

以上代码界说了一个杂乱的署理场景,咱们为 WebView 设置了两个署理服务器,localhost:1080 仅当 localhost:7890 失利的情况下启用,addDirect 声明了假如两个服务器都失利则直连服务器,addBypassRule 规则了 www.baidu.com 和以 .so 结束的域名一直不应该运用署理。

白名单署理

假如仅有少数的 URL 需求装备署理,咱们能够运用 setReverseBypassEnabled(true) 办法将addBypassRule 增加的 URL 转变为运用署理服务器,而其他的 URL 则直连服务。

安全的 WebView 和 Native 通讯支撑

树立 WebView 和 Native 的双向通讯是运用 Hybrid 混合开发模式的基础,在之前 Android 现已供给了一些机制能够让完成根本的通讯,可是已有的接口都存在一些安全和功用问题,在 AndroidX 中增加了一个功用强大的接口 addWebMessageListener 兼顾了安全和功用等问题。

代码示例中将 JavaSript 方针 replyObject 注入到匹配 allowedOriginRules的上下文中,这样只有在可信的网站中才能被运用此方针,也就防止了不明来历的网络攻击者对该方针的利用。

// 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);

调用上述办法之后,在 JavaScript 上下文中咱们就能够拜访 myObject ,调用 postMessage 就能够回调 Native 端的 onPostMessage 办法并主动切换到主线程履行,当 Native 端需求发送音讯给 WebView 时,能够经过 JavaScriptReplyProxy.postMessage 发送到 WebView ,并将音讯传递给 onmessage 闭包。

// 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!");

文件传递

在以往的通讯机制中,假如咱们想传递一个图片只能将其转换为 base64 等进行传输,假如从前运用过 shouldOverrideUrlLoading 拦截 url 大概率会遇见传输瓶颈,AndroidX Webkit 中很贴心的供给了字节流传递机制。

Native 传递文件给 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 传递文件给 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.
  }
 }
};

深色主题的支撑

Android 10 供给了深色主题的支撑,可是在 WebView 中显现的网页却不会主动显现深色主题, 这就表现出严重的分裂感,开发者只能经过修正 css 来到达意图,但这往往费时费力还存在兼容性问题,Android 官方为了改善这一用户体会,为 WebView 供给了深色主题的适配。

一个网页如何表现是和prefers-color-schemeandcolor-scheme 这两个 Web 标准互操作的。 Android官方供给了一张表阐述了他们之间的关系。

使用 AndroidX 增强 WebView 的能力

上面这张图比较杂乱,简略来说假如你想让 WebView 的内容和运用的主题相匹配,你应该一直界说深色主题并完成 prefers-color-scheme ,而关于未界说 prefers-color-scheme 的页面,体系依照不同的战略选择算法生成或者显现默许页面。

以 Android 12 或更低版别为方针渠道的运用 API 规划过于杂乱,以 Android 13 或更高版别为方针渠道的运用精简了 API ,详细改变请参考官方文档

JavaScript and WebAssembly 履行引擎支撑

咱们有时候咱们会在程序中运转 JavaScript 而不显现任何 Web 内容,比如小程序的逻辑层,运用 WebView 本能够满足咱们的要求可是浪费了过多的资源,咱们都知道在 WebView 中真正担任履行 JavaScript 的引擎是 V8 ,可是咱们又无法直接运用,所以咱们的安装包中出现了各式各样的引擎:HermesJSCV8等。

Android 发现了这”群雄割据“的局面,推出了AndroidX JavascriptEngine,JavascriptEngine 直接运用了 WebView 的 V8 完成,因为不用分配其他 WebView 资源所以资源分配更低,并能够敞开多个独立运转的环境,还针对传递大量数据做了优化。

代码展现了履行 JavaScript 和 WebAssembly 代码的运用:

if(!JavaScriptSandbox.isSupported()){
return;
}
//连接到引擎
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
               JavaScriptSandbox.createConnectedInstanceAsync(context);
//创立上下文 上下文间有简略的数据隔离
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
//履行函数 && 获取结果
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运转
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
}

更多支撑

AndroidX Webkit 是一个功用强大的库,因为篇幅原因上文将开发者比较常用的功用进行了罗列,AndroidX 还供给对 WebView 更精细化的操控,对 Cookie 的快捷拜访、对 Web 资源的快捷拜访,对 WebView 功用的搜集,还有对大屏幕的支撑等等强大的 API,我们能够检查发布页面检查最新的功用。

写在最后

本文从实际矛盾动身,带领我们考虑 AndroidX Webkit 的发生原因和完成原理,关于AndroidX Webkit 的几个功用别离做了简略的介绍,希望我们能在这篇文章获得一点启发和协助。