Android zygote访谈录

戳蓝字“牛晓伟”关注我哦!

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章,技术文章也可以有温度。

本文摘要

本文以访谈的方式来带大家了解zygote进程,了解zygote进程是啥?它的作用是啥?它是如何一步一步“长大成人”的。(文中代码基于Android13)

Android native系列的文章如下:
Android系统native进程之我是init进程
Android系统native进程之属性能力的设计魅力
Android系统native进程之进程杀手–lmkd
Android系统native进程之日志系统–logd、logcat
Android系统native进程之我是installd进程
Apk安装之谜
Android 存储成长记
Android vold(卷管理)
Android ServiceManager和它的兄弟们

Android 大话binder通信

鼎鼎大名的zygote

主持人:“大家好啊,我是今天的主持人,你们大伙儿可是赚到,为啥因为我今天有请到了鼎鼎大名的zygote,千万别和我说你不认识她,在Android所有的系统native进程中,她的名气已经完全超过了vold、installd、lmkd等兄弟们,甚至连她的父亲init都自愧不如。”

一位观众提问到:“不好意思主持人,我确实也听说过她,但都是从别人嘴里面得知的,完全不知道zygote名气大的原因是啥?”

“谢谢这位观众,这确实是我的疏忽,首先我认为zygote名气大的原因是她有很多的子进程,而这些子进程是可以直接跟用户打交道的比如微信、抖音,而像init它的很多直接子进程都是demon类型的,只是在后台默默无闻的工作,用户对他们完全没感知。其次她是所有系统native进程中唯一可以运行Java/Kotlin代码的进程。那就有请我们今天的主角zygote吧。”

zygote:“大家好,主持人完全过奖了,我可不敢当。我的真名是zygote64是一个系统native进程。我其实还有个妹妹她的真名字是zygote,她也和我一样也是一个系统native进程。我和我妹妹的主要工作职责是fork (孵化)可运行java代码的进程,我和她分工明确我是孵化64位进程,而她是孵化32位进程,这也就是为啥我的真名后面有64的原因。别看我的工作职责很单一,可是有非常多的重量级的进程如systemserver进程可都是我孵化出来的,而我妹妹是没有孵化systemserver进程的。”

又一位观众提问到:“您好zygote,我这有问题请教您提到的fork是啥意思?”

zygote:“fork是一个孵化子进程的方法,该方法的一个非常突出的特点就是,能以快到惊人的速度把子进程创建好,fork做到如此之快的原因是会创建一个与父进程几乎完全相同的副本,子进程从父进程继承了所有的内存布局、环境变量、打开的文件描述符等,也就是子进程会与父进程共享非常多的数据,在内核层只需要为子进程创建很少的数据即可。还有一个概念写时复制 (Copy-on-Write),子进程与父进程共享的数据,比如子进程或者父进程改变了一些数据的话,则这些数据不会再是共享状态了,而是会拷贝出一份来,这就是写时复制。”

这位观众继续提问到:“听了您的解释还是有些懵圈,能具体点吗?”

zygote:“那这样吧,我们来看段代码,看了代码您肯定就明白了。”

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  int globvar = 6;
char globbuf[] = "我是全局字符串";int main() {  pid_t pid;  int localvar = 88;// 调用fork()  pid = fork();  //小于0,则代表fork失败if (pid < 0) {  fprintf(stderr, "Fork failed\n");  return 1;  } else if (pid == 0) {  //pid == 0,则代表是fork成功的子进程,子进程会执行下面代码printf("I am the child process, my PID is %d\n", getpid());  printf("My parent's PID is %d\n", getppid());  } else {  // fork()返回非零值,表示这是父进程,返回值是子进程的PID,父进程执行下面代码printf("I am the parent process, my PID is %d\n", getpid());  printf("My child's PID is %d\n", pid);  } return 0;  
}

如上面的代码,其中globvarglobbuf都是全局变量,而localvar是局部变量,当fork成功后,子进程会把父进程的globvarglobbuflocalvar继承过来,其实也就是共享过来,它们的值也和父进程的值一样。

这就是fork后子进程与父进程共享数据,其实也就是有点懒加载的味道,你想啊如果孵化子进程的时候一上来就要把各种数据都复制一份,首先会出现浪费的情况,因为这些数据有可能很多是子进程和父进程都不会去修改的,其次孵化子进程的速度肯定慢。

