再学安卓 - Zygote

前言

在上一篇讲rc脚本的例子中,已经出现了zygote的身影,在Gityuan的系统启动架构图中也能看到zygote处在C++ Framework和Java Framework之间,由此可以猜测此进程必定是承上启下的开创性进程。 首先让我们感性的认识一下这个进程。

cmd 复制代码
adb shell "ps -A | grep zygote"

user          pid     ppid
root          1570     1    9069288 110472 0                   0 S zygote64
root          1630     1  534487112  50836 0                   0 S zygote

父进程id为1,就是上一篇的init进程,但有点奇怪,为啥有两个zygote?等会儿我们带着问题去源码中找答案。

看看跟zygote pid相关的进程有哪些

cmd 复制代码
adb shell "ps -A | grep 1570"

user          pid   ppid
root          1570     1    9069288 110472 0     0 S zygote64
system        3236  1570   17905656 760888 0     0 S system_server
u0_a206       4872  1570   12824556 477164 0     0 S com.android.systemui
u0_a248      14681  1570   10596876 120760 0     0 S com.android.mms
u0_a250      15976  1570    9754228 121768 0     0 S com.android.contacts
... ...

非常多且大部分是APP,他们的父进程都是pid 1570(zygote),其中包含一个叫system_server的进程。我们在第三篇曾单步调试过这个进程,当时它在UI中显示为system_process。

到这里我们几乎可以笃定的相信所有应用进程都是zygote孵化而来,因此,接下来我们就看看zygote怎么启动,它又如何启动(孵化)别的进程。

zygote的启动

上一篇讲到init进程通过rc脚本启动后续进程,那么我们找一下zygote在哪里

注:编译之前源码状态下,系统启动相关的rc脚本都在system\core\rootdir目录下,其他服务或进程的rc脚本一般都放在该模块代码根目录,比如:surfaceflinger.rc就在frameworks\native\services\surfaceflinger中。

rc 复制代码
// system\core\rootdir\init.rc
// 引入了zygote的rc脚本,adb shell getprop ro.zygote,查询到其值为zygote64_32

import /system/etc/init/hw/init.${ro.zygote}.rc
rc 复制代码
// system\core\rootdir\init.zygote64_32.rc
// 引入了init.zygote64.rc,同时定义了一个进程zygote_secondary

import /system/etc/init/hw/init.zygote64.rc
service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary --enable-lazy-preload
    ... ...
    onrestart restart zygote
    ... ...
rc 复制代码
// system\core\rootdir\init.zygote64.rc

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main      //入口
    ... ...
    socket zygote stream 660 root system  //此进程需要创建socket
    ... ...
    //zygote重启时,需要同时重启以下服务
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart media.tuner
    onrestart restart netd
    onrestart restart wificond

zygote的定义找到了,在哪里启动的呢?init.rc中一番搜索之后,得出结论。

rc 复制代码
// 1. late-init中触发zygote-start
on late-init
	... ...
    trigger zygote-start
    ... ...

// 2. zygote-start中直接启动zygote
on zygote-start
    ... ...
    start zygote
    start zygote_secondary
    ... ...

这里我们就可以回答开头的疑问了,init确实启动了两个进程:zygote和zygote_secondary,从rc脚本的关系来看,zygote_secondary有点像zygote的守护进程,因为zygote_secondary重启时也会重启zygote。因此我们的主要目标还是zygote,zygote_secondary就不去深究了。

zygote的入口

zygote的入口函数在frameworks/base/cmds/app_process/app_main.cpp中。

等等,这里我们需要稍微停一下,你咋知道入口在这里?难不成自学一个模块,要在网上先搜索从哪里开始?如果没人输出过这个模块的文章,还学不成了?

其实不然,从开头rc文件的分析我们已经知道,zygote进程代码被存放在了一个二进制文件中/system/bin/app_process64,这个文件名一定在编译脚本(.bp文件)中出现过,所以我们需要在bp文件中找到它。这里又需要大家有一点AOSP编译方面的基础知识,但不要害怕,真的只需要一点,甚至直接打开bp文件都能猜个大概。bp文件的语法及详细解释可以参考官方文档

我们直接在cs.android.com上搜索app_process64,出来的结果不是很满意,没有bp文件。仔细看看,后缀64?标识系统平台?去掉后缀再搜。

大概模块位置也清楚了。bp文件挑重点看。

bp 复制代码
cc_binary {
    name: "app_process",
    srcs: ["app_main.cpp"],
    multilib: {
        lib32: {
            suffix: "32",
        },
        lib64: {
            suffix: "64",
        },
    }
    ... ...
}

太明显了,官方文档基本不需要看。盲猜一下,它说的是:编译一个二进制文件,名称为app_process,源码在当前目录的app_main.cpp中,multilib指定多平台后缀。真真儿的大白话啊。

好了,这下我们可以非常安心的说出开头那句话:zygote的入口函数在frameworks/base/cmds/app_process/app_main.cpp中。

app_main.cpp

