学习ART运行时无缝对接Dalvik虚拟机

本文部分内容包括图片等参考罗升阳的博客,出处链接:http://blog.csdn.net/luoshengyang/article/details/18006645

首先从老罗博客中找到了这张图能够非常清晰显示两者的关系。可以看到dalvik虚拟机和art都是通过调用so库文件的导出函数实现的,而这两个机制的so库导出函数相同,就导致了在接口处可以无缝地替换dalvik:


至于这三个导出函数的具体作用,那就需要看其具体实现的代码了。而这两者在实现上也有很大的区别,dalvik虚拟机执行的是dex字节码,而art执行的则是机器码(应该可以理解为汇编代码)。而无缝替换的还有一个关键点就是不需要重新编译原本能在dalvilk上的apk,这就需要一个将dex字节码转换成机器码的一个过程,这个过程放在了安装apk应用的时候,之前在dalvik上安装apk的时候也会做一个优化,就是将class.dex文件从apk文件中提取出来优化成.odex文件,只是现在把这个过程变成了转换为机器码而已。因此可以说art能够无缝替换dalvilk的原因有两个:

1.在启动过程中,dalvik和art的接口函数相同
2.都有dex文件优化过程

接下来就是分析源码,弄清楚上面两个的过程。根据前一篇笔记,也就是Zygote进程启动终了解到,虚拟机的启动是从AndroidRuntime.start函数中启动的,而启动其他应用程序时的虚拟机都是从zygote中复制过去的,因此只需要了解zygote进程是如何创建虚拟机就行了

1.dalvik和art具有相同的jni接口

1.1.AndroidRuntime.start

首先来看启动代码,首先调用了JniInvocation的init函数来初始化JNI环境,随后调用AndoirdRuntime的startVM函数来启动虚拟机:

void AndroidRuntime::start(const char* className, const char* options)
{
    ... ...
    /* start the virtual machine */
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&mJavaVM, &env) != 0) {
        return;
    }
    onVmCreated(env);
    ... ...
}
1.2.JniInvocation.init

这个函数在libnativehelper\JniInvocation.cpp中,代码如下:

#ifdef HAVE_ANDROID_OS
static const char* kLibrarySystemProperty = "persist.sys.dalvik.vm.lib";
#endif
static const char* kLibraryFallback = "libdvm.so";

bool JniInvocation::Init(const char* library) {
#ifdef HAVE_ANDROID_OS
  char default_library[PROPERTY_VALUE_MAX];
  property_get(kLibrarySystemProperty, default_library, kLibraryFallback);
#else
  const char* default_library = kLibraryFallback;
#endif
  if (library == NULL) {
    library = default_library;
  }

  handle_ = dlopen(library, RTLD_NOW);
  if (handle_ == NULL) {
    if (strcmp(library, kLibraryFallback) == 0) {
      // Nothing else to try.
      ALOGE("Failed to dlopen %s: %s", library, dlerror());
      return false;
    }
    // Note that this is enough to get something like the zygote
    // running, we can't property_set here to fix this for the future
    // because we are root and not the system user. See
    // RuntimeInit.commonInit for where we fix up the property to
    // avoid future fallbacks. http://b/11463182
    ALOGW("Falling back from %s to %s after dlopen error: %s",
          library, kLibraryFallback, dlerror());
    library = kLibraryFallback;
    handle_ = dlopen(library, RTLD_NOW);
    if (handle_ == NULL) {
      ALOGE("Failed to dlopen %s: %s", library, dlerror());
      return false;
    }
  }
  if (!FindSymbol(reinterpret_cast<void**>(&JNI_GetDefaultJavaVMInitArgs_),
                  "JNI_GetDefaultJavaVMInitArgs")) {
    return false;
  }
  if (!FindSymbol(reinterpret_cast<void**>(&JNI_CreateJavaVM_),
                  "JNI_CreateJavaVM")) {
    return false;
  }
  if (!FindSymbol(reinterpret_cast<void**>(&JNI_GetCreatedJavaVMs_),
                  "JNI_GetCreatedJavaVMs")) {
    return false;
  }
  return true;
}

