上一篇文章中我们介绍了 Android 4.4 新开发的运行时 ART 项目,其中的一个重要模快是 dex2oat,简单讲就是使用 LLVM 把 dex 文件编译成 oat 文件(Optimized ART?)。下面我们详细研究一下 dex2oat 的功能,以及他是如何被调用的。

一、dex2oat 简介

dex2oat 顾名思义 dex file to oat file,就是在新旧两种运行时文件的转换。他是一个可执行 ELF 文件,adb 连接手机以后也可以直接调用。dex2oat 的源码只有一个文件 /art/dex2oat/dex2oat.cc。dex2oat 有很多参数,并且实用方法打印到 logcat里面,看起来非常不便,我们直接看源码 Usage()。我挑了几个比较重要的参数看了一下,先介绍一下,调用过程中可能会用到。

dex2oat 用法

  • --dex-file=<dex-file>: specifies a .dex file to compile.: 需要转换的 dex 文件,也可以是 apk, jar,dex2oat 会找到里面的 classes.dex 进行转换。
  • --oat-file=<file.oat>: specifies the oat output destination via a filename.: 指定输出 oat 的文件名。
  • --boot-image=<file.art>: provide the image file for the boot class path.: 系统运行时工具类在 ART 下编译后的文件,他的例子是指向/system/framework/boot.art。但其实 boot.art 不在这个目录下,而是在/data/dalvik-cache/system@[email protected]。后面还会详细介绍这个 boot 文件。
  • --compiler-backend=(Quick|QuickGBC|Portable): select compiler backend": dex2oat 好像只处理了 Quick 和 Portable 两种编译 backend,暂时还不理解有什么区别,待以后继续研究。

其他参数大多都能从他的描述中知道用途,等以后用到的时候再详细看。

oat 文件在哪里?