cpp 复制代码
// frameworks/base/cmds/app_process/app_main.cpp
int main(int argc, char* const argv[])  {
	//很多与参数相关的操作,读取、解析、设置等
	... ...
	if (strcmp(arg, "--zygote") == 0) {
		// rc文件中配置的启动参数有--zygote,因此变量zygote为true
	    zygote = true;
	    // ZYGOTE_NICE_NAME为宏定义,值为zygote64,即进程名
	    niceName = ZYGOTE_NICE_NAME;  
	}
	... ...
	if (zygote) {
	    // runtime的start函数参数:一个类名、args字符数组(参数)以及zygote布尔变量
	    runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
	} else if (!className.isEmpty()) {
	    runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
	} else {
	    fprintf(stderr, "Error: no class name or --zygote supplied.\n");
	    app_usage();
	    LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
	}
}

简单回溯一下runtime实例,可以得知,对应的类为AppRuntime,其父类为AndroidRuntime,start函数是在父类中实现的。

cpp 复制代码
// frameworks\base\core\jni\AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)  {
	... ...
	static const String8 startSystemServer("start-system-server");  
	// Whether this is the primary zygote, meaning the zygote which will fork system server.
	// 是否是主zygote就看是否有start-system-server参数,即是否负责启动system server进程。
	// 这就是zygote和zygote_secondary的区别之一
	bool primary_zygote = false;
	... ...
	/* start the virtual machine */
	// 初始化JNI环境,这里我们只需要理解JNI就是连接C++和Java的桥梁即可
	JniInvocation jni_invocation;
	// Init函数中会动态加载libart.so库,这个在后面会用到
	jni_invocation.Init(NULL);
	JNIEnv* env;
	// 创建虚拟机,如何创建?后面介绍
	if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
	    return;
	}
	onVmCreated(env);

    // startReg中注册当前阶段需要使用的JNI函数
    if (startReg(env) < 0) {  
	    ALOGE("Unable to register all android natives\n");  
	    return;  
	}
	... ...
	char* slashClassName = toSlashClassName(className != NULL ? className : "");  
	jclass startClass = env->FindClass(slashClassName);  
	if (startClass == NULL) {  
	    ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);   
	} 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 {
		    // 最终调用com.android.internal.os.ZygoteInit的main函数
		    // 这句调用是阻塞的,也就是说如果Java中的函数不执行完
		    // CallStaticVoidMethod就不会返回
	        env->CallStaticVoidMethod(startClass, startMeth, strArray);
	    }
	}
	... ...
}

以上代码中env(JNI环境)的各种调用就是C层调用Java函数的用法,看起来是不是跟Java反射调用的代码很相似。关于JNI的介绍,可以参考这一篇,快速过一下,用到时再查阅细节。

稍微解释一下虚拟机的创建,只是介绍个开始😂

cpp 复制代码
// frameworks\base\core\jni\AndroidRuntime.cpp
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool primary_zygote)  {
	... ...
	/*  
	 * 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. 
	 */
	// JNI_CreateJavaVM最终会调用libart.so中的函数JNI_CreateJavaVM创建虚拟机
	// 需要研究虚拟机的伙伴,可以从这个so开始
	if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
	    ALOGE("JNI_CreateJavaVM failed\n");
	    return -1;
	}
	return 0;
}

ZygoteInit

到这里,我们已经进入Java Framework。

java 复制代码
// frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

public static void main(String[] argv) {
	// 我们的zygote在rc文件中并没有配置懒加载,因此这里肯定会执行if分支
	if (!enableLazyPreload) {
		... ...
		// 预加载类和资源
		// 比如:/system/etc/preloaded-classes中的Java类,我们熟悉的四大组件赫然在列
		preload(bootTimingsTraceLog);
		... ...
	}

	if (startSystemServer) {
		Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);
		// {@code r == null} in the parent (zygote) process, 
		// and {@code r != null} in the child (system_server) process.
		// 这两行注释的意思是:
		// 在zygote进程中会返回null,接下来执行runSelectLoop
		// 在SystemServer进程中会返回一个runnable,接下来执行r.run()
		if (r != null) {
		    r.run();
		    return;
		}
	}

	Log.i(TAG, "Accepting command socket connections");
	// The select loop returns early in the child process after a fork and  
	// loops forever in the zygote.
	caller = zygoteServer.runSelectLoop(abiList);
	... ...
}

系统启动流程到forkSystemServer函数这里就开始"分叉"了,因为一个新的进程SystemServer诞生了。zygote和SystemServer将分别执行各自的后续逻辑。我们在下一篇讲SystemServer的文章中会以forkSystemServer函数作为开始。这里我们继续跟踪zygote的逻辑。

java 复制代码
// frameworks/base/core/java/com/android/internal/os/ZygoteServer.java

Runnable runSelectLoop(String abiList) {
	... ...
	while (true) {
		... ...
		// poll函数等待pollFDs文件描述符状态变化的消息
		//
		// pollTimeoutMs参数:
		// 为正:在指定的毫秒数内等待文件描述符的状态变化,超时或有消息到来后poll就返回
		// 为0:表示立即检测文件描述符状态,立即返回
		// 为-1:表示一直等待,直到有文件描述符的状态变化
		//
		// pollReturnValue返回值:
		// 为0:没有文件描述符可用,即没有需要处理的消息
		// 为正:有文件描述符可用,即需要处理消息
		// 为负:出现错误
		pollReturnValue = Os.poll(pollFDs, pollTimeoutMs);
		... ...
	}
}