在函数的上面还定义了kLibrarySystemProperty和kLibraryFallback两个变量。如果在之前有定义HAVE_ANDROID_OS这个宏,则调用property_get函数从persist.sys.dalvik.vm.lib这个系统属性中取出值,而这个值为"libdvm.so"或"libart.so"决定后面的代码调用的dalvik虚拟机还是art虚拟机的接口。可以看到HAVE_ANDROID_OS这个宏在4.4.2的代码实现中都设置为1,无论是x86、ARM还是MIPS平台:


后面的代码就比计较简单了,从相应的库中(dalvik或art)取出JNI_GetDefaultJavaVMInitArgs、JNI_CreateJavaVM和JNI_GetCreatedJavaVMs三个函数的接口并赋值给相应的变量。

1.3.AndroidRuntime.startVM

startVM函数的简要代码如下:

/*
 * Start the Dalvik Virtual Machine.
 * Various arguments, most determined by system properties, are passed in.
 * The "mOptions" vector is updated.
 */
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv)
{
    ... ... 
    initArgs.version = JNI_VERSION_1_4;
    initArgs.options = mOptions.editArray();
    initArgs.nOptions = mOptions.size();
    initArgs.ignoreUnrecognized = JNI_FALSE;

    /*
     * Initialize the VM.
     *
     * The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.
     * If this call succeeds, the VM is ready, and we can start issuing
     * JNI calls.
     */
    if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
        ALOGE("JNI_CreateJavaVM failed\n");
        goto bail;
    }

    result = 0;
}

可以看到这个函数的主要功能就是调用JNI_CreateJavaVM函数来创建虚拟机,而第三个参数initArgs是由前面的代码根据各种各样的系统属性获得的(就像注释中说的那样)。JNI_CreateJavaVM函数的声明如下:

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  return JniInvocation::GetJniInvocation().JNI_CreateJavaVM(p_vm, p_env, vm_args);
}

可以看到它调用了JniInvocation的JNI_CreateJavaVM函数,如下:

jint JniInvocation::JNI_GetCreatedJavaVMs(JavaVM** vms, jsize size, jsize* vm_count) {
  return JNI_GetCreatedJavaVMs_(vms, size, vm_count);
}

而它则是调用了函数指针JNIGetCreatedJavaVMs所指向的函数,在之前第一步的分析中得知这个指针指向的是dalvik或art虚拟机so文件中的JNI_CreateJavaVM函数。这篇文章主要关心的是art无缝对接dalvik虚拟机,所以具体后面的虚拟机启动过程就先放一放,等后面专门研究一下。

2.class.dex文件优化

2.1.PackageManagerService

在系统安卓apk文件的时候会调用PackageManagerService服务进行安装,代码在frameworks\base\services\java\com\android\server\pm\PackageManagerService.java中。在这个服务中会调用Install的dexopt函数来对apk文件中的class.dex进行优化 ,简要代码如下:

try {
     if (dalvik.system.DexFile.isDexOptNeeded(path)) {
        //对dex文件进行优化
         mInstaller.dexopt(path, Process.SYSTEM_UID, true);
        didDexOpt = true;
    }
} catch (FileNotFoundException e) {
    Slog.w(TAG, "Jar not found: " + path);
} catch (IOException e) {
    Slog.w(TAG, "Exception reading jar: " + path, e);
}
2.2 Installer.dexopt

这个函数在文件frameworks\base\services\java\com\android\server\pm\Installer.java中,代码如下,它将参数合并成一个字符串然后调用execute函数:

    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());
    }
2.3.Installer.execute

首先是execute函数代码,其又调用了transaction函数,并将命令作为参数cmd传入:

    private int execute(String cmd) {
        String res = transaction(cmd);
        try {
            return Integer.parseInt(res);
        } catch (NumberFormatException ex) {
            return -1;
        }
    }
2.4.Installer.transaction/connect/writeCommand

首先调用了connect函数,如果成功则调用writeCommand并将命令传入:

    private synchronized String transaction(String cmd) {
        if (!connect()) {
            Slog.e(TAG, "connection failed");
            return "-1";
        }

        if (!writeCommand(cmd)) {
           ... ...
            }
        }
        ... ...
    }