同样还是使用上面的例子,来介绍下写时复制,如下代码

    } else if (pid == 0) {  //pid == 0,则代表是fork成功的子进程,子进程会执行下面代码localvar = 100;globvar = 888;省略其他代码......}

上面代码,当子进程fork成功后,只是把localvarglobvar修改为100和888,则这时候localvarglobvar在子进程复制出自己单独的一份数据,与父进程的localvarglobvar已经不是共享了,而globbuf变量还是共享状态。

这位观众又说到:“谢谢您的解答,我还有个大胆的想法,感觉您只是简单的fork了子进程,如果孵化子进程的工作不由您来做,而是让init进程启动systemserver进程,systemserver进程来fork子进程,这样做的话会节省内存开销 (因为zygote进程不需要启动),同时加快孵化子进程的速度 (因为由您孵化子进程需要跨进程通信,而直接由systemserver就不用跨进程了)。不知道我这粗略的想法对不对。”

zygote:“哈哈,你这想法非常大胆,但是会有一个严重的问题,上面也提过fork机制子进程会与父进程共享非常多的数据,而systemserver中的很多的数据首先是非常重要及对安全性要求非常高的,这些数据如果被子进程共享的话,想想后果都非常可怕。还有systemserver中的非常多的数据对于子进程是完全无用的 (比如binder等相关数据binder驱动描述符了等),因为子进程需要自己的binder数据。因此fork子进程需要一个干净纯粹的环境。”

zygote拖了拖腮帮子继续说到:“而我可以提供这个非常干净的环境,别以为这样就完事了,还没有,你想啊一个可运行Java代码的进程肯定是需要加载JVM (Java虚拟机)的,如果每个子进程都加载一次JVM,那它们的启动速度可想而知了。作为母亲的我怎么可能让这种事情发生呢,我本着能让自己多吃苦多受罪也不能让孩子们吃一丁点苦的原则,我会加载JVM以及一些公共的资源,这样当子进程fork成功后,它们一“出生”就有了JVM,它们的启动速度可是杠杠硬啊。”

这位观众有些羞愧的说到:“不好意思啊,我这大胆的想法太过于鲁莽了,您说的我都明白了,谢谢。”

zygote:“没事,技术探讨不分对错,我也像我的父亲init进程一样拥有很多的孩子,下图是我和我妹及我的部分家族成员。”

image

如上图,zygote64是我,我的pid是1221,可以看出所有的子进程都基本是由我fork出来的。

主持人:“像您这么有名气,您能分享下您的成名过程吗,供年轻人参考参考,还有您是基于什么样的机缘巧合要立志成名的。”

zygote:“其实也没啥机缘巧合,无非是榜样的力量,当我刚出生不久的时候,我父亲init就是我的榜样,当我看到他有那么多的孩子 (子进程),我就立志也要像他一样能拥有很多的孩子 (子进程),我特别特别喜欢孩子。为了成名我可是吃了不少苦、做了非常多的努力。那就从我的出生说起吧。”

我的出生

我的出生要从init脚本文件说起。

主持人:“为啥从脚本文件说起?”

zygote:“是这样的,大家都知道我的父亲是init进程,因为它的子进程是非常非常多的,这么多子进程何时创建、创建之前需要执行哪些命令又更是多上加多,这么多的信息它完全是无招架之力,为了解决这个问题它创建了init脚本语言,哪个子进程需要创建,则配置自己的init脚本语言即可。”

主持人:“了解了解,那您继续说吧。”

下面是我和我妹的init脚本语言:

//文件路径:system/core/rootdir/init.zygote64_32.rc//名为zygote的service,在fork成功后会执行 /system/bin/app_process64 可执行文件,它是64为的,后面是跟的参数,--start-system-server是代表要fork systemserver进程,--socket-name代表启动的server socket的名字
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote//该service属于main类别class mainpriority -20//user是root级别的user rootgroup root readproc reserved_disksocket zygote stream 660 root systemsocket usap_pool_primary stream 660 root system//在重启的时候会重新启动下面这些服务onrestart exec_background - system system -- /system/bin/vdc volume abort_fuseonrestart write /sys/power/state ononrestart restart audioserveronrestart restart cameraserveronrestart restart mediaonrestart restart media.tuneronrestart restart netdonrestart restart wificondtask_profiles ProcessCapacityHigh MaxPerformancecritical window=${zygote.critical_window.minute:-off} target=zygote-fatal//名为zygote_secondary的service,fork成功后会执行/system/bin/app_process32可执行文件,它是32位的,后面是可执行文件跟随的参数,--socket-name代表启动的server socket的名字,--enable-lazy-preload代表不需要预加载各种资源
service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary --enable-lazy-preloadclass mainpriority -20user rootgroup root readproc reserved_disksocket zygote_secondary stream 660 root systemsocket usap_pool_secondary stream 660 root systemonrestart restart zygotetask_profiles ProcessCapacityHigh MaxPerformance

上面脚本文件配置了两个服务zygotezygote_secondary,下面简单列下它们的区别吧:

  1. zygote服务:它的可执行文件是/system/bin/app_process64,参数分别为–zygote --start-system-server --socket-name=zygote
  2. zygote_secondary服务:它的可执行文件是/system/bin/app_process32,参数分别为–zygote --socket-name=zygote_secondary --enable-lazy-preload

先记住上面的几个参数,在用到它们的时候在解释。

配置了上面的init脚本文件,还需要在init.rc脚本文件中配置何时启动zygotezygote_secondary服务,如下:

//文件路径:system/core/rootdir/init.rc
on zygote-start && property:ro.crypto.state=unencryptedwait_for_prop odsign.verification.done 1# A/B update verifier that marks a successful boot.exec_start update_verifier_nonencryptedstart statsdstart netdstart zygotestart zygote_secondary

init进程会在对应的时机,分别启动zygotezygote_secondary服务,启动这俩服务会fork (孵化)zygote64zygote进程,进而会分别执行/system/bin/app_process64和/system/bin/app_process32可执行文件,这俩可执行文件最终都会执行到app_main.cppmain方法,如下代码:

//文件路径:cmds/app_process/app_main.cpp
int main(int argc, char* const argv[])
{省略代码......
}

执行到app_main.cppmain方法,我和我妹妹就出生了。

主持人:“我记得要想做成一件事情,得需要列一些计划,您作为成功人士应该也不例外吧,若有计划的话能否讲讲您的计划?”

zygote:“是的,我确实列了很多的计划,那我就来分享给大家。”

我的计划

我的计划主要分为:解析参数、启动JVM、拦截native线程创建、注册所有JNI方法、进入Java世界、解析参数、预加载资源、启动systemserver进程、可孵化App进程。计划的前一步都是在为计划的后一步做铺垫。

主持人:“那就依次来介绍下我的计划,把您的成名之路分享给大家吧。”

解析参数

zygote:“解析参数作为计划的第一步,主要是把脚本文件中的参数进行解析。”

还记得在我的出生介绍过我和我妹妹的init脚本信息吗,在脚本信息中会传递一些参数,会在app_main.cppmain方法中解析这些参数,这些参数可是非常的重要。有非常重要的一点再次提醒下我和我妹可是两个不同进程,是分别执行app_main.cpp的main方法

传递给我的参数有–zygote、–start-system-server、–socket-name=zygote,其中start-system-server则代表我会启动systemserver进程,socket-name则用来建立socket通信,也就是systemserver进程想要孵化子进程的话,则会通过socket发信息给我

传递给我妹的参数有–zygote、–socket-name=zygote_secondary --enable-lazy-preload,这里只解释下enable-lazy-preload,它代表不需要预加载公共资源

这些参数会被重新放置在类型为Vectorargs变量中,该变量会被传递给上层。

产物

会生成一个Vectorargs变量,它装载了init脚本传递过来的参数。

启动JVM

zygote:“启动JVM作为第二步,JVM大家肯定都非常熟悉了,Java/Kotlin代码要想运行肯定是离不开它的,为了不让每个fork出来的子进程在重复的启动JVM,也为了让子进程启动速度更快。因此我会把JVM启动,这样子进程再fork成功后就能直接使用我启动的JVM实例了。同时启动JVM也是最关键最核心的一步,没有它则后面的计划根本执行不了。启动JVM的工作我是交给了AppRuntime,而它继承了AndroidRuntime,那就有请它俩来给大家介绍下吧。”

//文件路径:cmds/app_process/app_main.cpp
int main(int argc, char* const argv[])
{//构造AppRuntime实例AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));省略代码......//zygote值为trueif (zygote) {//调用start方法开始启动JVM,并做一些其他的初始化工作runtime.start("com.android.internal.os.ZygoteInit", args, zygote);}省略代码......}

AppRuntime:“可以看下上面的代码,使用init脚本文件传递过来的参数初始化一个AppRuntime的实例,进而调用我的start方法会把com.android.internal.os.ZygoteInitargszygote为true,这几个参数传递给我。”

主持人:“方便解释下这几个参数吗?”

AppRuntime:“好的,com.android.internal.os.ZygoteInit聪明的你一定能看出它是一个Java类,对的它就是zygote进程进入Java世界的入口类,args解析参数阶段的产物。进入start方法就交给AndroidRuntime了。”

启动何种JVM

AndroidRuntime:“在启动JVM之前,我会对一些目录进行检查比如/apex/com.android.art等,检查通过后,就准备启动JVM了,我采用解耦式来启动JVM。”

主持人:“解耦式?这个名字很新鲜啊,给解释下呗。”

AndroidRuntime:“解耦式就是指把启动JVM与启动何种JVM分离开,我AndroidRuntime只定义启动JVM的动作和返回信息,具体是启动Dalvik JVM还是ART JVM甚至是别的类型的JVM,我通通都不关心。”

主持人:“明白,也就是您只定义一些规范和接口,不关心实现者的具体实现。”

AndroidRuntime:“对的,我定义了如下接口,请看如下代码。”

struct JniInvocationImpl {省略代码......// Function pointers to methods in JNI provider.jint (*JNI_GetDefaultJavaVMInitArgs)(void*);//创建虚拟机的方法jint (*JNI_CreateJavaVM)(JavaVM**, JNIEnv**, void*);//创建或者获取JavaVM实例jint (*JNI_GetCreatedJavaVMs)(JavaVM**, jsize, jsize*);
};

如上JniInvocationImpl结构体,咱们主要看创建JVM的接口JNI_CreateJavaVM,它的第一个参数类型是JavaVM类型的,第二个参数是JNIEnv类型的,这两个参数都是out类型的,也就是具体实现者创建JVM成功后,会把自己的实现的JavaVMJNIEnv实例赋值给这两个参数,而第三个参数是启动JVM需要传递的参数。

JavaVMJNIEnv它们到底有啥作用呢?

咱们声明了创建JVM的JNI_CreateJavaVM方法指针,既然创建了JVM,那肯定还需要一些与JVM进行交流的机制比如销毁JVM,那JavaVM的作用就是与JVM进行交流的比如声明DestroyJavaVM方法来销毁JVM,而具体如何销毁JVM,是由创建JVM的具体实现者提供的。一个进程只存在一个JavaVM实例。

JNIEnv大家肯定在jni方法中经常看到,它的主要作用是让native代码 (c/c++)可以操控Java层的类、对象、类的方法、对象的方法。比如CallStaticVoidMethodV方法的作用就是让native代码可以调用Java的某个类的静态方法。一个线程也只存在一个JNIEnv实例。

不管是JavaVM还是JNIEnv,只是定义了一些接口,不管是创建JVM的接口JNI_CreateJavaVM还是该接口返回的JavaVMJNIEnv到底是什么类型的实例,都是由创建JVM的真正实现者来实现的。

下面先来看下它们的声明,请自行取阅:

//文件路径:libnativehelper/include_jni/jni.hstruct _JavaVM {const struct JNIInvokeInterface* functions;#if defined(__cplusplus)jint DestroyJavaVM(){ return functions->DestroyJavaVM(this); }jint AttachCurrentThread(JNIEnv** p_env, void* thr_args){ return functions->AttachCurrentThread(this, p_env, thr_args); }jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); }jint GetEnv(void** env, jint version){ return functions->GetEnv(this, env, version); }jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args){ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
typedef _JavaVM JavaVM;//文件路径:libnativehelper/include_jni/jni.hstruct _JNIEnv {/* do not rename this; it does not seem to be entirely opaque *///初始化了它,所有的方法的实现都是调用它const struct JNINativeInterface* functions;#if defined(__cplusplus)jint GetVersion(){ return functions->GetVersion(this); }jclass DefineClass(const char *name, jobject loader, const jbyte* buf,jsize bufLen){ return functions->DefineClass(this, name, loader, buf, bufLen); }jclass FindClass(const char* name){ return functions->FindClass(this, name); }void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...){va_list args;va_start(args, methodID);functions->CallStaticVoidMethodV(this, clazz, methodID, args);va_end(args);}void CallStaticVoidMethodV(jclass clazz, jmethodID methodID, va_list args){ functions->CallStaticVoidMethodV(this, clazz, methodID, args); }void CallStaticVoidMethodA(jclass clazz, jmethodID methodID, const jvalue* args){ functions->CallStaticVoidMethodA(this, clazz, methodID, args); }省略代码......
}typedef _JNIEnv JNIEnv;

主持人:“接口我看到了,那到底启动的是何种类型的JVM呢?”

AndroidRuntime:“启动JVM之前是需要传递各种参数的,在Android13上启动的是ART JVM,具体实现是在libart.so库中。JavaVM的真正实例是JavaVMExt类型,JNIEnv的真正实例是JNIEnvExt类型。”

如下查找创建JVM的真正实现者相关代码,请自行取阅:

//文件路径:libnativehelper/JniInvocation.c
//该方法主要是查找创建JVM的真正实现者
bool JniInvocationInit(struct JniInvocationImpl* instance, const char* library_name) {#ifdef __ANDROID__char buffer[PROP_VALUE_MAX];#elsechar* buffer = NULL;#endif//获取library_name,这时候它的值是 libart.solibrary_name = JniInvocationGetLibrary(library_name, buffer);DlLibrary library = DlOpenLibrary(library_name);省略代码......DlSymbol JNI_GetDefaultJavaVMInitArgs_ = FindSymbol(library, "JNI_GetDefaultJavaVMInitArgs");if (JNI_GetDefaultJavaVMInitArgs_ == NULL) {return false;}DlSymbol JNI_CreateJavaVM_ = FindSymbol(library, "JNI_CreateJavaVM");if (JNI_CreateJavaVM_ == NULL) {return false;}DlSymbol JNI_GetCreatedJavaVMs_ = FindSymbol(library, "JNI_GetCreatedJavaVMs");if (JNI_GetCreatedJavaVMs_ == NULL) {return false;}//下面代码把libart.so的相关方法赋值给instanceinstance->jni_provider_library_name = library_name;instance->jni_provider_library = library;instance->JNI_GetDefaultJavaVMInitArgs = (jint (*)(void *)) JNI_GetDefaultJavaVMInitArgs_;instance->JNI_CreateJavaVM = (jint (*)(JavaVM**, JNIEnv**, void*)) JNI_CreateJavaVM_;instance->JNI_GetCreatedJavaVMs = (jint (*)(JavaVM**, jsize, jsize*)) JNI_GetCreatedJavaVMs_;return true;
}

创建JVM相关代码,请自行取阅:

 
//文件路径:core/jni/AndroidRuntime.cppvoid AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{省略代码....../* start the virtual machine */JniInvocation jni_invocation;//调用jni_invocation的Init方法,会去找创建JVM的真正实现者是谁jni_invocation.Init(NULL);JNIEnv* env;//会调用下面的startVm方法,开始创建JVM,mJavaVM是JavaVM类型,env是JNIEnv类型if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) { //zygote为true primary_zygote为truereturn;}省略代码......
}int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool primary_zygote)
{JavaVMInitArgs initArgs;省略各种创建JVM参数的代码...../** 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创建JVMif (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {ALOGE("JNI_CreateJavaVM failed\n");return -1;}return 0;
}
产物

该阶段会启动ART JVM,启动成功后会创建JavaVMExt的对象赋值给pJavaVM,同时也会创建JNIEnvExt的对象赋值给pEnv,它们可都是在后面计划中起着非常重要的作用。

启动ART JVM后预示着可以运行Java代码了,但是先别急还有一些事情要做。

拦截native线程创建

主持人:“拦截native线程创建,这个计划着实有些让人摸不着头脑。”

AndroidRuntime:“哈哈,确实有些晕圈,在介绍该计划之前,我先介绍下Android中的线程吧。”

Android中的线程分为两大类:Java线程native线程。而native线程又可分为:纯native线程可调用Java代码的native线程

可调用Java代码的native线程是指该native线程的native代码是可以调用Java层的类、对象、类的方法、对象方法等;反之纯native线程就是指native线程的native代码不能调用Java层的任何代码。

对于Java线程的Java代码是可以通过jni调用native层的代码,而native层的代码如果要想调用Java层代码就需要用到JNIEnv对象,而该对象是在每个Java线程创建成功后会自动创建的。

而对于可调用Java代码的native线程,也需要使用到JNIEnv对象才能调用Java层的代码,因此拦截native线程所做的事情就是针对 可调用Java代码的native线程 拦截它的创建过程,这样就可以把JNIEnv对象交给它,进而可以调用Java层代码。

主持人:“创建的JNIEnv对象从何而来呢?”

还记得在启动JVM阶段,libart.so库创建的JavaVMExt对象吗?它可是每个进程都拥有的,调用它的AttachCurrentThread方法就可以创建JNIEnvExt类型的对象 (JNIEnvExt是JNIEnv的子类)

如下是部分代码,请自行取阅:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{省略代码....../* start the virtual machine */JniInvocation jni_invocation;//调用jni_invocation的Init方法,会去找创建JVM的真正实现者是谁jni_invocation.Init(NULL);JNIEnv* env;//会调用下面的startVm方法,开始创建JVM,mJavaVM是JavaVM类型,env是JNIEnv类型if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) { //zygote为true primary_zygote为truereturn;}/** Register android functions.*/if (startReg(env) < 0 ) {ALOGE("Unable to register all android natives\n");return;}省略代码......
}int AndroidRuntime::startReg(JNIEnv* env)
{省略代码......androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);省略代码......return 0;
}
产物