正常情况下,Zygote进程就会在这个循环里一直执行下去,响应新建进程的消息或继续循环等待。

注:while循环中会有USAP(Unspecialized App Process)的逻辑,这是Android10之后,新增的提升进程创建效率的机制,简单来说,就是zygote会预创建一些进程放入池中,当有新建需求时,就从池中取出一个。为了突出主线,我们剥离了这部分逻辑,若想深入了解,可以参考芦大佬的这篇文章

总结

流程来到zygote这里,逻辑变得复杂,Zygote自身的初始化、虚拟机的创建、特殊Zygote进程(webview_zygote)的创建、SystemServer的创建、普通APP进程的创建、USAP机制,代码分支众多,判断条件一层叠一层,我们紧紧抓住Zygote主线进行探索,后续再逐步完善其他分支的理解。让我们用一张图概括一下这段历程。

C/C++下的日志开关

面对复杂的逻辑,仅仅看代码已经不能满足需求,因此我们需要观察日志。对于不熟悉C/C++的伙伴,简单介绍一下相关内容。

以app_main.cpp为例,其中main函数一进来就会看到

cpp 复制代码
if (!LOG_NDEBUG) {  
  String8 argv_String;  
  for (int i = 0; i < argc; ++i) {  
    argv_String.append("\"");  
    argv_String.append(argv[i]);  
    argv_String.append("\" ");  
  }  
  ALOGV("app_process main with argv: %s", argv_String.string());  
}

LOG_NDEBUG是一个宏定义变量,是v级日志的输出开关,为0输出,为1不输出。默认情况下,只要你编译的是release版本,那么LOG_NDEBUG都为1。如果我们想要看到ALOGV的输出,就需要在当前cpp文件最开始加入"#define LOG_NDEBUG 0"。 不要被if语句的判断条件迷惑,如果不在文件开头定义LOG_NDEBUG为0,即使把if分支里面的代码提出来,一样得不到日志输出。这是为什么?因为ALOGV也是一个宏定义,且它的定义与LOG_NDEBUG的取值相关。具体原因看看以下宏定义就明白了。另外,除了在IDE中看日志,我们也经常使用命令导出日志到本地查看:adb logcat -d > log.txt。

cpp 复制代码
// system/logging/liblog/include/log/log_main.h
/*
 * Normally we strip the effects of ALOGV (VERBOSE messages),
 * LOG_FATAL and LOG_FATAL_IF (FATAL assert messages) from the
 * release builds by defining NDEBUG.  You can modify this (for
 * example with "#define LOG_NDEBUG 0" at the top of your source
 * file) to change that behavior.
 */

// 其实上面的注释已经说了打开日志的方法,有时看注释确实能走很多捷径
// NDEBUG也是一个宏定义变量,它是编译器在编译期间定义的,我这里在代码中打印它是为1
// 我猜测如果编译的是debug版本,NDEBUG应该打印出来就是0
#ifndef LOG_NDEBUG
#ifdef NDEBUG
#define LOG_NDEBUG 1
#else
#define LOG_NDEBUG 0
#endif
#endif
... ...
// ALOGV的宏定义
#ifndef ALOGV
#define __ALOGV(...) ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__))
#if LOG_NDEBUG
#define ALOGV(...)                   \
  do {                               \
    __FAKE_USE_VA_ARGS(__VA_ARGS__); \
    if (false) {                     \
      __ALOGV(__VA_ARGS__);          \
    }                                \
  } while (false)
#else
#define ALOGV(...) __ALOGV(__VA_ARGS__)
#endif
#endif

通过上面的头文件内容,还可以知道,我们可以通过LOG_TAG这个宏定义变量设置日志Tag,也是在cpp文件开头定义即可,就像我们经常在Java类第一行写的"public static final String TAG = xxx"。

相关推荐
xvch1 分钟前
Kotlin 2.1.0 入门教程(二十五)类型擦除
android·kotlin
simplepeng8 小时前
我的天,我真是和androidx的字体加载杠上了
android
别说我什么都不会9 小时前
鸿蒙轻内核M核源码分析系列十五 CPU使用率CPUP
操作系统·harmonyos
小猫猫猫◍˃ᵕ˂◍10 小时前
备忘录模式:快速恢复原始数据
android·java·备忘录模式
CYRUS_STUDIO12 小时前
使用 AndroidNativeEmu 调用 JNI 函数
android·逆向·汇编语言
梦否12 小时前
【Android】类加载器&热修复-随记
android
徒步青云12 小时前
Java内存模型
android
今阳12 小时前
鸿蒙开发笔记-6-装饰器之@Require装饰器,@Reusable装饰器
android·app·harmonyos
-优势在我17 小时前
Android TabLayout 实现随意控制item之间的间距
android·java·ui
hedalei17 小时前
android13修改系统Launcher不跟随重力感应旋转
android·launcher