Android消息机制(二)两个main函数与App的启动

584 阅读8分钟

1.main函数

通常来说,main函数被认为是我们编写程序的起点,无论是C还是Java,我们要接收外界的输入,通常是在命令行中,在程序的后面跟上我们的main args参数:

wc a.txt // 利用wc 统计a.txt文本文件的行数
git commit -am "xxxx" // 使用git程序来commit一个内容,其中的{commit}/{-am}/{feat: xxx}都是参数

当然,Java会比较特殊,一些在static代码块中的语句会先于main函数执行,这是JVM特有的类加载机制的特性。

而Android SDK中,为我们提供的所有套件,包括早期的Activity、Fragment或者是现代的Lifecycle、ViewModel等等,这些都是只提供了一个基础的功能切片,我们面向这个切片便可完成绝大多数功能。我们在Activity中编写业务的时候,绝大多数日常开发场景下我们并不需要去关心main函数是如何执行到Activity的onCreate,也不用特别去关心main函数是如何执行到管理Lifecycle的代码的,但这并不意味着main函数不存在。

当Android程序运行时,它始终存在于JVM的调用栈当中,不过,你会发现有两个main函数:

2. 程序是从哪来的?

我们大致可以将Android看做是一个Linux操作系统。而一个普通App既然是Android进程,那必然是运行在JVM中的程序,而JVM又是一个Linux程序,这样一来就形成了一种对应关系:

Android App <-> ART虚拟机 <-> Linux进程

所以,Android App的启动,势必和Linux进程的启动有关。

2.1 fork机制

fork机制大家想必都有所了解,作为Linux最基本的一个操作之一,fork提供了一种创建子进程的方式,通常情况,我们的程序是顺序执行的,而fork本意是叉子,也意味着分叉,言下之意就是当我们的程序遇到fork调用时,当前的顺序执行流会在一个新的进程中,产生一个新的、同样的执行流,例如:

void main(){
    fork();
    int x = 10;
}

我们的程序执行到fork之后,主程序(pid = 1233)在执行到fork之后,当前的执行流会被copy一份,并在一个新的进程中继续运行。假设新的进程的pid = 1234,那么此时操作系统中会有两个main.c对应程序执行流的进程在执行:

image.png

我们可以借助于fork函数的返回值来区分哪一个是原本的main进程(pid = 1233),哪一个是新复制出来的子进程1234,因为在原main进程调用fork时,fork函数会在源程序中返回子进程的pid = 1233,也就是fork的返回值为1233,而子进程因为完整地copy了一份主程序的执行流,同样地,它也会有一个fork的返回值,但在子进程中,fork的返回值为0,在上述的程序流图中,我们可以知道,左侧的是main进程,而右侧的是main fork出来的子进程。

2.2 懒加载与CopyOnWrite

Linux的fork机制相比较于传统的fork机制有一个比较特殊的点,就是Linux并不会立即将完整的源程序的内存副本复制到新的内存区域,而是采用写时复制技术,即既有的“读”操作全部映射到源程序的内存地址上,而只有“写”操作才会触发一个新的内存页写入,将新的内容写入到新进程的内存空间中,以保证新旧程序有完整又独立的内存空间。

这样的好处是显而易见的,一是fork创建子进程的时候,并不会涉及到大量的内存复制,即使是占用上GB的程序程序内存,使用fork创建子进程,也不会在瞬间产生大量的内存复制操作而消耗掉系统资源。其二是我们程序使用的绝大多数的共享库、资源可以轻松地共享(比如初始进程加载了一堆资源文件,这些资源文件是可读的,这样一来其余的程序只需要通过fork机制建立的映射关系就可以访问到初始进程的资源文件,而不必须去触发多次的IO去文件系统中再次加载同一份资源文件)。

3. Android App的main函数

既然我们知道,Android程序也是存在main函数的,同时Android程序也是通过「fork」创建出来的。 既然是fork,那必然有一个fork的源程序,它便是Android开发中,著名的:Zygote进程。Zygote就是Init进程通过解析init.rc文件之后,启动的一个进程,他有非常多的「特点」,但是我们此时只需要记住一个:即它是第一个Java进程

init进程是Linux的第一个用户空间的进程,而Zygote则是第一个Java进程。

类比着,我们也可以看看Android 是如何实现可交互程序的,首先我们可以试着做一件事,找到Android App的Main函数,因为我们知道,可交互程序大概率是运行在一个循环里面的,所以我们可以随意地,在任何可能停下来的地方打上断点,程序调用栈的最开始的栈帧一定就是这个循环了:

你会发现,有两个main函数,一个是ZygoteInit,另一个则是ActivityThread

3.1 第一个main函数

前面提到了,Zygote进程是系统重建的第一个Java进程。所以,Zygote进程的启动必然会去加载虚拟机。而其他的Java虚拟机进程,都需要通过fork Zygote进程来创建,因为fork操作的特性,会复制Zygote进程的内存空间(包括资源数据、代码,甚至是PC指针),于是乎在子进程中也有一份一模一样的Zygote虚拟机,子进程会从fork调用的位置接着往下走。

所以前面提到的,如何区分fork出来的是子进程还是父进程是非常有必要的。

image.png

我们来看看ZygoteInit.java中,它的main函数的代码,这个循环的主要任务,就是通过Socket链接,等待从外部传来的创建子进程的请求,然后通过fork创建子进程,然后关闭掉子进程的Socket链接,完成子进程的创建,然后自己进入下一次循环。

public static void main(String argv[]) {
    ZygoteServer zygoteServer = null;
    // ……
    Runnable caller;
    try {
        // ……
        // 预加载资源
        preload(bootTimingsTraceLog);
        // ……
        // 创建system_server
        if (startSystemServer) {
            Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);
            if (r != null) {
                r.run();
                return;
            }
            // ……
            // 启动循环,不断地去查询Socket
            caller = zygoteServer.runSelectLoop(abiList);
        }
    } catch (Throwable ex) {
        Log.e(TAG, "System zygote died with exception", ex);
        throw ex;
    } finally {
        if (zygoteServer != null) {
            zygoteServer.closeServerSocket();
        }
    }
    // ……
}

重点在于runSelectLoop(abiList)代码中,内部是一个循环:

Runnable runSelectLoop(String abiList) {
    // ……
    while (true) {
       try {
          ZygoteConnection connection = peers.get(pollIndex);
          final Runnable command = connection.processOneCommand(this);
          // ……
}

其中的processOneCommand处理了具体的fork细节:

if (pid == 0) {
    // in child
    zygoteServer.setForkChild();

    zygoteServer.closeServerSocket();
    IoUtils.closeQuietly(serverPipeFd);
    serverPipeFd = null;

    return handleChildProc(parsedArgs, descriptors, childPipeFd,
            parsedArgs.mStartChildZygote);
} else {
    // In the parent. A pid < 0 indicates a failure and will be handled in
    // handleParentProc.
    IoUtils.closeQuietly(childPipeFd);
    childPipeFd = null;
    handleParentProc(pid, descriptors, serverPipeFd);
    return null;
}

当然,ZygoteInit的main函数还有其他的一些重要作用,比如它会去创建system_server、做资源的预加载等等。

3.2 第二个main函数

接下来,我们看看ActivityThread中main方法中的循环:

public static void main(String[] args) {
   
    Looper.prepareMainLooper();
    ActivityThread thread = new ActivityThread();

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    // ……
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

我们可以看到,最后一行抛出了个异常,所以一般情况下不会走到最后一行。

你可以想一下如果走到这一行,一般来说就是程序直接崩溃,并给一个停止运行的提示。但是如果这个异常被线程catch住了会发生什么?

并且我们看到了我们的老熟人:Looper。说明主线程的循环一定是在这里初始化的,而我们知道Looper.loop,本质上也是一个循环:

@SuppressWarnings("AndroidFrameworkBinderIdentity")
public static void loop() {
    // ……
    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}

loopOnce里面无非就是循环一次的操作:

private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return false;
    }
    ……
}

具体干了什么不是我们今天关心的,但是我们可以看到:第一行的mQueue.next()后面的注释写明了:可能会阻塞,这其实就和Shell里面的getcmd()readLine()是一个意思。无非就是等等外部输入,只不过Shell的事件输入通常是从stdin中输入的,而这里的输入则不一定,Shell输入的是字符,不同的字符构成不同的指令;而Handler这里却并不是,它以Message为单位,接受来自外界的输入。

4. 总结

上面的内容,我们大致上介绍了一下Linux的fork机制、Android的Java层的两个main函数分别的作用,从main函数出发,介绍了循环在Android消息机制中的作用。

第一个循环,使用Socket监听,不断地去监听其它程序的请求,如果有其他程序需要创建子进程,那么就从的ZygoteInit中创建一个子进程,然后关闭掉原先用作监听创建请求的Socket,然后启动ActivityThread的main函数,第二个循环,至此我们熟悉的Message机制开始生效。