该阶段可调用Java代码的native线程的native代码是可以调用Java层的类、对象、类的方法、对象方法等Java层的代码。

注册所有JNI方法

JNI,它是Java Native Interface的缩写,它定义了Java层与native层之间的接口,这些接口约定了Java层与native层方法调用的规则。

JNI到底长啥样子,看下面代码:

//文件路径:/libnativehelper/include_jni/jni.htypedef struct {//name对应Java层的方法,在Java类中使用native关键字来标注该方法const char* name;//方法签名,会表明方法的参数、返回值等信息const char* signature;//native方法指针,也就是在Java层调用名称为name的方法,最终会调用到fnPtrvoid*       fnPtr;
} JNINativeMethod;

是不是有些抽象是吧,那举些例子吧,如下代码:

  
public class ZygoteInit{省略代码......private static native void nativeZygoteInit();省略代码......
}//文件路径:core/jni/AndroidRuntime.cppconst JNINativeMethod methods[] = {{ "nativeZygoteInit", "()V",(void*) com_android_internal_os_ZygoteInit_nativeZygoteInit },};

如上例子,ZygoteInit类的nativeZygoteInit方法是一个native方法,该方法没有参数,也没有返回值,因此它的签名是 ()V ,而它在native层的真正实现者是com_android_internal_os_ZygoteInit_nativeZygoteInit这个方法。

