如何让指定程序启动一个Android系统中的Java进程

Android系统中的所有应用程序都是由所谓的Zygote进程(准确的说是/system/bin/app_process)“孵化”出来的。所有新创建的程序,都会继承Zygote进程内所有的资源。这样做的好处是免去了各个程序自己加载各自资源的时间,同时减少了系统总的内存使用量,代价仅仅是增加了每次系统重启的时间。

但同时也带来了一些问题。例如,如果开发者想分析或调试某个应用程序,而某些分析工具必须要求由它自己来启动你的应用程序(例如Native程序内存检测工具valgrind), 就不能简单的通过直接调用app_process程序来完成。

好在Google意识到了这个问题,在最近的Android系统代码中留下了这个口子,下面通过分析原代码来详述其实现原理(代码基于4.4.2_r2)。具体应用程序的启动过程就不分析了,这里重点分析和本文内容相关的部分。

首先,来看ZygoteConnection类中的runOnce函数(代码位于frameworks\base\core\java\com\android\internal\os\ZygoteConnection.java):

boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {
    ...
    try {
        parsedArgs = new Arguments(args);
        ...
        applyInvokeWithSystemProperty(parsedArgs);
        ...
        pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
                parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
                parsedArgs.niceName);
    }
    ....
    try {
        if (pid == 0) {
            ...
            handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);
            ...
        } else {
            ...
        }
    }
    ...
}

可以看到,其调用了一个叫做applyInvokeWithSystemProperty的函数,然后fork出了子进程。对于子进程来说,接下来调用了handleChileProc函数。先来看第一个函数(代码位于frameworks\base\core\java\com\android\internal\os\ZygoteConnection.java):

public static void applyInvokeWithSystemProperty(Arguments args) {
    if (args.invokeWith == null && args.niceName != null) {
        if (args.niceName != null) {
            String property = "wrap." + args.niceName;
            if (property.length() > 31) {
                property = property.substring(0, 31);
            }
            args.invokeWith = SystemProperties.get(property);
            if (args.invokeWith != null && args.invokeWith.length() == 0) {
                args.invokeWith = null;
            }
        }
    }
}

其中所谓的niceName就是要启动程序的包名。代码很简单,如果包名不为空的话,就查看一下系统中有没有属性名是在你的包名前加上“wrap.”的属性。如果有的话,那么就将参数列表中的invokeWith变量的值设置成那个系统属性;如果没有的话,就将其设置成空。

通常,这个属性会被设置成你要使用的分析程序的名字。同时,由于属性名包含你要跑的程序的包名,所以只对你关心的程序起作用。

注意,这里还有个限制条件,就是拼完之后的属性名不能超过31个字符,如果超过的话只会取前31个字符。因此,这里就有了一个隐性的限制条件,就是你程序的包名不能太长(不要超过26个字符)。

接下来,当子进程启动之后,会继续调用handleChileProc函数,做接下来的工作(代码位于frameworks\base\core\java\com\android\internal\os\ZygoteConnection.java):

private void handleChildProc(Arguments parsedArgs,
        FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr)
        throws ZygoteInit.MethodAndArgsCaller {
    ...
    if (parsedArgs.runtimeInit) {
        if (parsedArgs.invokeWith != null) {
            WrapperInit.execApplication(parsedArgs.invokeWith,
                    parsedArgs.niceName, parsedArgs.targetSdkVersion,
                    pipeFd, parsedArgs.remainingArgs);
        } else {
            RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion,
                    parsedArgs.remainingArgs);
        }
    } else {
    ...
    }
}

这里会根据刚才在handleChileProc函数中对参数列表中的invokeWith变量设置值的不同做不一样的处理。如果invokeWith是空的话,这也是通常的情况,就直接调用RuntimeInit.zygoteInit函数了;如果不为空的话,也就是本文要分析的情况,会调用WrapperInit.execApplication函数(代码位于frameworks\base\core\java\com\android\internal\os\WrapperInit.java):

