使用 AndroidX 增强 WebView 的能力

在应用开发过程中,为了在多个平台上保持一致的用户体验和提高开发效率,许多应用程序选择使用 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 异常。

如果我们扒开 WebViewCompat 的外衣查看他的源码(如下所示),会发现如果在当前版本 Platform API 提供了接口,就会直接调用 Platform API 的接口,而对于低版本,则由 AndroidX Webkit 和 WebView 的"通道"提供服务。

对比上面的代码,使用平台 API(old code)时仅可以支持 90% 的用户,而使用 AndroidX Webkit(new code) 则可以覆盖大约 99% 的用户。

代理功能支持

一直以来WebView 的代理设置异常繁琐,当遇到复杂的代理规则就无能为力了。在 AndroidX Webkit 中增加了 ProxyController API 用于为 WebView 设置代理。ProxyConfig.Builder 类提供了设置代理以及配置代理的绕过方式等方法,通过组合可以满足复杂的代理场景。

以上代码定义了一个复杂的代理场景,我们为 WebView 设置了两个代理服务器,localhost:1080 仅当 localhost:7890 失败的情况下启用,addDirect 声明了如果两个服务器都失败则直连服务器,addBypassRule 规定了 www.bing.com 和以 .so 结尾的域名始终不应该使用代理。

白名单代理

如果仅有少量的 URL 需要配置代理,我们可以使用 setReverseBypassEnabled(true) 方法将addBypassRule 添加的 URL 转变为使用代理服务器,而其他的 URL 则直连服务。

安全的 WebView 和 Native 通信支持

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

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

调用上述方法之后,在 JavaScript 上下文中我们就可以访问 myObject ,调用 postMessage 就可以回调 Native 端的 onPostMessage 方法并自动切换到主线程执行,当 Native 端需要发送消息给 WebView 时,可以通过 JavaScriptReplyProxy.postMessage 发送到 WebView ,并将消息传递给 onmessage 闭包。

文件传递

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

Native 传递文件给 WebView

WebView 传递文件给 Native

深色主题的支持

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

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

上面这张图比较复杂,简单来说如果你想让 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 代码的使用:

更多支持

AndroidX Webkit 是一个功能强大的库,由于篇幅原因上文将开发者比较常用的功能进行了列举,AndroidX 还提供对 WebView 更精细化的控制,对 Cookie 的便捷访问、对 Web 资源的便捷访问,对 WebView 性能的收集,还有对大屏幕的支持等等强大的 API,大家可以查看发布页面查看最新的功能。

参考链接


Error: Gradle build failed to produce an .apk file. It's likely that this file was generated under /xxxx/build, but the tool couldn't find it.

Flutter 升级到 3.27.3 版本,然后升级 Android 构建工具到最新的 AGP 8.8.0 版本,然后编译报错:

于是在项目的配置文件中寻找配置 "build" 目录的地方,于是在 Flutter 项目的 Android 工程根目录下找到,如下:

注意 getLayout().setBuildDirectory('../build') ,以前的 Gradle 版本是可以正常编译的,现在的版本需要需要改为 rootProject.buildDir = "../build" 。

修改后的结果如下:

Android中WebView使用LoadUrl不刷新网页的问题,网址带#只能通过reload刷新

问题描述

最近在和公司其他项目组沟通联调H5的时候,意外发现对方发过来的地址可以加载,但是没有办法正常刷新。

尤其是对方服务器上已经重新部署页面,二次进入的情况下,依旧显示上一次加载过的内容,直到应用重启为止。

问题复现

写个 WebView 的 demo,然后在 WebViewClient 的所有方法加上日志来监控网页的运行情况。

此处假定测试网址为 https://www.mobibrw.com/#/?p=43064 

对网页第一次加载可以正常加载(即调用了 onPageStartedonPageFinished )。

第二次调用 loadUrl() 加载的时候只会刷新图标,不会真的重新加载网页(只调用了 onPageFinished )。