下面首先来看看connect函数的代码:

    private boolean connect() {
        //判断是否已经创建了socket套接字
        if (mSocket != null) {
            return true;
        }
        Slog.i(TAG, "connecting...");
        try {
            mSocket = new LocalSocket();
            //获取installd的套接字并进行连接
            LocalSocketAddress address = new LocalSocketAddress("installd",
                    LocalSocketAddress.Namespace.RESERVED);

            mSocket.connect(address);
            //获取socket的输入和输出流以方便与installd进行通信
            mIn = mSocket.getInputStream();
            mOut = mSocket.getOutputStream();
        } catch (IOException ex) {
            disconnect();
            return false;
        }
        return true;
    }

它会先判断之前是否创建了socket,如果没有那就获取守护进程installd的socket并与其连接。再来看writeCommand的代码:

private boolean writeCommand(String _cmd) {
    byte[] cmd = _cmd.getBytes();
    int len = cmd.length;
    if ((len < 1) || (len > 1024))
        return false;
    buf[0] = (byte) (len & 0xff);
    buf[1] = (byte) ((len >> 8) & 0xff);
    try {
        //往输出流(相对于自身,相对installd则是输入)写入命令,使installd进程执行
        mOut.write(buf, 0, 2);
        mOut.write(cmd, 0, len);
    } catch (IOException ex) {
        Slog.e(TAG, "write error");
        disconnect();
        return false;
    }
    return true;
}

向installd进程发送socket请求,让其优化dex文件的代码。

2.5.installd.dexopt

installd的dexopt函数在文件frameworks\native\cmds\installd\Commands.c实现的,代码如下:

int dexopt(const char *apk_path, uid_t uid, int is_public)
{
    ... ... 

    /* 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 */
    //获取persist.sys.dalvik.vm.lib属性
    property_get("persist.sys.dalvik.vm.lib", persist_sys_dalvik_vm_lib, "libdvm.so");
    //优化之后输出的文件名
    sprintf(out_path, "%s%s", apk_path, ".odex");
    ... ...
    pid_t pid;
    pid = fork();
    if (pid == 0) {
        ... ...
        根据上面获取的属性判断当前环境是dalvik还是art,分别执行对应的代码
        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);
        } 
        ... ...
    }
    ... ...
}

可以看到上面仍然是根据"persist.sys.dalvik.vm.lib"属性来判断属于dalvik还是art模式下的,如果是dalvik则执行run_dexopt函数将dex优化成dexy文件,而执行run_dex2oat则将dex优化成自定义的elf文件,但是输出的位置和文件名都是相同的odex。下面再来看这两个函数的代码。

2.6.run_dexopt & run_dex2oat

首先是run_dexopt函数:

static void run_dexopt(int zip_fd, int odex_fd, const char* input_file_name,
    const char* output_file_name, const char* dexopt_flags)
{
    static const char* DEX_OPT_BIN = "/system/bin/dexopt";
    static const int MAX_INT_LEN = 12;      // '-'+10dig+'\0' -OR- 0x+8dig
    char zip_num[MAX_INT_LEN];
    char odex_num[MAX_INT_LEN];

    sprintf(zip_num, "%d", zip_fd);
    sprintf(odex_num, "%d", odex_fd);

    ALOGV("Running %s in=%s out=%s\n", DEX_OPT_BIN, input_file_name, output_file_name);
    execl(DEX_OPT_BIN, DEX_OPT_BIN, "--zip", zip_num, odex_num, input_file_name,
        dexopt_flags, (char*) NULL);
    ALOGE("execl(%s) failed: %s\n", DEX_OPT_BIN, strerror(errno));
}

然后是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));
}

这两个函数都是先获取一堆参数,随后run_dexopt使用了"/system/bin/dexopt"来优化dex文件,而run_dex2oat函数则是使用了"/system/bin/dex2oat"来优化。至于这两个程序是如何对dex文件进行优化的,就要涉及很多语法、词法分析的东西了,系统性的知识没有,要了解这两个过程还是很困难的,所以就先放一放了,后面会再出一个odex文件格式解析,了解这个会相对轻松很多。

至此,art完全兼容dalvik的原因应该已经非常明确了。首先是这两个虚拟机所导出的接口完全一致,使得调用这些接口的代码都无需修改,其次在优化dex文件时系统会判断目前所处的虚拟机状态来进行对应的优化,并且产生的优化文件的全路径完全相同,所以art能在不修改任何apk代码的情况下无缝对接dalvik虚拟机。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值