public static void execApplication(String invokeWith, String niceName,
            int targetSdkVersion, FileDescriptor pipeFd, String[] args) {
    StringBuilder command = new StringBuilder(invokeWith);
    command.append(" /system/bin/app_process /system/bin --application");
    if (niceName != null) {
        command.append(" '--nice-name=").append(niceName).append("'");
    }
    command.append(" com.android.internal.os.WrapperInit ");
    command.append(pipeFd != null ? pipeFd.getInt$() : 0);
    command.append(' ');
    command.append(targetSdkVersion);
    Zygote.appendQuotedShellArgs(command, args);
    Zygote.execShell(command.toString());
}

函数的功能很简单,就是拼一个命令行字符串,然后交给Zygote.execShell函数去执行(代码位于libcore\dalvik\src\main\java\dalvik\system\Zygote.java):

public static void execShell(String command) {
    String[] args = { "/system/bin/sh", "-c", command };
    try {
        Libcore.os.execv(args[0], args);
    } catch (ErrnoException e) {
        throw new RuntimeException(e);
    }
}

这个函数实际上最后会调用系统的execv函数,用shell程序(/system/bin/sh)来执行指定的命令。而这个要执行的命令是在WrapperInit.execApplication函数中拼出来的。

分析前面的代码,可以知道,命令格式大致如下:

<属性值> /system/bin/app_process /system/bin --application '--nice-name=<包名>' com.android.internal.os.WrapperInit <管道号> <目标SDK版本号> <其它剩下的参数>

这里的属性值通常被设置成你要使用的那个测试工具,或者是一个特定的测试脚本。所以,推论这样下去的结果就是让你使用的测试工具启动app_process进程,并给其传递相应的参数,从而让它接着启动你要测试的程序。

这里特别说明一下,参数中的“--nice-name”被设置成你程序的包名,但它的作用只是将子进程的名字改成你的包名,并不是指定要app_process启动你的程序。另外,在所谓的“其它剩下的参数中”,会指定一个类名,但它也不是你所要启动程序的类名,无论你要启动的程序是什么,这个类名都被指定成“android.app.ActivityThread”。而真正决定到底启动哪个Java程序的另有玄机。

那到底是不是这样呢?我们接着看app_process中的main函数实现(代码位于frameworks\base\cmds\app_process\app_main.cpp):

int main(int argc, char* const argv[])
{
    ...
    AppRuntime runtime;
    const char* argv0 = argv[0];

    argc--;
    argv++;

    int i = runtime.addVmArguments(argc, argv);

    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    const char* parentDir = NULL;
    const char* niceName = NULL;
    const char* className = NULL;
    while (i < argc) {
        const char* arg = argv[i++];
        if (!parentDir) {
            parentDir = arg;
        } else if (strcmp(arg, "--zygote") == 0) {
            zygote = true;
            niceName = "zygote";
        } else if (strcmp(arg, "--start-system-server") == 0) {
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName = arg + 12;
        } else {
            className = arg;
            break;
        }
    }

    if (niceName && *niceName) {
        setArgv0(argv0, niceName);
        set_process_name(niceName);
    }

    runtime.mParentDir = parentDir;

    if (zygote) {
        ...
    } else if (className) {
        runtime.mClassName = className;
        runtime.mArgC = argc - i;
        runtime.mArgV = argv + i;
        runtime.start("com.android.internal.os.RuntimeInit",
                application ? "application" : "tool");
    } else {
        ...
    }
}
对照前面拼的命令,可以看出,传递给app_process的参数的含义如下:

1)“/system/bin”是所谓的父路劲(Parent Directory);

2)“--application”表示让app_process启动一个应用程序;

3)“--nice-name=<包名>”指定要实际启动的那个程序的包名;

4)“com.android.internal.os.WrapperInit”字面上是指定一个类名,app_process不能直接启动一个运行Dalvik指令的程序,必须要借助一些Java层的程序,下面分析中会提到。

