Chromium net android移植指南(上篇)

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代码库的根目录)命令:

$ gclient runhooks
$ cd src
$ ./build/install-build-deps.sh
$ ./build/install-build-deps-android.sh

这些命令帮我们下载构建Chromium所需的工具链。

对构建进行配置

构建之前需要对构建进行配置。编辑out/Default/args.gn文件,参照 Chromium Android编译指南 的说明,输入如下内容:

target_os = "android"
target_cpu = "arm"  # (default)
is_debug = false  # (default)

# Other args you may want to set:
is_component_build = true
is_clang = false
symbol_level = 1  # Faster build with fewer symbols. -g1 rather than -g2
enable_incremental_javac = false  # Much faster; experimental
android_ndk_root = "~/dev_tools/Android/android-ndk-r10e"
android_sdk_root = "~/dev_tools/Android/sdk"
android_sdk_build_tools_version = "23.0.2"

disable_file_support = true
disable_ftp_support = true
enable_websockets = false

use_platform_icu_alternatives = true

关键点主要有如下几个:

  • is_component_build被置为ture。Chromium文档 (gn args --list out/Default/输出) 中对这个配置项的说明如下:

    is_component_build  Default = false
     //build/config/BUILDCONFIG.gn:162
     Component build. Setting to true compiles targets declared as "components"
     as shared libraries loaded dynamically. This speeds up development time.
     When false, components will be linked statically.
    

    将这个配置项置为true,会使以components声明的targets被编译为动态链接库,否则它们将会被编译为静态库。这里我们需要将net等模块编译为动态链接库,因而将该配置项置为true。

  • is_debug被置为false,表示编译非Debug版。在这种情况下,enable_incremental_javac同样要被置为false。否则在执行gn gen out/Default生成 .ninja 文件时会报出如下的error:

    ERROR 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/输出) 如下:
android_ndk_major_version  Default = "10"
    //build/config/android/config.gni:65

android_ndk_root  Default = "//third_party/android_tools/ndk"
    //build/config/android/config.gni:66

android_ndk_version  Default = "r10e"
    //build/config/android/config.gni:67

可见默认的NDK版本是r10e的。因而我们也选用r10e版的标准NDK。

  • is_clang选项被置为了false。Chromium文档 (gn args --list out/Default/输出) 中对这个配置项的说明如下:

is_clang  Default = false
    //build/config/BUILDCONFIG.gn:141
    Set to true when compiling with the Clang compiler. Typically this is used
    to configure warnings.

这个配置项用于指定是否要用Clang编译器。

  • disable_file_supportdisable_ftp_supportenable_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文件退出,执行如下命令:

$ gn gen out/Default

产生ninja构建所需的 .ninja 文件。

编译Chromium net

输入如下命令编译net模块:

$ ninja -C out/Default net

这个命令会编译net模块,及其依赖的所有模块,包括base,crypto,boringssl,protobuf,url等。看一下我们编译的成果:

$ ls -alh out/Default/ | grep so$
-rwxrwxr-x  1 chrome chrome 967K 11月 11 15:34 libbase.cr.so
-rwxrwxr-x  1 chrome chrome 785K 11月 11 15:34 libboringssl.cr.so
-rwxrwxr-x  1 chrome chrome  50K 11月 11 15:34 libcrcrypto.cr.so
-rwxrwxr-x  1 chrome chrome 3.3M 11月 11 15:36 libnet.cr.so
-rwxrwxr-x  1 chrome chrome  90K 11月 11 15:34 libprefs.cr.so
-rwxrwxr-x  1 chrome chrome 154K 11月 11 15:34 libprotobuf_lite.cr.so
-rwxrwxr-x  1 chrome chrome  70K 11月 11 15:36 liburl.cr.so

总共7个共享库文件,总大小5.4M。

使用Chromium net

将Chromium net导入Android应用

在我们工程的app模块的jni目录下为chromium创建文件夹app/src/main/jni/third_party/chromium/libsapp/src/main/jni/third_party/chromium/include,分别用于存放我们编译出来的共享库和net等模块导出的头文件及这些头文件include的其它头文件。

