项目要求支持 8K 高清视频(H265编码)播放,拿到板子后却发现使用 App 可以播放 8K 高清视频,但使用浏览器却不行,即使安装上最新的 Chrome for Android 也不行。根据以往的浏览器内核开发经验,在 Android 平台上,Chromium WebView 最终是调用系统框架层的 MediaPlayer 进行播放。理论上只要系统框架层能够支持 8K 高清播放,那么浏览器应该也支持。实际情况却并非如此,而且 Android 10 预编译 WebView 没任何日志输出,所以需要下载源码编译 Chromium WebView,找出问题所在。
版本选择坑
Chromium 源码更新非常平凡,而且架构也经常变化,不像我们做项目,一套代码恨不得修修补补用上十几年。上一个项目是基于 Chromium V53 进行定制的,这次并不想采用最新的 Chromium 版本,大概浏览了一下最新源码,和 V53 差别太大,以往的定制工作要移植过来相当麻烦。首先想到的是直接使用 V53 的源码,但无法应用到 Android 10 上,主要是 Android 10 的 WebView API 接口发生了一些变化。最终令我放弃的是 Android 10 框架层移除了 HardwareCanvas 类,要知道,在 Android 5.1 中,WebView 中有一个重要的绘制方法:
代码语言:javascript复制public void callDrawGlFunction(Canvas canvas, long nativeDrawGLFunctor) {
if (!(canvas instanceof HardwareCanvas)) {
// Canvas#isHardwareAccelerated() is only true for subclasses of HardwareCanvas.
throw new IllegalArgumentException(canvas.getClass().getName()
" is not hardware accelerated");
}
((HardwareCanvas) canvas).callDrawGLFunction2(nativeDrawGLFunctor);
}
不能使用 V53,接下来考虑 Android 10 中预编译的 Chromium Webview 版本,使用 WebView Shell,查看版本号为 74.0.3729.183:
然而,这里有一个巨大的坑。等我费了九牛二虎之力把代码下载下来,把 Chromium WebView 编译出来并安装到系统,结果浏览器启动就崩溃,查看系统日志,有如下错误信息:
代码语言:javascript复制03-05 00:41:09.457 3299 4011 W WebViewUpdater: creating relro file timed out
03-05 00:41:09.458 12565 12565 E WebViewFactory: Chromium WebView package does not exist
03-05 00:41:09.458 12565 12565 E WebViewFactory: android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.webkit.WebViewFactory.getWebViewContextAndSetProvider(WebViewFactory.java:339)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.webkit.WebViewFactory.getProviderClass(WebViewFactory.java:402)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:252)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.webkit.WebView.getFactory(WebView.java:2551)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.webkit.WebView.setWebContentsDebuggingEnabled(WebView.java:1974)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at org.chromium.webview_shell.WebViewBrowserActivity.onCreate(WebViewBrowserActivity.java:230)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.Activity.performCreate(Activity.java:7802)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.Activity.performCreate(Activity.java:7791)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1306)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.os.Handler.dispatchMessage(Handler.java:107)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.os.Looper.loop(Looper.java:214)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at android.app.ActivityThread.main(ActivityThread.java:7368)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at java.lang.reflect.Method.invoke(Native Method)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
03-05 00:41:09.458 12565 12565 E WebViewFactory: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
03-05 00:41:09.459 12565 12565 D AndroidRuntime: Shutting down VM
查看了一下框架层代码,一个合格的 WebView Provider,首先其 targetSdk 版本号要大于或等于 29,而查看 chromium v74 源码的 build/config/android/sdk.gni 文件:
代码语言:javascript复制# The default SDK release used by public builds. Value may differ in
# internal builds.
default_android_sdk_release = "p"
# SDK releases against which public builds are supported.
public_sdk_releases = [ "p" ]
Android 10 的代号为 Q,也就是说这个版本的 Chromium WebView 实际上只支持到 Android 9,也不知道 Google 使用了什么魔法。可能是当时 Android 10 在开发过程中,使用了内部的 SDK 进行构建。
这个时候,我还是不想使用最新版本,那就采用正式支持 Android Q 的最早版本吧。方法就是查看这个 sdk.gni 的修改记录,看看哪个提交从 "p" 修改到 "q"。因为代码的分支超级多,必须要查看所有分支的 git 提交:
代码语言:javascript复制$ git log -p --all -G 'sdk' sdk.gni
查到 commit id 为 c2481863282a401926e0ee479334c68ec362d302 ,接下来查询哪些分支包含这个提交:
代码语言:javascript复制$ git branch -a --contains c2481863282a401926e0ee479334c68ec362d302
remotes/branch-heads/3967
remotes/branch-heads/3968
remotes/branch-heads/3969
remotes/branch-heads/3970
remotes/branch-heads/3971
remotes/branch-heads/3972
remotes/branch-heads/3973
remotes/branch-heads/3974
remotes/branch-heads/3975
remotes/branch-heads/3976
remotes/branch-heads/3977
remotes/branch-heads/3978
remotes/branch-heads/3979
remotes/branch-heads/3980
remotes/branch-heads/3981
remotes/branch-heads/3982
remotes/branch-heads/3983
remotes/branch-heads/3984
remotes/branch-heads/3985
remotes/branch-heads/3986
remotes/branch-heads/3987
remotes/branch-heads/3987_100
remotes/branch-heads/3987_137
remotes/branch-heads/3987_158
remotes/branch-heads/3987_87
remotes/branch-heads/3988
remotes/branch-heads/3989
remotes/branch-heads/3990
remotes/branch-heads/3991
remotes/branch-heads/3992
remotes/branch-heads/3993
remotes/branch-heads/3994
remotes/branch-heads/3995
remotes/branch-heads/3996
remotes/branch-heads/3997
remotes/branch-heads/3998
remotes/branch-heads/3999
remotes/branch-heads/4000
remotes/branch-heads/4001
remotes/branch-heads/4002
remotes/branch-heads/4003
remotes/branch-heads/4004
remotes/branch-heads/4005
remotes/branch-heads/4006
remotes/branch-heads/4007
remotes/branch-heads/4008
remotes/branch-heads/4009
remotes/branch-heads/4010
remotes/branch-heads/4011
remotes/branch-heads/4012
包含这个提交的分支非常多,数字越大,表明分支代码越新,所以这里搜索这中间的一个稳定发布版本即可。访问如下链接:
https://chromiumdash.appspot.com/releases?platform=Android
这里包含了所有曾经发布过的版本,最终我选择的是 V80.0.3987.165 。在趟过 代码下载坑、编译坑、安装坑后,终于成功运行起来:
代码下载坑
由于众所周知的原因, Chromium 源码不能直接下载,我是挂了代理进行下载。然而在下载时经常出现如下错误:
代码语言:javascript复制remote: Counting objects: 867615, done
remote: Finding sources: 100% (10514569/10514569)
error: RPC failed; curl 56 GnuTLS recv error (-9): Error decoding the received TLS packet.
fatal: the remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed
Git 没有断点续传机制,下载失败了需要重来,碰到 Chromium 这样的超级大库,下载到十几 G 的时候断掉,想死的心都有。上网查了很多方法都不管用,后来在一个帖子中找到解决方法:
代码语言:javascript复制git config --global http.postBuffer 5242880000
git config --global https.postBuffer 5242880000
sudo ifconfig eth0 mtu 9000
网上的指导一般是将 http.postBuffer 设为一个比较大的值,对于 Chromium 这样单个 git 库超过 30 G 的还是不够,我将缓冲设成 5G 大小,终于不再出错。另外有指导将 MTU 大小设置为 14000,但在 Ubuntu 上无法设成功,我尝试出来的最大值为 9000。
源码 clone 成功后,执行 gclient runhooks 命令又出现如下错误:
代码语言:javascript复制NOTICE: You have PROXY values set in your environment, but gsutilin depot_tools does not (yet) obey them.
Also, --no_auth prevents the normal BOTO_CONFIG environmentvariable from being used.
To use a proxy in this situation, please supply those settingsin a .boto file pointed to by the NO_AUTH_BOTO_CONFIG environmentvariable.
这是使用了代理的原因,解决的方法很简单,创建一个文本文件(比如 $HOME/.boto),内容为:
代码语言:javascript复制[boto]
proxy=代理IP
proxy_port=代理端口
然后设置环境变量 NO_AUTH_BOTO_CONFIG 指向这个文件:
代码语言:javascript复制export NO_AUTH_BOTO_CONFIG=$HOME/.boto
至此,终于解决了源码下载和二进制文件下载的问题。
代码编译坑
编译代码过程中,出现如下错误:
代码语言:javascript复制FAILED: gen/build/android/buildhooks/build_hooks_android_java.javac.jar gen/build/android/buildhooks/build_hooks_android_java.javac.jar.info
python ../../build/android/gyp/javac.py --depfile=gen/build/android/buildhooks/build_hooks_android_java__compile_java.d --generated-dir=gen/build/android/buildhooks/build_hooks_android_java/generated_java --jar-path=gen/build/android/buildhooks/build_hooks_android_java.javac.jar --java-srcjars=[] --java-version=1.8 --full-classpath=@FileArg(gen/build/android/buildhooks/build_hooks_android_java.build_config:deps_info:javac_full_classpath) --interface-classpath=@FileArg(gen/build/android/buildhooks/build_hooks_android_java.build_config:deps_info:javac_full_interface_classpath) --processorpath=@FileArg(gen/build/android/buildhooks/build_hooks_android_java.build_config:javac:processor_classpath) --processors=@FileArg(gen/build/android/buildhooks/build_hooks_android_java.build_config:javac:processor_classes) --java-srcjars=@FileArg(gen/build/android/buildhooks/build_hooks_android_java.build_config:deps_info:owned_resource_srcjars) --bootclasspath=@FileArg(gen/build/android/buildhooks/build_hooks_android_java.build_config:android:sdk_interface_jars) --chromium-code=1 --errorprone-path bin/errorprone @gen/build/android/buildhooks/build_hooks_android_java.sources
Traceback (most recent call last):
File "../../build/android/gyp/javac.py", line 595, in <module>
sys.exit(main(sys.argv[1:]))
File "../../build/android/gyp/javac.py", line 590, in main
add_pydeps=False)
File "/data/chromium/chromium_v74.0.3729.183/src/build/android/gyp/util/build_utils.py", line 653, in CallAndWriteDepfileIfStale
pass_changes=True)
File "/data/chromium/chromium_v74.0.3729.183/src/build/android/gyp/util/md5_check.py", line 87, in CallAndRecordIfStale
function(*args)
File "/data/chromium/chromium_v74.0.3729.183/src/build/android/gyp/util/build_utils.py", line 638, in on_stale_md5
function(*args)
File "../../build/android/gyp/javac.py", line 584, in <lambda>
lambda: _OnStaleMd5(options, javac_cmd, java_files, classpath),
File "../../build/android/gyp/javac.py", line 350, in _OnStaleMd5
stderr_filter=ProcessJavacOutput)
File "/data/chromium/chromium_v74.0.3729.183/src/build/android/gyp/util/build_utils.py", line 227, in CheckOutput
raise CalledProcessError(cwd, args, stdout stderr)
util.build_utils.CalledProcessError: Command failed: ( cd /data/chromium/chromium_v74.0.3729.183/src/out/Default; bin/errorprone -g -encoding UTF-8 -sourcepath : -XepDisableAllChecks -source 1.8 -target 1.8 -Xlint:unchecked -Werror -bootclasspath lib.java/third_party/android_tools/android.interface.jar:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/rt.jar -d /tmp/tmpzNZrEr/classes @/tmp/tmpzNZrEr/files_list.txt )
-Xbootclasspath/p is no longer a supported option.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
原因是 Ubuntu 20.04 使用的 JDK 版本为 11,但这个版本的 Chromium 编译需要 JDK 8,解决的方法就是将 JDK 切换到 JDK:
代码语言:javascript复制$ java --version
openjdk 11.0.18 2023-01-17
OpenJDK Runtime Environment (build 11.0.18 10-post-Ubuntu-0ubuntu120.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18 10-post-Ubuntu-0ubuntu120.04.1, mixed mode, sharing)
$ sudo update-alternatives --config java
[sudo] password for alex:
There are 2 choices for the alternative java (providing /usr/bin/java).
Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1111 auto mode
1 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1111 manual mode
2 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java 1081 manual mode
Press <enter> to keep the current choice[*], or type selection number: 2
update-alternatives: using /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java to provide /usr/bin/java (java) in manual mode
$ java -version
openjdk version "1.8.0_362"
OpenJDK Runtime Environment (build 1.8.0_362-8u362-ga-0ubuntu1~20.04.1-b09)
OpenJDK 64-Bit Server VM (build 25.362-b09, mixed mode)
JDK 切换后,成功编译出 system_webview_apk。
WebView 安装坑
编译出 system_webview_apk 后进行安装,出现如下错误:
代码语言:javascript复制Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.android.webview signatures do not match previously installed version; ignoring!]
- Performing Streamed Install
这是由于 Chromium 编译所使用的证书和 Android 10 的 platform 证书不一致导致的。解决这一问题的方法就是将两边的证书弄成一样。
第一步,查看 Android 10 中 weview.apk 的签名信息。使用压缩工具解压出 apk 中的 META-INF/WEBVIEW-.RSA 文件,然后使用如下命令查看签名信息:
代码语言:javascript复制$ keytool -printcert -file META-INF/WEBVIEW-.RSA
第二步,确定 Android 10 使用的证书。一般使用 build/target/product/security/ 下的证书,也有的产品使用自定义的证书,一般放在 device 下的产品目录。如果确定呢?查看一下证书的签名信息,对得上就是的。
代码语言:javascript复制openssl x509 -in testkey.x509.pem -noout -fingerprint
第三步,导入 keystore。Chromium 中使用的是 Java 应用的比较广的 keystore 格式,所以需要将 Android 10 中的证书导入进来。
代码语言:javascript复制# 私钥格式 pkcs8 转 PEM
$ openssl pkcs8 -inform DER -nocrypt -in $ANDROID_SOURCE/build/target/product/security/testkey.pk8 -out testkey.pem
# 证书从 PEM 转 pkcs12
$ openssl pkcs12 -export -in $ANDROID_SOURC/build/target/product/security/testkey.x509.pem -inkey testkey.pem -out testkey.p12 -name "android_testkey"
Enter Export Password: (输入密码android,默认是android,如是自己制作的key,输入对应的密码)
Verifying - Enter Export Password: (输入密码android)
# 生成 keystore
$ keytool -importkeystore -deststorepass android -destkeypass android -srckeystore testkey.p12 -srcstoretype pkcs12 -srcstorepass android -destkeystore AOSP.keystore -alias android_testkey
第四步,修改 gn args,使用自己的 keystore,修改 out/Default/args.gn 文件,加入如下行:
代码语言:javascript复制# 假设 AOSP.keystore 文件位于 chromium src 目录下
android_keystore_path = "//AOSP.keystore"
android_keystore_name = "android_testkey"
android_keystore_password = "android"
第五步,重新 build system_webview_apk
代码语言:javascript复制autoninja -C out/Default system_webview_apk
再次安装,可以成功。
可以预料,后面还会继续踩坑。没办法,只能遇坑填坑,这不就是程序员的工作职责吗?