我们知道选择 ART 作为运行时后,需要重启手机。然后系统会使用 dex2oat 把所有 App 编译成 oat 文件。那么 oat 都存在哪里?我怎么找不到?通过查看代码和开机的 log,发现 oat 文件原来在 /data/dalvik-cache/*@classes.dex。竟然和 odex 文件名字一样!检验一下文件格式:

$ file system@[email protected]@classes.dex
system@[email protected]@classes.dex: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped

果然是 ELF 文件。

二、调用过程分析

PackageManagerService 是负责 apk 包管理的服务,包括安装,检查,删除,更新等等所有与 apk 相关的服务。我在源码中研究了一下 dex2oat 详细的调用过程,在下面介绍过程的时候我打算从 PackageManagerService 入手,经过一系列的调用过程才真正执行 dex2oat。

PackageManagerService (PMS) 我们知道 PMS 服务启动之后会监控某些目录(/data/data/ 等)。如果目录多了某个文件,PMS 会调用一些方法对文件进行处理。扫描监控目录的方法是:scanDirLI(),在 PackageManagerService 的构造方法中调用,详见代码 /frameworks/base/services/java/com/android/server/pm/PackageManagerService.java

// Find base frameworks (resource packages without code).
mFrameworkInstallObserver = new AppDirObserver(
    frameworkDir.getPath(), OBSERVER_EVENTS, true, false);
mFrameworkInstallObserver.startWatching();
scanDirLI(frameworkDir, PackageParser.PARSE_IS_SYSTEM
        | PackageParser.PARSE_IS_SYSTEM_DIR,
        scanMode | SCAN_NO_DEX, 0);

// Collected privileged system packages.
File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app");
mPrivilegedInstallObserver = new AppDirObserver(
        privilegedAppDir.getPath(), OBSERVER_EVENTS, true, true);
mPrivilegedInstallObserver.startWatching();
    scanDirLI(privilegedAppDir, PackageParser.PARSE_IS_SYSTEM
            | PackageParser.PARSE_IS_SYSTEM_DIR
            | PackageParser.PARSE_IS_PRIVILEGED, scanMode, 0);

// Collect ordinary system packages.
File systemAppDir = new File(Environment.getRootDirectory(), "app");
mSystemInstallObserver = new AppDirObserver(
    systemAppDir.getPath(), OBSERVER_EVENTS, true, false);
mSystemInstallObserver.startWatching();
scanDirLI(systemAppDir, PackageParser.PARSE_IS_SYSTEM
        | PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);

// Collect all vendor packages.
File vendorAppDir = new File("/vendor/app");
mVendorInstallObserver = new AppDirObserver(
    vendorAppDir.getPath(), OBSERVER_EVENTS, true, false);
mVendorInstallObserver.startWatching();
scanDirLI(vendorAppDir, PackageParser.PARSE_IS_SYSTEM
        | PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);

从源码中可以看到,有四个地方调用 scanDirLI()。再调用之前有一个 AppDirObserver 类引起了我的注意,看一下是怎么对目录进行监控的。依然是在 PackageManagerService 中找到了 AppDirObserver 继承于 FileObserver。FileObserver 有一个 native 方法 startWatching 找到了他的实现 /frameworks/base/core/java/android/os/FileObserver.java

const char* path = env->GetStringUTFChars(pathString, NULL);
res = inotify_add_watch(fd, path, mask);
env->ReleaseStringUTFChars(pathString, path);

man 一下 inotify_add_watch:

INOTIFY_ADD_WATCH(2)       Linux Programmer's Manual      INOTIFY_ADD_WATCH(2)



NAME
       inotify_add_watch - add a watch to an initialized inotify instance

SYNOPSIS
       #include <sys/inotify.h>

       int inotify_add_watch(int fd, const char *pathname, uint32_t mask);

DESCRIPTION
       inotify_add_watch()  adds  a  new watch, or modifies an existing watch,
       for the file whose location is specified in pathname; the  caller  must
       have read permission for this file.  The fd argument is a file descrip‐
       tor referring to the inotify instance whose watch list is to  be  modi‐
       fied.   The  events  to  be monitored for pathname are specified in the
       mask bit-mask argument.  See inotify(7) for a description of  the  bits
       that can be set in mask.
...

原来 inotify_add_watch() 是监控目录的。

PackageManagerService.scanDirLI()

回到正题,我们从构造方法中找到了 scanDirLI(),现在来看一下 scanDirLI() 都做了什么:

for (i=0; i<files.length; i++) {
    File file = new File(dir, files[i]);
    if (!isPackageFilename(files[i])) {
        // Ignore entries which are not apk's
        continue;
    }
    PackageParser.Package pkg = scanPackageLI(file,
            flags|PackageParser.PARSE_MUST_BE_APK, scanMode, currentTime, null);

scanDirLI() 便利所有目录下的 apk 文件并调用 scanPackageLI(),我们继续。

PackageManagerService.scanPackageLI()

scanPackageLI() 先对文件的设置进行读取,还包含其他处理。我们跳过直接看

// Note that we invoke the following method only if we are about to unpack an application
PackageParser.Package scannedPkg = scanPackageLI(pkg, parseFlags, scanMode
      | SCAN_UPDATE_SIGNATURE, currentTime, user);

又调用了另外一个重载的 scanPackageLI()。在这个 scanPackageLI() 里面又进行了一系列的包的解析,都与我们的目标无关,直接跳到这里:

if ((scanMode&SCAN_NO_DEX) == 0) {
    if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
            == DEX_OPT_FAILED) {
        mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
        return null;
    }
}

在这里又调用 performDexOptLI() 来处理进行 dex 优化,参数就是这个 package。这里已经感觉到有点快到 dex2oat 的调用点了。我们继续看 performDexOptLI()。

PackageManagerService.performDexOptLI()

performDexOptLI 先检查 dex 是否需要优化,然后再有一些判断,忽略他,看到这里:

/* /frameworks/base/services/java/com/android/server/pm/PackageManagerService.java */
if (forceDex || dalvik.system.DexFile.isDexOptNeeded(path)) {
    if (!forceDex && defer) {
        if (mDeferredDexOpt == null) {
            mDeferredDexOpt = new HashSet<PackageParser.Package>();
        }
        mDeferredDexOpt.add(pkg);
        return DEX_OPT_DEFERRED;
    } else {
        Log.i(TAG, "Running dexopt on: " + pkg.applicationInfo.packageName);
        final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.   uid);
        ret = mInstaller.dexopt(path, sharedGid, !isForwardLocked(pkg));
        pkg.mDidDexOpt = true;
        performed = true;
    }
}

最后 mInstaller.dexopt 调用的优化 dex 的函数,Google 在这里把 dex2oat 和 dex2odex 统称为 dexopt。mInstaller 是 Installer 类。下面我们详细了解一下 Installer 的运行机制。

Installer

从 Installer 的代码中我们可以看到 connect(), disconnect(), readBytes(), writeCommand() 等方法。在 connect() 方法中可以看到,Installer 就是与 installd daemon 进行通信的辅助类。代码在这里:

mSocket = new LocalSocket();

