Android VM 的启动流程以及 JNI 的通信原理

详细分析在 zygote 的启动流程中, Android 的 Art/Dalvik VM 是如果启动的;以及 Android 为支持 JNI 调用做了哪些工作。

Init

Android 系统在启动之后, Linux 内核的启动进程 *init 进程*将会启动 system/bin/app_process 程序, 其对应的源码为 app_main.cpp, app_process 被执行时就会从 app_main.cppmain 方法开始执行:

int main(int argc, char* const argv[]) {
    // 创建 AppRuntime
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    //...
    // 通过 zygote 标志区分不同进程
    if (zygote) {
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    }
    //...
}

AppRuntime 继承自 AndroidRuntime:

class AppRuntime : public AndroidRuntime
{
public:
    AppRuntime(char* argBlockStart, const size_t argBlockLength)
        : AndroidRuntime(argBlockStart, argBlockLength), mClass(NULL) {
        }
}

AndroidRuntime 的构造函数:

AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength) :
        mExitWithoutCleanup(false),
        mArgBlockStart(argBlockStart),
        mArgBlockLength(argBlockLength)
{
    SkGraphics::Init();
    assert(gCurRuntime == NULL);        // 整个系统中只能有一个 AndroidRuntime
    gCurRuntime = this;
}
  • 初始化 Skia 图像库
  • 将 AndroidRuntime 对象存在全局的 gCurRuntime 变量中

AndroidRuntime#start

AndroidRuntime 的构造函数中没有涉及 JNI 的内容。于是接着看 runtime.start 的执行:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    //...
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
        return;
    }
    onVmCreated(env);
    //...
}

libnativehelper/include/nativehelper/JniInvocation.h 中的 JniInvocation 是JNI调用的一个工具类。

class JniInvocation 的声明:

class JniInvocation final {
 public:
  JniInvocation() {
    // 构造时创建 JniInvocationImpl 实例
    impl_ = JniInvocationCreate();
  }

  bool Init(const char* library) {
    // 代理给 impl 执行
    return JniInvocationInit(impl_, library) != 0;
  }

  static const char* GetLibrary(const char* library, char* buffer) {
    return JniInvocationGetLibrary(library, buffer);
    //const char* JniInvocationGetLibrary(const char* library, char* buffer) {
    //    return JniInvocationImpl::GetLibrary(library, buffer);
    //}
    // 所以也是代理给 impl 执行
  }

 private:
  static const char* GetLibrary(const char* library, char* buffer, bool (*is_debuggable)(),
                                int (*get_library_system_property)(char* buffer));
  /*
  const char* JniInvocation::GetLibrary(const char* library,
                                      char* buffer,
                                      bool (*is_debuggable)(),
                                      int (*get_library_system_property)(char* buffer)) {
       return JniInvocationImpl::GetLibrary(library, buffer, is_debuggable, get_library_system_property);
       所以也是代理给 impl 执行
  }
  */

  JniInvocationImpl* impl_;
};

JniInvocation 的所有操作其实都是代理给 JniInvocationImpl 的。

class JniInvocationImpl 的声明:

struct JniInvocationImpl final {
 public:
  JniInvocationImpl();
  ~JniInvocationImpl();

  bool Init(const char* library);

  static const char* GetLibrary(const char* library,
                                char* buffer,
                                bool (*is_debuggable)() = IsDebuggable,
                                int (*get_library_system_property)(char* buffer) = GetLibrarySystemProperty);

  static JniInvocationImpl& GetJniInvocation();

  jint JNI_GetDefaultJavaVMInitArgs(void* vmargs);
  jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args);
  jint JNI_GetCreatedJavaVMs(JavaVM** vms, jsize size, jsize* vm_count);

 private:
  bool FindSymbol(FUNC_POINTER* pointer, const char* symbol);

  // 全局的单例
  static JniInvocationImpl* jni_invocation_;

  // 通过 dlopen 加载动态链接库返回的句柄
  void* handle_;
  // 持有通过 dlopen 加载的库中的函数指针
  // JNI_GetDefaultJavaVMInitArgs, JNI_CreateJavaVM, JNI_GetCreatedJavaVMs. 是 libart 中包含的方法
  jint (*JNI_GetDefaultJavaVMInitArgs_)(void*);
  jint (*JNI_CreateJavaVM_)(JavaVM**, JNIEnv**, void*);
  jint (*JNI_GetCreatedJavaVMs_)(JavaVM**, jsize, jsize*);
};

class JniInvocationImpl 的实现 libnativehelper/JniInvocation.cpp:

构造函数:

JniInvocationImpl* JniInvocationImpl::jni_invocation_ = NULL;