在Android系统中像ZygoteInit这样具有native方法的类很多比如ParcelBinder等,注册所有的JNI方法这里的所有就是指这些具有native方法的Java类。而注册就是调用JNIEnv对象的RegisterNatives方法进行注册,注册后才可以在Java层调用native层的方法。

如下是注册所有JNI方法的代码,有兴趣自行取阅:

int AndroidRuntime::startReg(JNIEnv* env)
{省略代码......//调用register_jni_procs方法注册所有的JNI方法,gRegJNI保存了所有的JNI方法if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) { //注册各种jni方法env->PopLocalFrame(NULL);return -1;}省略代码......return 0;
}static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{for (size_t i = 0; i < count; i++) {if (array[i].mProc(env) < 0) {
#ifndef NDEBUGALOGD("----------!!! %s failed to load\n", array[i].mName);
#endifreturn -1;}}return 0;
}static const RegJNIRec gRegJNI[] = {REG_JNI(register_com_android_internal_os_RuntimeInit),REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),省略代码......REG_JNI(register_android_os_Process),REG_JNI(register_android_os_SystemProperties),REG_JNI(register_android_os_Binder),REG_JNI(register_android_os_Parcel),省略代码......
}

注意,如上注册所有JNI方法的代码,其中用到的JNIEnv对象是在启动JVM那阶段的产生的JNIEnv对象。

产物

该阶段注册了所有的JNI方法,这样当fork出来的子进程就不需要再次注册所有的JNI方法了,因为它们已经从zygote进程“继承”了,这样做可以加快子进程的启动速度。

进入Java世界

以上的计划都是在native世界,那我们总得来到Java世界吧,init脚本携带的参数也解析了、ART JVM也启动了、所有的JNI方法也都注册了,有了这些基础就已经具备了进入Java世界的条件,进入Java世界唯一缺少的就是进入哪个类的哪个方法了?

还记得在启动JVM的时候,调用AppRuntimestart方法传递的第一个参数 com.android.internal.os.ZygoteInit ,它就是要进入Java世界的入口类,而要进入的方法是它的main方法。会调用JNIEnvCallStaticVoidMethod方法进入ZygoteInit类的main方法,当然也会把一些参数传递给main方法 (如init脚本携带的参数)

下面是对应代码,请自行取阅:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{省略代码....../** We want to call main() with a String array with arguments in it.* At present we have two arguments, the class name and an option string.* Create an array to hold them.*/jclass stringClass;jobjectArray strArray;jstring classNameStr;//使用JNIEnv来查找java/lang/String的classstringClass = env->FindClass("java/lang/String");assert(stringClass != NULL);strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);assert(strArray != NULL);//className就是 com.android.internal.os.ZygoteInitclassNameStr = env->NewStringUTF(className);assert(classNameStr != NULL);env->SetObjectArrayElement(strArray, 0, classNameStr);for (size_t i = 0; i < options.size(); ++i) {jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());assert(optionsStr != NULL);env->SetObjectArrayElement(strArray, i + 1, optionsStr);}/** Start VM.  This thread becomes the main thread of the VM, and will* not return until the VM exits.*/char* slashClassName = toSlashClassName(className != NULL ? className : "");jclass startClass = env->FindClass(slashClassName);if (startClass == NULL) {没找到ZygoteInit类} else {//查找main方法jmethodID startMeth = env->GetStaticMethodID(startClass, "main","([Ljava/lang/String;)V");if (startMeth == NULL) {没找到main方法} else {//调用ZygoteInit的main方法env->CallStaticVoidMethod(startClass, startMeth, strArray);#if 0if (env->ExceptionCheck())threadExitUncaughtException(env);
#endif}}//退出Java世界后需要释放slashClassName,并且把JVM销毁掉free(slashClassName);ALOGD("Shutting down VM\n");if (mJavaVM->DetachCurrentThread() != JNI_OK)ALOGW("Warning: unable to detach main thread\n");if (mJavaVM->DestroyJavaVM() != 0)ALOGW("Warning: VM did not shut down cleanly\n");
}

如上代码,进入Java世界后,会停留在Java世界,除非由于各种原因退出Java世界后会销毁JVM等操作。

产物

该阶段进入了ZygoteInit类的main方法,也代表着完全的进入了Java世界。

解析参数

从native层是传递了多个参数过来的,因此要在ZygoteInit类的main方法把这些参数解析出来,而这些参数大部分来自init脚本定义的参数,那我们再次把init脚本的部分信息请出来供大家看下 (如下代码)

//文件路径:system/core/rootdir/init.zygote64_32.rc//名为zygote的service,在fork成功后会执行 /system/bin/app_process64 可执行文件,它是64为的,后面是跟的参数,--start-system-server是代表要fork systemserver进程,--socket-name代表启动的server socket的名字
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote省略其他配置......service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary --enable-lazy-preload省略其他配置......

同时结合ZygoteInit类的main方法的解析参数代码 (如下)

  public static void main(String[] argv) {省略代码......try {省略代码......//是否要启动systemserver进程boolean startSystemServer = false;String zygoteSocketName = "zygote";String abiList = null;//是否是推迟预加载资源boolean enableLazyPreload = false; for (int i = 1; i < argv.length; i++) {//zygote64进程需要启动systemserver进程if ("start-system-server".equals(argv[i])) {startSystemServer = true;} else if ("--enable-lazy-preload".equals(argv[i])) {//zygote进程该值为true,代表不需要预加载资源enableLazyPreload = true;} else if (argv[i].startsWith(ABI_LIST_ARG)) { //这个值从native层传递过来的,是从 adb shell getprop ro.product.cpu.abilist64获取的 值是arm64-v8aabiList = argv[i].substring(ABI_LIST_ARG.length());} else if (argv[i].startsWith(SOCKET_NAME_ARG)) {//app_process64为zygote, app_process32为zygote_secondaryzygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length());} else {throw new RuntimeException("Unknown command line argument: " + argv[i]);}}省略代码......} catch (Throwable ex) {Log.e(TAG, "System zygote died with fatal exception", ex);throw ex;} finally {if (zygoteServer != null) {zygoteServer.closeServerSocket();}}// We're in the child process and have exited the select loop. Proceed to execute the// command.if (caller != null) {caller.run();}}

zygote:“如上代码,我zygote64进程,startSystemServer为true代表需要启动systemserver进程,enableLazyPreload为false代表需要预加载资源,zygoteSocketName值为zygote。”

“而我妹zygote进程,startSystemServer为false代表不需要启动systemserver进程,enableLazyPreload为true代表不需要预加载资源,zygoteSocketName值为zygote_secondary。”

预加载资源

预加载资源就是把每个子进程都会用到的通用的、静态的资源提前在我zygote内加载,这样当子进程被fork出来后就可以不需要在执行这些资源的加载流程了,完全加快了子进程的启动速度。

只有我zygote才会预加载资源,而我妹是不会的,其主要原因是因为我是作为fork子进程的主力,基本所有的子进程都是由我孵化的。而我妹只是一个辅助而已,有可能在Android设备打开的这段时间内我妹基本上不会孵化进程,或者说她孵化的子进程非常少,而为了这么少的子进程来提前预加载资源这不是大大的浪费珍贵的内存吗,这是大大的“犯罪”。

而通用的、静态的资源,这里的通用指所有子进程都会用到的,而静态指的是这些资源加载到内存后基本是不会发生变化的。

通用的、静态的资源有各种Java类,这些类提前加载到JVM内后,在子进程中就不需要再次加载它们了。还有Resource资源,比如基础图片、文字等,这些Resource资源提前加载进来子进程也不需要再次加载它们了。当然还有别的资源比如共享库、字体资源等。

下面是相关代码,请自行取阅:

public static void main(String[] argv) {省略代码......// In such cases, we will preload things prior to our first fork.//zygote64进程,该值为falseif (!enableLazyPreload) {bootTimingsTraceLog.traceBegin("ZygotePreload");EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,SystemClock.uptimeMillis());//预加载资源preload(bootTimingsTraceLog); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,SystemClock.uptimeMillis());bootTimingsTraceLog.traceEnd(); // ZygotePreload}省略代码......}static void preload(TimingsTraceLog bootTimingsTraceLog) {Log.d(TAG, "begin preload");bootTimingsTraceLog.traceBegin("BeginPreload");beginPreload();bootTimingsTraceLog.traceEnd(); // BeginPreloadbootTimingsTraceLog.traceBegin("PreloadClasses");//虚拟机预加载各种类preloadClasses();bootTimingsTraceLog.traceEnd(); // PreloadClassesbootTimingsTraceLog.traceBegin("CacheNonBootClasspathClassLoaders");cacheNonBootClasspathClassLoaders();bootTimingsTraceLog.traceEnd(); // CacheNonBootClasspathClassLoadersbootTimingsTraceLog.traceBegin("PreloadResources");//预加载ResourcepreloadResources();bootTimingsTraceLog.traceEnd(); // PreloadResourcesTrace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadAppProcessHALs");nativePreloadAppProcessHALs();Trace.traceEnd(Trace.TRACE_TAG_DALVIK);Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadGraphicsDriver");maybePreloadGraphicsDriver();Trace.traceEnd(Trace.TRACE_TAG_DALVIK);//预加载共享libpreloadSharedLibraries();//预加载字体资源preloadTextResources(); // Ask the WebViewFactory to do any initialization that must run in the zygote process,// for memory sharing purposes.WebViewFactory.prepareWebViewInZygote();endPreload();warmUpJcaProviders();Log.d(TAG, "end preload");sPreloadComplete = true;}
产物

该阶段预加载了各种资源,这样在子进程fork成功后就不需要再次加载了,大大的提高了子进程的启动速度。

启动systemserver进程

经过前面的几个阶段,是完全已经具备了fork子进程的能力了,但是作为所有子进程的“长子”systemserver进程,它是需要最先被启动的,只有它完全启动成功后,哪个子进程需要被fork都是由systemserver进程内发出的命令。

关于启动systemserver进程,在此节我们不做过多的讨论,会在systemserver进程那节咱们再来分析。

可孵化App进程

启动完毕systemserver进程后,前面的所有的铺垫工作启动ART JVM拦截native线程创建注册所有的JNI方法预加载资源启动systemserver进程都已经结束,那最后的工作内容就是孵化App进程,当systemserver进程发出孵化App进程的请求时候,zygote就开始孵化App进程。

那这就有个需要解决的问题了,systemserver进程和zygote64zygote进程完全不是一个进程,那它们之间应该采用何种通信方式呢?

到底是选用binder or socket呢 ?

我觉得需要从以下几方面考虑。

binder通信就一定比socket通信快吗?

大家都非常清楚binder通信要比socket通信快,但是这个快我觉得得有一个前提条件,那就是传递的数据量是不是比较大,比较大的话我觉得快,非常小的话就不至于快了,为啥这样说呢,还要从binder通信说起。

在binder通信中调用的方法的参数在通信过程中是只有一次拷贝,而像方法对应的code值 (int类型),它其实在通信过程中是需要两次拷贝的。而socket通信数据是需要两次拷贝的。

若采用binder通信调用了一个无参的方法,像code值在整个binder通信过程中是需要进行两次拷贝的;若采用socket通信实现同样功能的话,只需要传递一个类似于code值一样的简单类型,这时候的code值也是进行了两次拷贝。那在以上的情况下,就不见得binder通信比socket通信快了,甚至有可能比socket通信还慢,因为binder通信还要涉及到一些转换处理等流程。

若还是上面的情况,换成调用的方法的参数是简单类型,这时候也不见得binder通信比socket通信快了。

若采用binder通信,遇到哪些问题?需要做哪些处理?

采用binder通信的话,在fork子进程成功后需要做以下事情:

  1. 子进程是需要把从父进程继承过来的binder fd (文件描述符)关闭掉的
  2. binder使用mmap打开的匿名共享内存也需要ummap掉,因为这块内存是与zygote进程共享的,而其他子进程是完全不需要的。
  3. 子进程会继承父进程的线程,若线程存在锁的话也需要对锁进行特殊处理,否则子进程的继承过来的线程出现死锁情况。
  4. 当然除了上面还需要子进程重新把自己的binder驱动打开,在重新使用mmap打开自己的匿名共享内存。

上面想到的只是其中一部分,还有需要做的事情,因此如果采用binder通信的话,子进程需要做的事情真的非常多,这无疑增加了复杂度。

结合实际使用场景

systemserver进程是孵化App进程的发起方 (它是client端),而zygote是孵化App进程的执行方 (它是server端),非常的明确的一点client端只有一个,并且还有个很关键的一点它们之间传递的数据量不大

在基于传递的数据量不大情况下,binder通信不至于比socket通信快,并且若采用binder通信需要处理的问题确实比较多。基于以上分析反而binder通信的优势不明显了,而采用socket通信的话,首先是实现简单;其次是子进程fork成功后,只需要把继承过来的socket关闭掉即可,这时候在启动binder通信。

如上systemserver进程和zygote64zygote进程之间采用socket通信方式,zygote64zygote是作为server端,而systemserver是作为client端,zygote64zygote若没有孵化App进程的请求,则进入阻塞状态;如若有则开始孵化App进程,当孵化成功后,再次去查找是否有孵化APp进程的请,有孵化没有则进入阻塞状态。zygote64zygote就是这样周而复始的重复的执行着上面的流程。

关于如何孵化App进程的分析会在App启动流程章节再次进行探讨。

结束

主持人:“今天我们认识了zygote (真名是zygote64)和她的妹妹 (真名是zygote),zygote是孵化App进程的主力,而她的妹妹是作为辅助存在。zygote她是一个伟大的母亲,她为了让她的孩子们 (子进程)少受些‘累’,它把启动JVM注册所有的JNI方法预加载资源等这些事情提前做了,这样当她的孩子们在启动时候就不需要做这些重复的事情了,进而提升了它们的启动速度。并且她还启动了systemserver进程。关于zygote如何启动systemserver进程和孵化App进程的内容,会在后面节目中继续邀请她为大家分享。谢谢大家。”

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/371466.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从零开始开发跑腿配送系统:技术选型与架构设计

开发一个跑腿配送系统涉及多个技术栈和模块&#xff0c;从前端到后端&#xff0c;再到数据库和实时通信&#xff0c;每一个环节都至关重要。本文将详细介绍从零开始开发跑腿配送系统的技术选型与架构设计&#xff0c;并提供部分代码示例以帮助理解。 一、技术选型 前端技术&am…

国产化新标杆:TiDB 助力广发银行新一代总账系统投产上线

随着全球金融市场的快速发展和数字化转型的深入推进&#xff0c;金融科技已成为推动银行业创新的核心力量。特别是在当前复杂多变的经济环境下&#xff0c;银行业务的高效运作和风险管理能力显得尤为重要。总账系统作为银行会计信息系统的核心&#xff0c;承载着记录、处理和汇…

Linux网络管理

一、linux网络管理 1.获取计算机的网络信息 基本语法&#xff1a; #ifconfig #ip address &#xff08;ip a&#xff09; 解析&#xff1a; ens33&#xff1a;默认网卡 lo&#xff1a;环回网卡&#xff0c;127.0.0.1作为固定ip代表本机 virbr0&#xff1a;虚拟网络接口&…

Python入门 2024/7/3

目录 for循环的基础语法 遍历字符串 练习&#xff1a;数一数有几个a range语句 三个语法 语法1 语法2 语法3 练习&#xff1a;有几个偶数 变量作用域 for循环的嵌套使用 打印九九乘法表 发工资案例 continue和break语句 函数的基础定义语法 函数声明 函数调用 …

探索Linux:开源世界的无限可能

Linux是一款开源操作系统&#xff0c;它的起源可以追溯到上世纪90年代初。这个故事始于一个名叫Linus Torvalds的芬兰大学生&#xff0c;他在1983年开始编写一个用于个人电脑的操作系统内核。在他的努力下&#xff0c;Linux逐渐发展成为一个稳定而强大的操作系统。 然而&#…

Apache Seata配置管理原理解析

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Apache Seata配置管理原理解析 说到Seata中的配置管理&#xff0c;大家可能会想到Seata中适配…

盘点8款国内顶尖局域网监控软件(2024年国产局域网监控软件排名)

局域网监控软件对于企业网络管理至关重要&#xff0c;它们可以帮助IT部门维护网络安全&#xff0c;优化网络性能&#xff0c;同时监控和控制内部员工的网络使用行为。以下是八款备受推崇的局域网监控软件&#xff0c;每一款都有其独特的优势和适用场景。 1.安企神软件 试用版领…

【机器学习实战】Datawhale夏令营:Baseline精读笔记2

# AI夏令营 # Datawhale # 夏令营 在原有的Baseline上除了交叉验证&#xff0c;还有一种关键的优化方式&#xff0c;即特征工程。 如何优化特征&#xff0c;关系着我们提高模型预测的精准度。特征工程往往是对问题的领域有深入了解的人员能够做好的部分&#xff0c;因为我们要…

护网在即,知攻善防助力每一位安服仔~

前言 是不是已经有师傅进场了呢~ 是不是有安服&#x1f412;在值守呢~ 您是不是被网上眼花缭乱的常用应急响应工具而烦恼呢&#xff1f; 何以解忧&#xff1f;唯有知攻善防&#xff01; 创作起源&#xff1a; 驻场、护网等&#xff0c;有的客户现场只允许用客户机器&…

Linux-gdb

目录 1.-g 生成含有debug信息的可执行文件 2.gdb开始以及gdb中的常用执行指令 3.断点的本质用法 4.快速跳出函数体 5.其他 1.-g 生成含有debug信息的可执行文件 2.gdb开始以及gdb中的常用执行指令 3.断点的本质用法 断点的本质是帮助我们缩小出问题的范围 比如&#xff0c;…

Linux系统安装软件包的方法rpm和yum详解

起因&#xff1a; 本篇文章是记录学习Centos7的历程 关于rpm 常见命令 1&#xff09;查看已经安装的软件包 rpm -q 软件包名 2&#xff09;查看文件的相关信息 rpm -qi 软件包名 3&#xff09;查看软件包的依赖关系 就是说要想安装这个软件包&#xff0c;就必须把一些前…

非NI GPIB卡与LabVIEW兼容性分析

在许多测试和测量应用中&#xff0c;通用接口总线&#xff08;GPIB&#xff09;是一种广泛使用的标准。尽管国家仪器公司&#xff08;NI&#xff09;提供的GPIB硬件和LabVIEW软件的组合被广泛接受和使用&#xff0c;但成本可能较高。因此&#xff0c;一些用户会考虑使用其他厂商…

ASP.NET Core----基础学习03----开发者异常页面 MVC工作原理及实现

文章目录 1. 开发者异常页面(1)Startup.cs 页面的基础配置(2)自定义显示报错代码的前后XX行 2. MVC 的原理3. MVC 的实现4.默认路由路径5.返回Json字符串 1. 开发者异常页面 (1)Startup.cs 页面的基础配置 namespace ASP.Net_Blank {public class Startup{private readonly IC…

半导体制造企业 文件共享存储应用

用户背景&#xff1a;半导体设备&#xff08;上海&#xff09;股份有限公司是一家以中国为基地、面向全球的微观加工高端设备公司&#xff0c;为集成电路和泛半导体行业提供具竞争力的高端设备和高质量的服务。 挑战&#xff1a;芯片的行业在国内迅猛发展&#xff0c;用户在上海…

英语学习交流小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;每日打卡管理&#xff0c;备忘录管理&#xff0c;学习计划管理&#xff0c;学习资源管理&#xff0c;论坛交流 微信端账号功能包括&#xff1a;系统首页&#xff0c;学习资源&…

Python之numpy常用知识点总结

文章目录 前言知识点1&#xff1a;np.maximum知识点2&#xff1a;ndarray数据类型知识点3&#xff1a;数据运算知识点4&#xff1a;数组和标量间的运算知识点5&#xff1a;数组的索引和切片知识点6&#xff1a;数组的转置和轴对称知识点7&#xff1a;检索数组元素 前言 在机器学…

Java项目:基于SSM框架实现的德云社票务管理系统【ssm+B/S架构+源码+数据库+开题报告+毕业论文】

一、项目简介 本项目是一套基于SSM框架实现的德云社票务管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功…

Java连接Mysql数据库

在使用Java连接Mysql数据库之前我们得先下载一个Mysql JDBC驱动程序&#xff0c;为了防止大家迷路&#xff0c;我把链接放下面了 【免费】MysqlJDBC驱动程序资源-CSDN文库 下载完了之后&#xff0c;我们就需要导入Mysql JDBC驱动程序啦~ 1.首先我们先创建一个Java文件&#…

Vue 邮箱登录界面

功能 模拟了纯前端的邮箱登录逻辑 还没有连接后端的发送邮件的服务 后续计划&#xff0c;再做一个邮箱、密码登录的界面 然后把这两个一块连接上后端 技术介绍 主要介绍绘制图形人机验证乃个 使用的是canvas&#xff0c;在源码里就有 界面控制主要就是用 表格、表单&#x…

SLF4J的介绍与使用(有logback和log4j2的具体实现案例)

目录 1.日志门面的介绍 常见的日志门面 &#xff1a; 常见的日志实现&#xff1a; 日志门面和日志实现的关系&#xff1a; 2.SLF4J 的介绍 业务场景&#xff08;问题&#xff09;&#xff1a; SLF4J的作用 SLF4J 的基本介绍 日志框架的绑定&#xff08;重点&#xff09…