前言

CronetChromium 浏览器中用于网络请求的库,支持 HTTP1/2SPDYQUIC 等协议。并且还支持移动端。

需要注意的是编译 Android 需要在 Linux 下进行,并且只支持 UbuntuDebian 的发行版,其它的不支持。

下载

由于 CronetChromium 的一部分,所以我们需要下载 Chromium 的源码。

Chromium 是谷歌开源的项目,所以下载 Chromium 需要用到谷歌提供的工具 depot_tools 来进行下载

1
2
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PATH:$(pwd)/depot_tools" # 设置环境变量,可以写到 .bashrc 中,这样就不用每次都执行一遍

如果到了这一步没有出现问题,就可以进行下一步,如果下载不下来,需要配置 git 代理

depot_tools 下载好了,就可以下载 Chromium 的源码了,这就要用到 depot_tools 中的 fetch 工具了

1
2
mkdir ~/chromium && cd ~/chromium
fetch --nohooks --no-history android

--no-history 可以忽略历史,加快下载

--nohooks 会在 fetch 结束后帮你执行下载一些依赖的二进制。

这个过程比较漫长,取决于网速,网速快大概一个小时可以搞定,慢一点有可能会失败。

失败了可以看看 chromium 文件夹中有没有 .gclient ,如果有可以使用 gclient sync 继续下载,如果没有那就需要重新 fetch

代码下载好之后需要安装一些额外的依赖,谷歌也提供了工具,我们只需要直接执行就可以

1
2
cd ~/chromium/src
./build/install-build-deps-android.sh

编译

Debug 版本

1
2
3
cd ~/chromium/src
./components/cronet/tools/cr_cronet.py gn --out_dir=out/Cronet
ninja -C out/Cronet cronet_package

编译一遍大概在30分钟的样子,还不算很久

Release 版本

1
2
3
cd ~/chromium/src
./components/cronet/tools/cr_cronet.py gn --release
ninja -C out/Release cronet_package

指定编译的CPU架构

在进行编译的时候默认是 ARMv7 32位,如果你需要指定编译的CPU架构,需要在 chromium/src/out/Cronet/args.gn 中设置CPU的架构

1
target_cpu="arm64"

通常有以下几个值

  • ARMv8 64位: target_cpu="arm64"
  • ARMv7 64位: target_cpu="arm"
  • x86 64位: target_cpu="x64"
  • x86 32位: target_cpu="x32"

使用

编译之后所有的 jarout/Cronet/cronet/ 目录下, soout/Cronet/cronet/libs 目录下

新建一个 Android 项目,把 cronet_api.jarcronet_impl_common_java.jarcronet_impl_native_java.jarcronet_impl_platform_java.jar 拷贝到 app/libs 下面

然后需要把 armeabi-v7a/libcronet.95.0.4638.50.so 拷贝到 src/main/jniLibs 接下来就可以开始用 cronet 进行网络请求了

MainActivity 中加入如下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private ByteArrayOutputStream mBytesReceived = new ByteArrayOutputStream();
private WritableByteChannel mReceiveChannel = Channels.newChannel(mBytesReceived);

public void cronetRequest() {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    CronetEngine engine = new CronetEngine.Builder(this)
        .setStoragePath(context.getFilesDir().getAbsolutePath())
        .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
        .enableHttp2(true)
        .enableQuic(true)
        .enableBrotli(true)
        .build();
    engine.newUrlRequestBuilder("https://httpbin.org/get", new UrlRequest.Callback() {

        @Override
        public void onRedirectReceived(
                UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
            Log.d(TAG, "onRedirectReceived");
            request.followRedirect();
        }

        @Override
        public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
            Log.i(TAG, "Response Started");
            request.read(ByteBuffer.allocateDirect(64 * 1024));
        }

        @Override
        public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
            byteBuffer.flip();
            Log.i(TAG, "onReadCompleted");

            try {
                mReceiveChannel.write(byteBuffer);
            } catch (IOException e) {
                Log.i(TAG, "IOException during ByteBuffer read. Details: ", e);
            }
            byteBuffer.clear();
            request.read(byteBuffer);
        }

        @Override
        public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
            Log.i(TAG, "Request Completed, status code is " + info.getHttpStatusCode()
                    + ", total received bytes is " + info.getReceivedByteCount());
            Log.d(TAG, mBytesReceived.toString());
        }

        @Override
        public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) {
            Log.i(TAG, "****** onFailed, error is: " + error.getMessage());

            if (mHttpCallback != null)
                mHttpCallback.onResponse(new HttpRequestAdapter(request),
                        info.getHttpStatusCode(), new HttpResponseAdaptor(info, null));
        }
    }, executorService).build().start();
}

运行之后就可以在控制台看到输出结果了

如果你碰到 java.lang.UnstatisfiedLinkError: dlopen failed: library "libcronet.95.0.4638.50.so" not found 需要在 app/build.gradle 中加入如下配置

1
2
3
4
5
6
7
android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a'
        }
    }
}

切换到指定的tag

由于 chromium 的代码随时都可能更新,所以我们希望可以在一个稳定的tag上进行开发,这就需要把源码切换到指定的tag上,并且需要把 depot_tools 也切换到匹配的版本上。

切换到指定的tag上

假设我们需要切换到 95.0.4638.50

1
2
3
cd ~/chromium/src
git fetch origin 95.0.4638.50
git checkout 95.0.4638.50

获取匹配的 depot_tools

chromium 切换之后,我们需要把 depot_tools 也切换到对应的版本上,可以通过如下命令

1
2
3
4
cd ~/chromium/src
COMMIT_DATE=$(git log -n 1 --pretty=format:%ci)
cd ~/depot_tools
git checkout $(git rev-list -n 1 --before="$COMMIT_DATE" main)

同步依赖

depot_tools 切换好之后,需要把依赖也同步到对应的版本。

由于 gclient 会自动更新 depot_tools ,所以在同步依赖之前需要禁用 depot_tools 的自动更新,然后再用 gclient 进行同步。

1
2
3
4
export DEPOT_TOOLS_UPDATE=0     # 禁用 depot_tools 自动更新
cd ~/chromium/src
git clean -ffd                  # 清空git工作目录,以免发生冲突
gclient sync -D --force --reset --with_branch_heads

碰到的错误

FAILED: gen/components/cronet/android/generate_javadoc.zip

1
2
3
4
5
6
7
8
9
FAILED: gen/components/cronet/android/generate_javadoc.zip
python3 ../../components/cronet/tools/generate_javadoc.py --output-dir cronet
--input-dir ../../components/cronet --overview-file cronet/README.html
--readme-file ../../components/cronet/README.md
--depfile gen/components/cronet/android/generate_javadoc.d
--zip-file gen/components/cronet/android/generate_javadoc.zip
--android-sdk-jar ../../third_party/android_sdk/public/platforms/android-31/android.jar
--support-annotations-jar lib.java/third_party/androidx/androidx_annotation_annotation.jar
--input-src-jar cronet/cronet_api-src.jar

该问题是没有安装 JDK 导致的,所以只需要安装一下 JDK 就可以解决了

1
sudo apt install openjdk-8-jdk

参考