// 参数列表赋值, 将几个字段赋值 NULL
JniInvocationImpl::JniInvocationImpl() :
    handle_(NULL),
    JNI_GetDefaultJavaVMInitArgs_(NULL),
    JNI_CreateJavaVM_(NULL),
    JNI_GetCreatedJavaVMs_(NULL) {
  // 将当前实例存到全局的 jin_invocation_ 中
  jni_invocation_ = this;
}

通过 JniInvocationImpl 的构造函数能看出,JniInvocationImpl 的实例在一个进程中只会存在一个单例对象(jni_invocation_)。在 AndroidRuntime 初始化完成之后,之后系统中的所有 JNI 调用都会通过 jni_invocation_ 这个单例对象执行。当然因为应用进程都是 fork 自 zygote 进程,所以每个应用的进程都会拥有一个jin_invocation_ 对象。

Jni Invocation Init 过程

bool JniInvocationImpl::Init(const char* library) {
  char buffer[PROP_VALUE_MAX];
  library = GetLibrary(library, buffer);
  handle_ = OpenLibrary(library);
  // 加载之后,判断是否能找到一些符号对应的函数指针,验证加载是否成功
  if (!FindSymbol(reinterpret_cast<FUNC_POINTER*>(&JNI_GetDefaultJavaVMInitArgs_), 
                  "JNI_GetDefaultJavaVMInitArgs")) {
    return false;
  }
  if (!FindSymbol(reinterpret_cast<FUNC_POINTER*>(&JNI_CreateJavaVM_),
                  "JNI_CreateJavaVM")) {
    return false;
  }
  if (!FindSymbol(reinterpret_cast<FUNC_POINTER*>(&JNI_GetCreatedJavaVMs_),
                  "JNI_GetCreatedJavaVMs")) {
    return false;
  }
  return true;
}

GetLibarary 对传入的 library 参数做重新赋值,在非*debug*模式下会将library都设置为默认的 *libart.so*。

static const char* kLibraryFallback = "libart.so";
const char* JniInvocationImpl::GetLibrary(const char* library, char* buffer, bool (*is_debuggable)()) {
  const char* default_library;
  if (!is_debuggable()) {
    library = kLibraryFallback;
    default_library = kLibraryFallback;
  }else {/*...*/}
  return library;
}

OpenLibrary

void* OpenLibrary(const char* filename) {
#ifdef _WIN32
  //...
#else
  // Android 会走 dlopen 方式
  const int kDlopenFlags = RTLD_NOW | RTLD_NODELETE;
  return dlopen(filename, kDlopenFlags);
#endif
}

The function dlopen() loads the dynamic shared object (shared library) file named by the null-terminated string filename and returns an opaque “handle” for the loaded object. This handle is employed with other functions in the dlopen API, such as dlsym(3),dladdr(3), dlinfo(3), and dlclose().

dlopen 方法用于加载动态分享目标文件(.so .a),并返回一个 handle。返回的 handle 可供其他 dlopen 库中的方法使用。

JniInvocationImpl#OpenLibrary 调用 dlopen 时传入了两个 flag:

  • RTLD_NOW: 需要在dlopen返回前,解析出所有未定义符号,如果解析不出来,在dlopen会返回NULL
  • RTLD_NODELETE: 在dlclose()期间不卸载库,并且在以后使用dlopen()重新加载库时不初始化库中的静态变量。使用这个 flag 的目的在于: 确保*libart.so*在关闭时不会被取消映射。因为即使在 JNI_DeleteJavaVM 调用之后,某些线程仍可能尚未完成退出,如果卸载该库,则可能导致段错误

FindSymbol

FindSymbol 将目标符号对应的函数指针存到 pointer 函数指针中。

bool JniInvocationImpl::FindSymbol(FUNC_POINTER* pointer, const char* symbol) {
  *pointer = GetSymbol(handle_, symbol);
  if (*pointer == NULL) {
    ALOGE("Failed to find symbol %s: %s\n", symbol, GetError().c_str());
    CloseLibrary(handle_);
    handle_ = NULL;
    return false;
  }
  return true;
}

GetSymbol 方法接收 dlopen 返回的 handle, 和要查找的目标符号。然后使用 dlsym 系统调用从dlopen加载的库中找出符号对应的内存地址。

FUNC_POINTER GetSymbol(void* handle, const char* symbol) {
#ifdef _WIN32
  //...
#else
  return dlsym(handle, symbol);
#endif
}

returns the address of the code or data location specified by the null-terminated character string symbol. Which libraries and bundles are searched depends on the handle parameter. If dlsym() is called with a handle, returned by dlopen() then only that image and any libraries it depends on are searched for symbol.

dlsym 返回匹配目标符号的方法代码或数据在内存中的地址。如果调用 dlsym 传递的 handle 参数是由 dlopen 返回的,那么就只会在 dlopen 加载的库中寻找目标符号。

启动 VM