最后发现当我们第二次加载链接的时候,如果调用的是 webview.loadUrl(url) ,就无法刷新。

但是如果调用的是 webview.reload(url) ,就可以正常刷新网页。

调用 reload 时日志如下:

原因分析

知道了调用 reload 可以重刷网页,调用 loadUrl 无法重刷网页,问题就在于这两个方法的差别了。

经过查找,得知了真正原因是因为对方的 URL 带了 #,所以导致 loadUrl 不会刷新网页。

将网址改为 https://www.mobibrw.com/?p=43064,再次调用 loadUrl,第二次加载日志如下:

可以看到有正常执行 onPageStartedonPageFinished

那么为什么 URL 里面带了个 #,我们的 WebView 就无法通过 loadUrl 刷新网页了呢?

简单来说,URL # 以及其后面的部分,是客户端这边的位置定位符,在加载网页的时候并不会真正的发送给服务端。

我们的测试网址由于带有 #,所以无论怎么调用 loadUrl,他都判断我们只是改变了网页内部的相对位置(虽然实际上我们并没有改变),不会重新加载这个网页,只是加载网页图标。

那为什么 webview.reload 又可以重刷网页呢,实际上是因为 loadUrl 默认会有缓存策略,而 reload 是无视缓存策略强制刷新的,所以我们拿这个地址去浏览器运行,是可以正常刷新的。

结论

loadUrl 会有缓存策略,二次加载遇到带 # 的网页不会刷新,reload 无视缓存策略会强制刷新。

参考链接


BlowFish加解密原理与代码实现

一丶简介

​ BlowFish 是一个对称加密的加密算法。由 Bruce Schneier,1993年设计的。是一个免费自由使用的加密算法。

了解的必要知识

  • BlowFish是一个对称区块加密算法。每次加密数据为 64位 (2个int)类型数据大小。八个字节
  • BlowFish 密钥采用32-448位
  • BlowFish是由一个16轮循环的Feistel结构进行加密的。

二丶原理与代码介绍

2.1 BlowFish算法流程

BlowFish 算法流程是由两部分组成 分别是密钥扩展以及数据加密

在数据加密中是一个16轮循环的Feistel网络。每一轮由一个密钥相关置换和一个密钥与数据相关的替换组成的。

先说一下BlowFish需要的子密钥。

BlowFish在加密或者初始化的过程中会使用两个盒来进行加密

分别是PBOX 以及SBOX

PBOX是 由18个32位的字的子密钥组成的。这些密钥可以通过预计算产生的。

其中PBOX记录的就是Π后面的小数位。转换成16进制存储到pBox中

例如:

关于数学中Π大家应该知道。这里不再累赘。关于如何计算Π后面的小数位以及将小数位转为十六进制存储到p[0]---p[18] 这里也不再赘述。 因为这些都是预计算好的。我们直接使用就可以。

关于小数转为16进制可以使用网站进行转换(不确保以后还能否使用):小数转换16进制

例子如下:

Sbox跟PBOX一样也是Π的小数位组成。sBox是4组8*32的数组。 如下

pBox与Sbox就是BlowFish算法进行加密的核心置换表

2.2 子密钥生成

​ 如果想进行数据加密。那么我们就要进行子密钥生成。也就是要将我们的Key 与 pbox进行 异或

那么说一下流程吧

  • 1.按顺序使用Π的小数部分初始化pbox与sbox (预先计算好了可以直接使用)

  • 2.使用key参数得出key_pbox, 具体流程为 将pbox中的数据 与 key进行 ^ 然后设置到key_pbox中,轮询key每次取出四个字节来与pbox[i]进行 异或

    如果Key不足的情况下 key从零开始补齐4个字节 继续与pbox[i]进行异或

    例子:

  • 3.使用当前的key_pbox数组 与s_box数组对一个64位0数据进行加密。 也就是 两个int 类型数据 8个字节 输出的结果 重新修改到key_pbox 与 key_sbox中。

    key_sbox就是当前的 使用s_box对64位数据进行加密,产生的输出修改的key_sbox中。

  • 一直循环直到修改完key_pbox与key_sbox为止