由于命令没有指定--zygote,并且设置了类名是“com.android.internal.os.WrapperInit”,所以接下来会调用AppRuntime.start函数,第一个参数是“com.android.internal.os.RuntimeInit”,第二个参数是“application”,并且将AppRuntime实例的mClassName设置成了“com.android.internal.os.WrapperInit”。

由于AppRuntime类继承自AndroidRuntime类,且没有重载其中的start函数,所以这里实际是调用的AndroidRuntime.start函数(代码位于frameworks\base\core\jni\AndroidRuntime.cpp):

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

    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }

    jclass stringClass;
    jobjectArray strArray;
    jstring classNameStr;
    jstring optionsStr;

    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(2, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env->NewStringUTF(className);
    assert(classNameStr != NULL);
    env->SetObjectArrayElement(strArray, 0, classNameStr);
    optionsStr = env->NewStringUTF(options);
    env->SetObjectArrayElement(strArray, 1, optionsStr);

    char* slashClassName = toSlashClassName(className);
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
       ...
    } else {
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
        } else {
            env->CallStaticVoidMethod(startClass, startMeth, strArray);
        }
    }
    ...
}

代码很简单,启动并初始化好Dalvik虚拟机,然后找出名字为className参数指定的类(这里是“com.android.internal.os.RuntimeInit”),并调用它的静态main方法,且将传递进来的className和options参数转换成字符串数组传递进去。所以,接下来我们看RuntimeInit.main函数(代码位于frameworks\base\core\java\com\android\internal\os\RuntimeInit.java):

public static final void main(String[] argv) {
    ...
    commonInit();

    nativeFinishInit();
    ...
}

还是继续做一些初始化的动作,那传进来的“com.android.internal.os.WrapperInit”用到了什么地方,具体的程序又是怎么被启动的呢?

奥秘在nativeFinishInit函数中,这是一个Native的函数(代码位于frameworks\base\core\jni\AndroidRuntime.cpp):

static void com_android_internal_os_RuntimeInit_nativeFinishInit(JNIEnv* env, jobject clazz)
{
    gCurRuntime->onStarted();
}

这个函数会调用当前AndroidRuntime.onStarted函数,而通过前面可以看到,实际创建的是AppRuntime类的实例,且onStarted函数被申明成虚函数,所以由于多态性,实际调用的是AppRuntime.onStarted函数(代码位于frameworks\base\cmds\app_process\app_main.cpp):

virtual void onStarted()
{
    ...
    AndroidRuntime* ar = AndroidRuntime::getRuntime();
    ar->callMain(mClassName, mClass, mArgC, mArgV);
    ...
}

看到了吗,它会接着调用mClassName变量指定类中的main函数,并且传递参数。但是,这个参数已经不是前面那个完整的命令了,而是截取了在类名(本例中是““com.android.internal.os.WrapperInit”)后面剩下命令字符串作为参数。

那好,我们接着看WrapperInit.main函数的实现(代码位于frameworks\base\core\java\com\android\internal\os\WrapperInit.java):

public static void main(String[] args) {
    try {
        int fdNum = Integer.parseInt(args[0], 10);
        int targetSdkVersion = Integer.parseInt(args[1], 10);
        ...

        ZygoteInit.preload();

        String[] runtimeArgs = new String[args.length - 2];
        System.arraycopy(args, 2, runtimeArgs, 0, runtimeArgs.length);
        RuntimeInit.wrapperInit(targetSdkVersion, runtimeArgs);
    } catch (ZygoteInit.MethodAndArgsCaller caller) {
        caller.run();
    }
}

很简单,在命令字符串中接着解析出管道号和目标SDK版本号,然后调用RuntimeInit.wrapperInit,并将SDK版本号和后面剩下的命令传递进去。

注意,这里还特别调用了ZygoteInit.preload函数,这是因为当前的子进程已经是一个全新的app_process进程了(通过exec执行的),并没有继承Zygote进程的所有资源,所以要重新加载一下。

接着看RuntimeInit.wrapperInit(代码位于frameworks\base\core\java\com\android\internal\os\RuntimeInit.java):