加载了 libart.so 之后,zygote 进程中就拥有了 libart.so 中的所有方法,libart.so 中的 JNI_CreateJavaVM 等方法的函数指针也已经在 FindSymbol 时存到了 JniInvocationImpl 的对应字段中。

AndroidRuntime::start 中调用startVm启动虚拟机:

if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
    return;
}

最终会调用到

jint JniInvocationImpl::JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  return JNI_CreateJavaVM_(p_vm, p_env, vm_args);
}

JniInvocatinImpl 的 JNI_CreateJavaVM_ 字段是一个函数指针,所以调用它就相当于调用它指向的函数: libart.so 中的 JNI_CreateJavaVM

art/runtime/jni/java_vm_ext.cc 中定义了 JNI_CreateJavaVM 方法,会被编译进 libart.so 中。

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  // 解析启动VM的参数
  const JavaVMInitArgs* args = static_cast<JavaVMInitArgs*>(vm_args);
  RuntimeOptions options;
  for (int i = 0; i < args->nOptions; ++i) {
    JavaVMOption* option = &args->options[i];
    //...
  }

  // 创建 Runtime
  if (!Runtime::Create(options, ignore_unrecognized)) {
    return JNI_ERR;
  }

  // 加载需要用到的 native 库
  // 这些库的名字放在 /etc/public.libraries.txt 中
  // 这些库的加载直接通过 dlopen 做的
  // 最终加载的地方在 art/libnativeloader/library_namespaces.cpp LibraryNamespaces::Initialize
  android::InitializeNativeLoader();

  Runtime* runtime = Runtime::Current();
  // 启动虚拟机
  bool started = runtime->Start();
  if (!started) {
    delete Thread::Current()->GetJniEnv();
    delete runtime->GetJavaVM();
    LOG(WARNING) << "CreateJavaVM failed";
    return JNI_ERR;
  }

  *p_env = Thread::Current()->GetJniEnv();
  *p_vm = runtime->GetJavaVM();
  return JNI_OK;
}

总结

init 进程会调用 app_process 程序,app_process 程序在执行时会 fork init 进程作为 zygote 进程, 然后会创建 AndroidRuntime, 接着 AndroidRuntime 会创建 JniInvocation, 并执行 JniInvocation#Init 方法去加载 libart.so, libart.so 包含了 VM 的创建方法(比如JNI_CreateJavaVM)等。所以在 JniInvocation Init 成功之后,就会执行 startVm 创建虚拟机。startVm 方法会调用到 libart.so 的 JNI_CreateJavaVM 方法。


dlopen 实践

目录结构:

.
├── lib.c
└── main.c

实现 lib

#include <stdio.h>

void hello() {
   printf("Hello!\n");
}

编译为动态链接库 lib.so: gcc -shared lib.c -o lib.so

动态加载 lib.so

#include <dlfcn.h>

int main() {
    const char* library = "lib.so";
    const char* symbol = "hello";
    void(*hello)();
    void* handle = dlopen(library, RTLD_NOW);
    void* hello_func = dlsym(handle, symbol);
    hello = hello_func;
    hello();
    return 0;
}

编译然后执行 gcc main.c -o main && ./main,会输出 Hello!

既然dlopen这么强,那我们是不是可以在 Android 上随意去动态加载一个动态链接库。在 Android JNI 环境下试下从 SD 卡通过 dlopen 加载一个动态链接库。

🤦‍♂️ 加载失败: E/linker: library “sdcard/jy.so” (“/storage/emulated/0/jy.so”) needed or dlopened by “/data/app/xx.playground-EX5qy0zftySydkkVUVoXdQ==/lib/x86_64/libjni_lib.so” is not accessible for the namespace: [name=“classloader-namespace”, ld_library_paths=“”, default_library_paths=“/data/app/xx.playground-EX5qy0zftySydkkVUVoXdQ==/lib/x86_64:/data/app/xx.playground-EX5qy0zftySydkkVUVoXdQ==/base.apk!/lib/x86_64”, permitted_paths=“/data:/mnt/expand:/data/data/xx.playground”]

public.libraries.txt

查看模拟器(因为只有模拟器直接就有root权限🤦‍♂️)的 etc/public.libraries.txt 能看到一般会加载哪些库。

generic_x86_64:/etc $ cat public.libraries.txt
libandroid.so
libaaudio.so
libc.so
libcamera2ndk.so
libdl.so
libEGL.so
libGLESv1_CM.so
libGLESv2.so
libGLESv3.so
libicui18n.so
libicuuc.so
libjnigraphics.so
liblog.so
libmediandk.so
libm.so
libnativewindow.so
libneuralnetworks.so
libOpenMAXAL.so
libOpenSLES.so
libRS.so
libstdc++.so
libsync.so
libvulkan.so
libwebviewchromium_plat_support.so
libz.so