LocalSocketAddress address = new LocalSocketAddress("installd",
        LocalSocketAddress.Namespace.RESERVED);

mSocket.connect(address);

mIn = mSocket.getInputStream();
mOut = mSocket.getOutputStream();

了解了这个之后,我们找一下 dexopt 看看 Installer 向 installd 发送了什么信息:

public int dexopt(String apkPath, int uid, boolean isPublic) {
    StringBuilder builder = new StringBuilder("dexopt");
    builder.append(' ');
    builder.append(apkPath);
    builder.append(' ');
    builder.append(uid);
    builder.append(isPublic ? " 1" : " 0");
    return execute(builder.toString());
}

其实发送了 dexopt apkpath uid 1|0 给 installd。好了,Installer 分析结束,我们转战 installd。

installd.c

installd 是 socket 通信的 server,会 accept 连接,并读取数据。从下面的两个死循环就能看出来:

for (;;) {
    alen = sizeof(addr);
    s = accept(lsocket, &addr, &alen);
    if (s < 0) {
        ALOGE("Accept failed: %s\n", strerror(errno));
        continue;
    }
    fcntl(s, F_SETFD, FD_CLOEXEC);

    ALOGI("new connection\n");
    for (;;) {
        unsigned short count;
        if (readx(s, &count, sizeof(count))) {

之后会调用 execute 函数:

if (execute(s, buf)) break;

execute() 函数先把 cmd 分割然后通过 cmds 表查询函数名称:

struct cmdinfo {
    const char *name;
    unsigned numargs;
    int (*func)(char **arg, char reply[REPLY_MAX]);
};

struct cmdinfo cmds[] = {
    { "ping",                 0, do_ping },
    { "install",              4, do_install },
    { "dexopt",               3, do_dexopt },
    { "movedex",              2, do_move_dex },
    { "rmdex",                1, do_rm_dex },
    { "remove",               2, do_remove },
    { "rename",               2, do_rename },
    { "fixuid",               3, do_fixuid },
    { "freecache",            1, do_free_cache },
    { "rmcache",              2, do_rm_cache },
    { "getsize",              6, do_get_size },
    { "rmuserdata",           2, do_rm_user_data },
    { "movefiles",            0, do_movefiles },
    { "linklib",              3, do_linklib },
    { "mkuserdata",           3, do_mk_user_data },
    { "rmuser",               1, do_rm_user },
};

dexopt 对应的是 do_dexopt() 函数,do_dexopt() 又调用了 commands.c 里面的 dexopt() 函数。感觉离我们的目标不远了,继续看 commands.c。

commands.c

在 commands.c 中的 dexopt() 函数,我们看到此函数从 property 中获得 dalvik.vm.dexopt-flags 和 persist.sys.dalvik.vm.lib:

/* platform-specific flags affecting optimization and verification */
property_get("dalvik.vm.dexopt-flags", dexopt_flags, "");
ALOGV("dalvik.vm.dexopt_flags=%s\n", dexopt_flags);

/* The command to run depend ones the value of persist.sys.dalvik.vm.lib */
property_get("persist.sys.dalvik.vm.lib", persist_sys_dalvik_vm_lib, "libdvm.so");

dalvik.vm.dexopt-flags 暂时不知道是什么意思,persist.sys.dalvik.vm.lib 就是当前系统使用的运行时默认是 Dalvik VM,如果选择了 ART,应该就是 libart.so 了。

之后函数通过检查是否有 odex 文件来判断是否已经进行过 dexopt。然后进行一系列检查 chmod 和 chown。最后函数 fork 了一个子进程处理 dexopt:

pid_t pid;
pid = fork();
if (pid == 0) {
    /* child -- drop privileges before continuing */
    if (setgid(uid) != 0) {
        ALOGE("setgid(%d) failed in installd during dexopt\n", uid);
        exit(64);
    }
    if (setuid(uid) != 0) {
        ALOGE("setuid(%d) failed in installd during dexopt\n", uid);
        exit(65);
    }
    // drop capabilities
    struct __user_cap_header_struct capheader;
    struct __user_cap_data_struct capdata[2];
    memset(&capheader, 0, sizeof(capheader));
    memset(&capdata, 0, sizeof(capdata));
    capheader.version = _LINUX_CAPABILITY_VERSION_3;
    if (capset(&capheader, &capdata[0]) < 0) {
        ALOGE("capset failed: %s\n", strerror(errno));
        exit(66);
    }
    if (flock(out_fd, LOCK_EX | LOCK_NB) != 0) {
        ALOGE("flock(%s) failed: %s\n", out_path, strerror(errno));
        exit(67);
    }

    if (strncmp(persist_sys_dalvik_vm_lib, "libdvm", 6) == 0) {
        run_dexopt(zip_fd, out_fd, apk_path, out_path, dexopt_flags);
    } else if (strncmp(persist_sys_dalvik_vm_lib, "libart", 6) == 0) {
        run_dex2oat(zip_fd, out_fd, apk_path, out_path, dexopt_flags);
    } else {
        exit(69);   /* Unexpected persist.sys.dalvik.vm.lib value */
    }
    exit(68);   /* only get here on exec failure */
} else {
    res = wait_dexopt(pid, apk_path);
    if (res != 0) {
        ALOGE("dexopt in='%s' out='%s' res=%d\n", apk_path, out_path, res);
        goto fail;
    }
}

开始先 drop capabilities(关于 Linux capabilities: man capabilities)。最后比较 persist_sys_dalvik_vm_lib,如果是 libart 则调用 run_dex2oat()。Good,终于有头绪了,再看 run_dex2oat():

static void run_dex2oat(int zip_fd, int oat_fd, const char* input_file_name,
    const char* output_file_name, const char* dexopt_flags)
{
    static const char* DEX2OAT_BIN = "/system/bin/dex2oat";
    static const int MAX_INT_LEN = 12;      // '-'+10dig+'\0' -OR- 0x+8dig
    char zip_fd_arg[strlen("--zip-fd=") + MAX_INT_LEN];
    char zip_location_arg[strlen("--zip-location=") + PKG_PATH_MAX];
    char oat_fd_arg[strlen("--oat-fd=") + MAX_INT_LEN];
    char oat_location_arg[strlen("--oat-name=") + PKG_PATH_MAX];

    sprintf(zip_fd_arg, "--zip-fd=%d", zip_fd);
    sprintf(zip_location_arg, "--zip-location=%s", input_file_name);
    sprintf(oat_fd_arg, "--oat-fd=%d", oat_fd);
    sprintf(oat_location_arg, "--oat-location=%s", output_file_name);

    ALOGV("Running %s in=%s out=%s\n", DEX2OAT_BIN, input_file_name, output_file_name);
    execl(DEX2OAT_BIN, DEX2OAT_BIN,
          zip_fd_arg, zip_location_arg,
          oat_fd_arg, oat_location_arg,
          (char*) NULL);
    ALOGE("execl(%s) failed: %s\n", DEX2OAT_BIN, strerror(errno));
}

终于找到了,这就是调用 dex2oat 的地方,写的非常清楚。–zip-fd 是 apk 的 fd,–oat-fd 是输出 oat 的 fd。最终使用 execl() 执行 /system/bin/dex2oat 程序对 apk 进行编译。

三、总结

dex2oat 对所有 apk 进行编译并保存在 dalvik-cache 目录里。PMS 会持续扫描安装目录,如果有新的 App 安装则马上调用 dex2oat 进行编译。

最后,之前我们提到的 boot.art 我在 logcat 中找到了他的来源:

/system/bin/dex2oat --image=/data/dalvik-cache/system@[email protected] \
--runtime-arg -Xms64m --runtime-arg -Xmx64m \
--dex-file=/system/framework/core-libart.jar \
--dex-file=/system/framework/conscrypt.jar \
--dex-file=/system/framework/okhttp.jar \
--dex-file=/system/framework/core-junit.jar \
--dex-file=/system/framework/bouncycastle.jar \
--dex-file=/system/framework/ext.jar --dex-file=/system/framework/framework.jar \
--dex-file=/system/framework/framework2.jar \
--dex-file=/system/framework/telephony-common.jar \
--dex-file=/system/framework/voip-common.jar \
--dex-file=/system/framework/mms-common.jar \
--dex-file=/system/framework/android.policy.jar \
--dex-file=/system/framework/services.jar \
--dex-file=/system/framework/apache-xml.jar \
--dex-file=/system/framework/webviewchromium.jar \
--oat-file=/data/dalvik-cache/system@[email protected] --base=0x60000000 \
--image-classes-zip=/system/framework/framework.jar \
--image-classes=preloaded-classes

似乎是把所有 framework 的 jar 编译成 boot.art,在 dex2oat 的时候使用。还是有很多不理解的地方,相信在以后的分析中应该有更深入的理解。

附:相关代码链接