示例代码如下:

2.3 加密原理

​ BlowFish加密很简单。就是 将一个数 拆分为 左右各32位数值 (也就是 8个字节 左边四个字节,右边四个字节) 然后 左边进行 ^ key_pbox 当作下一次循环的右边。 右边 则是 右边 ^ F(左边) 下次当作循环的左边。

循环16轮。

说着挺难。看下图:

简单的加密流程图:

在这里我们只看加密

这里不得不介绍下F函数。 F函数的输入是一个32位数 也就是四个字节类型数据。 然后内部就是将32位数进行拆分成abcd

例如:

拆分后的a b c d然后去Key_sbox中查表 然后取出对应的值。

公式如下:

F函数明白了那么回头继续看一下加密操作。

图如下:

可以看到 首先计算 L ^Kr(Key_pbox[i]) 然后结果直接作为下一轮的右侧

右侧则是 经过论函数F 进行设置的。 最后变为下一轮循环的左侧。

R = R ^F(L)

最后一轮的话需要单独设置

其实加密函数就是循环16次。 然后每一轮都对 左侧32位 右侧32位进行操作。 操作之后直接作为下一轮循环的左右侧

核心代码如下:

至此我们的blowFish加密已经完成

2.4 BlowFish的解密

​ 解密与加密是相反操作。 加密的时候是从0开始遍历16轮 然后依次对xl xr进行操作。

解密的时候做事从16开始然后迭代16轮 对左右两侧做操作

最后16轮循环完毕之后还是进行一次^

代码如下:

至此解密完成

2.5.完成代码

经过测试加解密输出结果是对的。

参考链接


BlowFish加解密原理与代码实现

make: *** No rule to make target `sqlite_cfg.h', needed by `.target_source'

前置条件

  • macOS Sonoma (14.4.1) 
  • MacBook Pro 2023-Apple M2 Pro (4能效核、8性能核、32GB内存、2TB磁盘)
  • Homebrew (4.2.18 或更高版本)
  • Xcode Version 15.3 (15E204a)
  • DevEco Studio NEXT Developer Preview2 4.1.3.700

错误信息

源代码编译 sqlite3

或者使用 pod 安装 sqlite3 报错:

观察源代码目录,也确实没有 sqlite_cfg.h 文件生成。

继续阅读make: *** No rule to make target `sqlite_cfg.h', needed by `.target_source'

修复 SecurityException: getDataNetworkTypeForSubscriber 问题

1.问题

  • 用户切换改变网络的过程中,应用概率会出现崩溃,日志如下

另外一种场景就是集成了华为的二维码扫描  com.huawei.hms:scanplus:1.3.2.300 ,这个  SDK 比较奇葩的地方在于,使用 Wi-Fi 网络情况下是不会出现问题的。

但是在手机流量的情况下,不申请 android.permission.READ_PHONE_STATE 权限,初始化这个 SDK 会导致应用闪退。

崩溃堆栈如下:

2.分析

  • 根据 SecurityException: getDataNetworkTypeForSubscriber 可以看到,这是一个安全性异常,所以猜测应用在 Android11 的权限有关,由于缺少该权限导致无法访问接口而异常。

  • 找到网络状态检测方法,可以看到调用了 TelephonyManager.getNetworkType()接口获取网络类型,该方法是需要 READ_PHONE_STATE 权限的,该方法上面也有 RequiresPermission 注解声明

  • 进一步分析,接口的调用堆栈为:

3.解决方法

参考链接


修复 SecurityException: getDataNetworkTypeForSubscriber 问题

Android: 通过Intent筛选多种类型文件

一般使用setType()方法来实现文件过滤,如:只显示PDF文件:

但如果要指定多种类型呢,同时要指定pdf,excel,word,ppt这些类型的文件,那要怎样设置呢?

指定多种类型文件

在网上查了,有些答案是

错误方式1——setType中进行拼接:

错误方式2——调用多次setType:

这两种方式都是错误的

我们看下源码

我们可以看到,setType每次都是重新赋值,没有添加到list和数组中,因此这两种方式是实现不了指定多种类型文件的。
既然这种方式实现不了,那么Intent会不会提供字段以便我们传递过滤数据,我们通过官方文档及源码,发现Intent提供了EXTRA_MIME_TYPES这个字段来传递,而且是数组类型:

因此结果就简单了,我们要指定ppt,word,excel,pdf类型的文件,代码如下

MimeType文件

Intent指定多种类型的文件,正确的做法,是通过Intent.EXTRA_MIME_TYPES传递Mime类型数组实现

MimeType 文件列表参考

 1. Mozilla MDN Web Docs: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types

扩展名 文档类型 MIME 类型
.aac AAC audio audio/aac
.abw AbiWord document application/x-abiword
.arc Archive document (multiple files embedded) application/x-freearc
.avi AVI: Audio Video Interleave video/x-msvideo
.azw Amazon Kindle eBook format application/vnd.amazon.ebook
.bin Any kind of binary data application/octet-stream
.bmp Windows OS/2 Bitmap Graphics image/bmp
.bz BZip archive application/x-bzip
.bz2 BZip2 archive application/x-bzip2
.csh C-Shell script application/x-csh
.css Cascading Style Sheets (CSS) text/css
.csv Comma-separated values (CSV) text/csv
.doc Microsoft Word application/msword
.docx Microsoft Word (OpenXML) application/vnd.openxmlformats-officedocument.wordprocessingml.document
.eot MS Embedded OpenType fonts application/vnd.ms-fontobject
.epub Electronic publication (EPUB) application/epub+zip
.gif Graphics Interchange Format (GIF) image/gif
.htm
.html
HyperText Markup Language (HTML) text/html
.ico Icon format image/vnd.microsoft.icon
.ics iCalendar format text/calendar
.jar Java Archive (JAR) application/java-archive
.jpeg
.jpg
JPEG images image/jpeg
.js JavaScript text/javascript
.json JSON format application/json
.jsonld JSON-LD format application/ld+json
.mid
.midi
Musical Instrument Digital Interface (MIDI) audio/midi audio/x-midi
.mjs JavaScript module text/javascript
.mp3 MP3 audio audio/mpeg
.mpeg MPEG Video video/mpeg
.mpkg Apple Installer Package application/vnd.apple.installer+xml
.odp OpenDocument presentation document application/vnd.oasis.opendocument.presentation
.ods OpenDocument spreadsheet document application/vnd.oasis.opendocument.spreadsheet
.odt OpenDocument text document application/vnd.oasis.opendocument.text
.oga OGG audio audio/ogg
.ogv OGG video video/ogg
.ogx OGG application/ogg
.otf OpenType font font/otf
.png Portable Network Graphics image/png
.pdf Adobe Portable Document Format (PDF) application/pdf
.ppt Microsoft PowerPoint application/vnd.ms-powerpoint
.pptx Microsoft PowerPoint (OpenXML) application/vnd.openxmlformats-officedocument.presentationml.presentation
.rar RAR archive application/x-rar-compressed
.rtf Rich Text Format (RTF) application/rtf
.sh Bourne shell script application/x-sh
.svg Scalable Vector Graphics (SVG) image/svg+xml
.swf Small web format (SWF) or Adobe Flash document application/x-shockwave-flash
.tar Tape Archive (TAR) application/x-tar
.tif
.tiff
Tagged Image File Format (TIFF) image/tiff
.ttf TrueType Font font/ttf
.txt Text, (generally ASCII or ISO 8859-n) text/plain
.vsd Microsoft Visio application/vnd.visio
.wav Waveform Audio Format audio/wav
.weba WEBM audio audio/webm
.webm WEBM video video/webm
.webp WEBP image image/webp
.woff Web Open Font Format (WOFF) font/woff
.woff2 Web Open Font Format (WOFF) font/woff2
.xhtml XHTML application/xhtml+xml
.xls Microsoft Excel application/vnd.ms-excel
.xlsx Microsoft Excel (OpenXML) application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
.xml XML application/xml 代码对普通用户来说不可读 (RFC 3023, section 3)
text/xml 代码对普通用户来说可读 (RFC 3023, section 3)
.xul XUL application/vnd.mozilla.xul+xml
.zip ZIP archive application/zip
.3gp 3GPP audio/video container video/3gpp
audio/3gpp(若不含视频)
.3g2 3GPP2 audio/video container video/3gpp2
audio/3gpp2(若不含视频)
.7z 7-zip archive application/x-7z-compressed