public static void wrapperInit(int targetSdkVersion, String[] argv)
            throws ZygoteInit.MethodAndArgsCaller {
    if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application from wrapper");

    applicationInit(targetSdkVersion, argv);
}

private static void applicationInit(int targetSdkVersion, String[] argv)
        throws ZygoteInit.MethodAndArgsCaller {
    ...
    final Arguments args;
    try {
        args = new Arguments(argv);
    } catch (IllegalArgumentException ex) {
        ...
    }

    invokeStaticMain(args.startClass, args.startArgs);
}

RuntimeInit.wrapperInit函数很简单,简单的直接调用RuntimeInit.applicationInit函数。

而RuntimeInit.applicationInit函数接着解析剩下的参数,从中解析出要加载并运行的类和参数。前面曾经提到过,这个类,无论你启动什么程序,都是“android.app.ActivityThread”。所以,下面就会加载“android.app.ActivityThread”类,执行它的main函数,并将剩下的参数传递进去。

讲到这里,估计大家还在疑惑,还有一个大问题没有解决,所有程序都加载同样的类,那我们自己的程序到底是在哪里加载进来的呢?

这个问题说来话长,限于本篇的篇幅,不能做太详细的介绍,只能大致说一下。由于Android系统的特殊性,要启动一个正常的应用程序,需要涉及到一个ActivityManagerService系统服务,还有一个就是Zygote进程,大概经历了以下几步:

1)ActivityManagerService收到一个Binder事件,让它启动一个指定的Activity。通常,当你在Launcher中点击一个应用程序图标之后,是由Launcher负责将这个事件发送给ActivityManagerService的。

当然,你也可以通过am命令,向ActivityManagerService发送任何你想要的事件,例如:

adb shell am start -n <包名>/.<Activity名>

2)ActivityManagerService查找本地记录,发现并没有任何已运行的程序包含这个Activity,因此通过本地套接字(Local Socket)向Zygote进程发消息,请求它帮忙启动一个新的进程。

这个请求是同步的,当一个新的子进程被创建出来后,ActivityManagerService会得到这个子进程的进程号,并将这个进程号和随后要在这个进程中启动程序的信息一一对应的记录下来,等待以后查询时用。

而对于那个子进程来说,它启动起来之后,都会去执行“android.app.ActivityThread”类中的main函数。上面分析的部分,只是说明了从Zygote进程拿到启动新进程的本地套接字消息(ZygoteConnection.runOnce函数),到创建一个新的进程并执行“android.app.ActivityThread”类的main函数(RuntimeInit.wrapperInit)之间,为了达到让你的工具启动程序的目的,所做的一些特殊的处理。

3)子进程在“android.app.ActivityThread”类的“领导”下,接着会试着附着(Attach)到那个应用程序上,这也是这个子进程被创建出来的真正目的。

这里所谓的附着,其实就是子进程通过Binder通信,请求ActivityManagerService告知,对应自己这个子进程号的应用程序的信息(包括包名、组件名等)。在前一步中已经说明了,进程号和应用程序信息是一一对应的记录在ActivityManagerService中的。

当子进程得到了应用程序信息之后,就会用类加载器将其加载进来,并执行。

通过以上的分析,回答了我一直以来都有的一个疑问,那就是为什么不能直接通过app_process启动一个应用程序,非要绕这么个大圈子呢?答案其实很简单,你如果直接在命令行中敲入app_process命令,即使你把命令都敲对了也没用,因为你绕过了ActivityManagerService,它那并没有注册你这个进程号对应的要启动的程序的信息。而通过前面的代码分析,如果你对应用程序设置了wrapper系统属性的话,ActivityManagerService并没有被绕过,它那确实记录着你子进程号对应的应用程序信息,后面还是能够查找的到,一切的一切只是让你启动的那个子进程自己去切换环境,执行你设定的工具程序,再让它继续启动你程序。

你可能感兴趣的:(android,wrapper)