这里我们将编译出来的所有so文件拷贝到app/src/main/jni/third_party/chromium/libs/armeabiapp/src/main/jni/third_party/chromium/libs/armeabi-v7a目录下:

cp out/Default/*.so ~/MyApplication/app/src/main/jni/third_party/chromium/libs/armeabi/
cp out/Default/*.so ~/MyApplication/app/src/main/jni/third_party/chromium/libs/armeabi-v7a/

提取导出头文件

为了使用net模块提供的API,不可避免地要将net导出的头文件引入我们的项目。要做到这些,需要从chromium工程提取net导出的头文件。不像许多其它的C/C++项目,源代码文件、私有头文件及导出头文件存放的位置被很好地做了区隔,chromium各个模块的头文件和源代码文件都是放在一起的。这给我们提取导出头文件的工作带来了一点麻烦。

好在有gn工具。gn工具提供的desc命令(参考 GN的使用 - GN工具 一文)的输出有如下这样两段:

$ gn desc out/Default/ net
Target //net:net
Type: shared_library
Toolchain: //build/toolchain/android:arm
......
sources
  //net/base/address_family.cc
  //net/base/address_family.h
......

public
  [All headers listed in the sources are public.]

我们可以据此编写脚本提取net模块的头文件。

我们可以为脚本传入[chromium代码库的src目录路径][输出目录的路径][模块名],及[保存头文件的目标目录路径]作为参数,以提取头文件,[保存头文件的目标目录路径]参数缺失时默认使用当前目录,如:

$ cd /media/data/MyProjects/MyApplication/app/src/main/jni/third_party/chromium/include
$ chromium_mod_headers_extracter.py ~/data/chromium_android/src  out/Default net
$ chromium_mod_headers_extracter.py ~/data/chromium_android/src  out/Default base
$ chromium_mod_headers_extracter.py ~/data/chromium_android/src  out/Default url

利用我们的脚本,提取net、base和url这三个模块导出的头文件。

这里一并将该脚本的完整内容贴出来供大家参考:

#!/usr/bin/env python

import os
import shutil
import sys

def print_usage_and_exit():
    print sys.argv[0] + " [chromium_src_root]" + "[out_dir]" + " [target_name]" + " [targetroot]"
    exit(1)

def copy_file(src_file_path, target_file_path):
    if os.path.exists(target_file_path):
        return
    if not os.path.exists(src_file_path):
        return
    target_dir_path = os.path.dirname(target_file_path)
    if not os.path.exists(target_dir_path):
        os.makedirs(target_dir_path)

    shutil.copy(src_file_path, target_dir_path)

def copy_all_files(source_dir, all_files, target_dir):
    for one_file in all_files:
        source_path = source_dir + os.path.sep + one_file
        target_path = target_dir + os.path.sep + one_file
        copy_file(source_path, target_path)

if __name__ == "__main__":
    if len(sys.argv) < 4 or len(sys.argv) > 5:
        print_usage_and_exit()
    chromium_src_root = sys.argv[1]
    out_dir = sys.argv[2]
    target_name = sys.argv[3]
    target_root_path = "."
    if len(sys.argv) == 5:
        target_root_path = sys.argv[4]
    target_root_path = os.path.abspath(target_root_path)

    os.chdir(chromium_src_root)

    cmd = "gn desc " + out_dir + " " + target_name
    outputs = os.popen(cmd).readlines()
    source_start = False
    all_headers = []

    public_start = False
    public_headers = []

    for output_line in outputs:
        output_line = output_line.strip()
        if output_line.startswith("sources"):
            source_start = True
            continue
        elif source_start and len(output_line) == 0:
            source_start = False
            continue
        elif source_start and output_line.endswith(".h"):
            output_line = output_line[1:]
            all_headers.append(output_line)
        elif output_line == "public":
            public_start = True
            continue
        elif public_start and len(output_line) == 0:
            public_start = False
            continue
        elif public_start:
            public_headers.append(output_line)

    if len(public_headers) == 1:
        public_headers = all_headers
    if len(public_headers) > 1:
        copy_all_files(chromium_src_root, public_headers, target_dir=target_root_path)

此外,前面的提取过程会遗漏一些必须的头文件。主要是如下几个:

base/callback_forward.h
base/message_loop/timer_slack.h
base/files/file.h
net/cert/cert_status_flags_list.h
net/cert/cert_type.h
net/base/privacy_mode.h
net/websockets/websocket_event_interface.h
net/quic/quic_alarm_factory.h

对于这些文件,我们直接从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宏的定义为:

#if 0
#define FRIEND_TEST_ALL_PREFIXES(test_case_name, test_name) \
  FRIEND_TEST(test_case_name, test_name); \
  FRIEND_TEST(test_case_name, DISABLED_##test_name); \
  FRIEND_TEST(test_case_name, FLAKY_##test_name)
#else
#define FRIEND_TEST_ALL_PREFIXES(test_case_name, test_name)
#endif

这样就可以注释掉类定义中专门为gtest插入的代码。

Chromium net的简单使用

参照chromium/src/net/tools/get_server_time/get_server_time.cc的代码,来编写简单的demo程序。

首先是JNI的Java层代码:

package com.example.hanpfei0306.myapplication;

public class NetUtils {
    static {
        System.loadLibrary("neteasenet");
    }
    private static native void nativeSendRequest(String url);

    public static void sendRequest(String url) {
        nativeSendRequest(url);
    }
}

然后是JNI的native实现,app/src/main/jni/src/NetJni.cpp

//
// Created by hanpfei0306 on 16-8-4.
//

#include <stdio.h>
#include <net/base/network_delegate_impl.h>

#include "jni.h"

#include "base/at_exit.h"
#include "base/json/json_writer.h"
#include "base/message_loop/message_loop.h"
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/values.h"
#include "net/http/http_response_headers.h"
#include "net/proxy/proxy_config_service_fixed.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_builder.h"
#include "net/url_request/url_request_context_getter.h"
#include "net/url_request/url_request.h"

#include "JNIHelper.h"

#define TAG "NetUtils"

// Simply quits the current message loop when finished. Used to make
// URLFetcher synchronous.
class QuitDelegate : public net::URLFetcherDelegate {
public:
QuitDelegate() {}

~QuitDelegate() override {}

// net::URLFetcherDelegate implementation.
void OnURLFetchComplete(const net::URLFetcher* source) override {
LOGE("OnURLFetchComplete");
base::MessageLoop::current()->QuitWhenIdle();
int responseCode = source->GetResponseCode();

const net::URLRequestStatus status = source->GetStatus();
if (status.status() != net::URLRequestStatus::SUCCESS) {
LOGW("Request failed with error code: %s", net::ErrorToString(status.error()).c_str());
return;
}

const net::HttpResponseHeaders* const headers = source->GetResponseHeaders();
if (!headers) {
LOGW("Response does not have any headers");
return;
}
size_t iter = 0;
std::string header_name;
std::string date_header;
while (headers->EnumerateHeaderLines(&iter, &header_name, &date_header)) {
LOGW("Got %s header: %s\n", header_name.c_str(), date_header.c_str());
}

std::string responseStr;
if(!source->GetResponseAsString(&responseStr)) {
LOGW("Get response as string failed!");
}

LOGI("Content len = %lld, response code = %d, response = %s",
source->GetReceivedResponseContentLength(),
source->GetResponseCode(),
responseStr.c_str());
}

void OnURLFetchDownloadProgress(const net::URLFetcher* source,
int64_t current,
int64_t total) override {
LOGE("OnURLFetchDownloadProgress");
}

void OnURLFetchUploadProgress(const net::URLFetcher* source,
int64_t current,
int64_t total) override {
LOGE("OnURLFetchUploadProgress");
}

private:
DISALLOW_COPY_AND_ASSIGN(QuitDelegate);
};

// NetLog::ThreadSafeObserver implementation that simply prints events
// to the logs.
class PrintingLogObserver : public net::NetLog::ThreadSafeObserver {
public:
PrintingLogObserver() {}

~PrintingLogObserver() override {
// This is guaranteed to be safe as this program is single threaded.
net_log()->DeprecatedRemoveObserver(this);
}

// NetLog::ThreadSafeObserver implementation:
void OnAddEntry(const net::NetLog::Entry& entry) override {
// The log level of the entry is unknown, so just assume it maps
// to VLOG(1).
const char* const source_type = net::NetLog::SourceTypeToString(entry.source().type);
const char* const event_type = net::NetLog::EventTypeToString(entry.type());
const char* const event_phase = net::NetLog::EventPhaseToString(entry.phase());
std::unique_ptr<base::Value> params(entry.ParametersToValue());
std::string params_str;
if (params.get()) {
base::JSONWriter::Write(*params, ¶ms_str);
params_str.insert(0, ": ");
}
#ifdef DEBUG_ALL
LOGI("source_type = %s (id = %u): entry_type = %s : event_phase = %s params_str = %s",
source_type, entry.source().id, event_type, event_phase, params_str.c_str());
#endif
}

private:
DISALLOW_COPY_AND_ASSIGN(PrintingLogObserver);
};

// Builds a URLRequestContext assuming there's only a single loop.
static std::unique_ptr<net::URLRequestContext> BuildURLRequestContext(net::NetLog *net_log) {
net::URLRequestContextBuilder builder;
builder.set_net_log(net_log);
//#if defined(OS_LINUX)
// On Linux, use a fixed ProxyConfigService, since the default one
// depends on glib.
//
// TODO(akalin): Remove this once http://crbug.com/146421 is fixed.
builder.set_proxy_config_service(
base::WrapUnique(new net::ProxyConfigServiceFixed(net::ProxyConfig())));
//#endif
std::unique_ptr<net::URLRequestContext> context(builder.Build());
context->set_net_log(net_log);
return context;
}

static void NetUtils_nativeSendRequest(JNIEnv* env, jclass, jstring javaUrl) {
const char* native_url = env->GetStringUTFChars(javaUrl, NULL);
LOGW("Url: %s", native_url);
base::AtExitManager exit_manager;
LOGW("Url: %s", native_url);

GURL url(native_url);
if (!url.is_valid() || (url.scheme() != "http" && url.scheme() != "https")) {
LOGW("Not valid url: %s", native_url);
return;
}
LOGW("Url: %s", native_url);

base::MessageLoopForIO main_loop;

QuitDelegate delegate;
std::unique_ptr<net::URLFetcher> fetcher =
net::URLFetcher::Create(url, net::URLFetcher::GET, &delegate);

net::NetLog *net_log = nullptr;
#ifdef DEBUG_ALL
net_log = new net::NetLog;
PrintingLogObserver printing_log_observer;
net_log->DeprecatedAddObserver(&printing_log_observer,
net::NetLogCaptureMode::IncludeSocketBytes());
#endif

std::unique_ptr<net::URLRequestContext> url_request_context(BuildURLRequestContext(net_log));
fetcher->SetRequestContext(
// Since there's only a single thread, there's no need to worry
// about when the URLRequestContext gets created.
// The URLFetcher will take a reference on the object, and hence
// implicitly take ownership.
new net::TrivialURLRequestContextGetter(url_request_context.get(),
main_loop.task_runner()));
fetcher->Start();
// |delegate| quits |main_loop| when the request is done.
main_loop.Run();

env->ReleaseStringUTFChars(javaUrl, native_url);
}

int jniRegisterNativeMethods(JNIEnv* env, const char *classPathName, JNINativeMethod *nativeMethods, jint nMethods) {
jclass clazz;
clazz = env->FindClass(classPathName);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, nativeMethods, nMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}

static JNINativeMethod gNetUtilsMethods[] = {
NATIVE_METHOD(NetUtils, nativeSendRequest, "(Ljava/lang/String;)V"),
};

void register_com_netease_volleydemo_NetUtils(JNIEnv* env) {
jniRegisterNativeMethods(env, "com/example/hanpfei0306/myapplication/NetUtils",
gNetUtilsMethods, NELEM(gNetUtilsMethods));
}

// DalvikVM calls this on startup, so we can statically register all our native methods.
jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
LOGE("JavaVM::GetEnv() failed");
abort();
}

register_com_netease_volleydemo_NetUtils(env);
return JNI_VERSION_1_6;
}

这个文件里,在nativeSendRequest()函数中调用chromium net执行网络请求,获取响应,并打印出响应的headers及content。



相关阅读:Chromium net android移植指南(下篇)

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者韩鹏飞授权发布。