2. AOSP MediaFle:  

根据不同的MIME类型,显示不同的图标:

参考链接


android: 通过Intent筛选多种类型文件

macOS Sonoma(14.2.1)通过Docker编译Android 12.1源码过程总结(MacBook Pro 2023-Apple M2 Pro)

前置条件


根据 Google 官方文档,2021年6月22日之后的Android系统版本不支持在macOS系统上构建,我们在 Applic SiliconmacOS 系统是不能直接成功构建后续版本的,但是之前的版本可以在修改编译配置后成功编译,只是是否能正常运行存疑

另外,我们需要安装 Rosetta 2 支持运行部分 x86_64 应用。注意  Rosetta 2 只支持 64 位应用,不支持 32 位应用。 参考 Does Rosetta 2 support 32-bit Intel apps?

继续阅读macOS Sonoma(14.2.1)通过Docker编译Android 12.1源码过程总结(MacBook Pro 2023-Apple M2 Pro)

Android:Installed Build Tools revision 33.0.2 is corrupted.

使用33.0.2及以上版本的build-tools编译Android应用时。

有些人会按照提示去SDK Manager中重新安装build tools,然后发现这样做是无用的

编译时会收到:

Windows:

Linux/macOS:

解决方案:

更改批处理文件名称

Windows系统:

  1. 找到build tools目录中的d8.bat,将文件名修改为dx.bat。
  2. 找到build tools目录中的lib/d8.jar,将文件名修改为dx.jar。
  3. 回到Android Studio重新打包。

Linux/macOS系统:

  1. 找到build tools目录中的d8,创建软链接 ln -s d8 dx
  2. 找到build tools目录中的lib/d8.jar,创建软链接 ln -s d8.jar dx.jar
  3. 回到Android Studio重新打包。

参考链接


Android:Installed Build Tools revision 33.0.2 is corrupted.

使用UrlQuerySanitizer来处理URL

网上对于 UrlQuerySanitizer 的资料比较少,这个是 Android 提供的一个用来处理 url 的 API。由于项目的需要,需要对 url 的 query 参数进行排序,因此需要解析 url 并处理 query 参数。

最初的方法是使用 Uri:

通过这样的方式就可以解析 url,并获取到各个 query 参数。但后来发现 Uri 不能处理一些特殊字符,比如#,Uri 会截断#以后的内容,这样就不能满足开发需求。经过各种 google,最后发现了一个 UrlQuerySanitizer 的 API:

首先要使用 setAllowUnregisteredParamaters 让其支持特殊字符,然后使用 setUnregisteredParameterValueSanitizer 来设置支持哪些特殊字符,UrlQuerySanitizer 提供了集中默认的 ValueSanitizer:

每种 ValueSanitizer 都对应过滤哪些字符,被过滤掉的特殊字符会被替换成_或者空格。
如果默认的 ValueSanitizer 不能满足开发需求,还可以自己构造 ValueSanitizer:

UrlQuerySanitizer 也可以通过 key 来获取相应的 value,比如给一个 url:http://coolerfall.com?name=vincent:

UrlQuerySanitizer 还可以只解析 query 参数,比如:name=vincent&article=first:

以上就是 UrlQuerySanitizer 大致用法,用来解析处理 url 非常的方便。

参考链接


使用 UrlQuerySanitizer 来处理 url