Chromium浏览器的网络库是一个功能非常强大的网络库,它支持的网络协议非常多,除了常见的HTTP/1.1,它还支持HTTP/2,QUIC等比较新的协议。这里我们尝试将Chromium net网络库移植到Android平台,在我们的Android应用中跑起来。
移植Chromium net网络库有两种方式,一是将Chromium net网络库及其依赖的所有其它库编译为动态链接库,将这些so导入我们的Android应用,然后提取Chromium net网络库导出的头文件并导入我们的Android应用,我们自己编写JNI代码进而让Chromium net跑起来;二是利用Cronet。Cronet是对Chromium net网络库的封装,它为我们提供方便的Java接口。编译Cronet时,会将Cronet的JNI代码,Chromium net库及其依赖的所有其它库编译为一个单独的so,并将各个库的Java接口编译为jar包,我们可以将这些jar文件和so文件导入我们的Android应用,并调用Java接口。
这里我们会介绍这两种方式。
编译Chromium net模块
下载Chromium代码
首先要做的是下载完整的Chromium代码,这可以参考 Chromium Android编译指南 完成。然后执行(假设当前目录是chromium代码库的根目录)命令:
这些命令帮我们下载构建Chromium所需的工具链。
对构建进行配置
构建之前需要对构建进行配置。编辑out/Default/args.gn
文件,参照 Chromium Android编译指南 的说明,输入如下内容:
关键点主要有如下几个:
is_component_build
被置为ture。Chromium文档 (gn args --list out/Default/
输出) 中对这个配置项的说明如下:
|
|
将这个配置项置为true,会使以components
声明的targets被编译为动态链接库,否则它们将会被编译为静态库。这里我们需要将net等模块编译为动态链接库,因而将该配置项置为true。
is_debug
被置为false,表示编译非Debug版。在这种情况下,enable_incremental_javac
同样要被置为false。否则在执行gn gen out/Default
生成 .ninja 文件时会报出如下的error:123456789ERROR at //build/config/android/config.gni:136:3: Assertion failed.assert(!(enable_incremental_javac && !is_java_debug))^-----See //build/config/compiler/compiler.gni:5:1: whence it was imported.import("//build/config/android/config.gni")^-----------------------------------------See //BUILD.gn:11:1: whence it was imported.import("//build/config/compiler/compiler.gni")^--------------------------------------------配置android_ndk_root为标准NDK的路径,即直接从Google开发者站点下载的NDK包。这个配置指示构建系统,在编译时使用标准NDK工具链,而不是Chromium代码库中的NDK工具链。这主要是因为Chromium代码库中的NDK工具链与标准NDK工具链之间的差异,会导致我们在JNI代码中调用Chromium net函数时,出现诡异的链接诶问题。
NDK版本的选择也要注意。Chromium使用的默认NDK相关信息 (gn args --list out/Default/
输出) 如下:
|
|
可见默认的NDK版本是r10e
的。因而我们也选用r10e版的标准NDK。
is_clang
选项被置为了false。Chromium文档 (gn args --list out/Default/
输出) 中对这个配置项的说明如下:
|
|
这个配置项用于指定是否要用Clang编译器。
disable_file_support
、disable_ftp_support
和enable_websockets
分别被置为true、true和false。这几个设置主要是为裁剪需要。我们要禁掉chromium net对这几种协议的支持,以减小最终编译出来的so文件的大小。use_platform_icu_alternatives
被置为true。这个配置也是为了减小最终的so文件的总大小。ICU相关的几个so文件总和接近2M,通过将use_platform_icu_alternatives
置为true,指示不使用Chromium代码库中的ICU。
保存out/Default/args.gn
文件退出,执行如下命令:
产生ninja构建所需的 .ninja 文件。
编译Chromium net
输入如下命令编译net模块:
这个命令会编译net模块,及其依赖的所有模块,包括base,crypto,boringssl,protobuf,url等。看一下我们编译的成果:
总共7个共享库文件,总大小5.4M。
使用Chromium net
将Chromium net导入Android应用
在我们工程的app模块的jni目录下为chromium创建文件夹app/src/main/jni/third_party/chromium/libs
和app/src/main/jni/third_party/chromium/include
,分别用于存放我们编译出来的共享库和net等模块导出的头文件及这些头文件include的其它头文件。
这里我们将编译出来的所有so文件拷贝到app/src/main/jni/third_party/chromium/libs/armeabi
和app/src/main/jni/third_party/chromium/libs/armeabi-v7a
目录下:
提取导出头文件
为了使用net模块提供的API,不可避免地要将net导出的头文件引入我们的项目。要做到这些,需要从chromium工程提取net导出的头文件。不像许多其它的C/C++项目,源代码文件、私有头文件及导出头文件存放的位置被很好地做了区隔,chromium各个模块的头文件和源代码文件都是放在一起的。这给我们提取导出头文件的工作带来了一点麻烦。
好在有gn工具。gn工具提供的desc命令(参考 GN的使用 - GN工具 一文)的输出有如下这样两段:
我们可以据此编写脚本提取net模块的头文件。
我们可以为脚本传入[chromium代码库的src目录路径],[输出目录的路径],[模块名],及[保存头文件的目标目录路径]作为参数,以提取头文件,[保存头文件的目标目录路径]参数缺失时默认使用当前目录,如:
利用我们的脚本,提取net、base和url这三个模块导出的头文件。
这里一并将该脚本的完整内容贴出来供大家参考:
此外,前面的提取过程会遗漏一些必须的头文件。主要是如下几个:
对于这些文件,我们直接从chromium的代码库拷贝到我们的工程中对应的位置即可。
我们还需要引入chromium的build配置头文件build/build_config.h
。直接将chromium代码库中的对应文件拷贝过来,放到对应的位置。
将app/src/main/jni/third_party/chromium/include/base/gtest_prod_util.h
文件中对testing/gtest/include/gtest/gtest_prod.h
的include注释掉,同时修改FRIEND_TEST_ALL_PREFIXES宏的定义为:
这样就可以注释掉类定义中专门为gtest插入的代码。
Chromium net的简单使用
参照chromium/src/net/tools/get_server_time/get_server_time.cc
的代码,来编写简单的demo程序。
首先是JNI的Java层代码:
然后是JNI的native实现,app/src/main/jni/src/NetJni.cpp
:
这个文件里,在nativeSendRequest()函数中调用chromium net执行网络请求,获取响应,并打印出响应的headers及content。
配置Gradle
要在Android Studio中使用JNI,还需要对Gralde做一些配置文。这里需要对MyApplication/build.gradle
、MyApplication/gradle/wrapper/gradle-wrapper.properties
,和MyApplication/app/build.gradle
这几个文件做修改。
修改MyApplication/build.gradle
文件,最终的内容为:
在这个文件中配置gradle插件的版本为gradle-experimental:0.7.0
。
修改MyApplication/gradle/wrapper/gradle-wrapper.properties
文件,最终的内容为:
在这个文件中配置gradle的版本。
修改MyApplication/app/build.gradle
文件,最终的内容为:
关键点主要有如下这些:
- 为net、base和url这几个模块创建PrebuiltLibraries libs元素,并正确的设置对这些模块的依赖。
- 配置stl为”c++_shared”。
- cppFlags的”-std=gnu++11”选项必不可少。
- buildType下的debug和release,需要给它们ndk的abiFilters添加我们想要支持的ABI,而不是留空,以防止Android Studio为我们编译我们不打算支持的ABI的so,而出现找不到文件的问题。
- CFlags和cppFlags中除了配置头文件搜索路径的那两行之外,其它的内容,主要是从chromium的构建环境中提取的。方法为:
|
|
主要是build.gradle的cppFlags添加的那些宏定义,它们来自defines。如果这些配置,在编译chromium net so的环境,和构建我们的工程的环境之间存在差异,则很可能会导致运行期一些莫名奇妙的问题,比如意外的缓冲区溢出之类的。
Cronet移植
如我们前面提到的,Chromium已经有提供一个称为cronet的模块,封装chromium net,提供Java接口。使用这个模块将大大简化我们的移植工作。构建cronet所需步骤主要有:
- 基于前面看到的配置文件
out/Default/args.gn
,编辑该文件将is_component_build
配置选项置为false。 - 执行如下命令:1$ gn gen out/Default
产生ninja构建所需的 .ninja 文件。
- 执行如下命令生成cronet so文件:1$ ninja -C out/Default/ cronet
并将产生的libcronet.so文件导入我们的应用中。我们的应用的build.gradle将如下面这样:
|
|
- 执行如下命令生成cronet Java层代码的jar包:1$ ninja -C out/Default/ cronet_java
这将在out/Default/lib.java/的子目录下产生出多个jar文件,cronet_java.jar,cronet_api.jar,url_java.jar,base_java.jar,net_java.jar。将这些jar文件全部导入我们的应用中。
- 调用cronet提供的Java接口执行网络请求:
|
|
Done。