当前位置: 首页 > news >正文

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进程的内容,会在后面节目中继续邀请她为大家分享。谢谢大家。”

请添加图片描述

相关文章:

Android zygote访谈录

戳蓝字“牛晓伟”关注我哦&#xff01; 用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章&#xff0c;技术文章也可以有温度。 本文摘要 本文以访谈的方式来带大家了解zygote进程&#xff0c;了解zygote进程是啥&#xff1f;它的作用是啥&#xff1f;它是如何一步…...

nuxt、vue树形图d3.js

直接上代码 //安装 npm i d3 --save<template><div class"d3"><div :id"id" class"d3-content"></div></div> </template> <script> import * as d3 from "d3";export default {props: {d…...

香橙派AIpro测评:yolo8+usb鱼眼摄像头的Camera图像获取及识别

一、前言 近期收到了一块受到业界人士关注的开发板"香橙派AIpro",因为这块板子具有极高的性价比&#xff0c;同时还可以兼容ubuntu、安卓等多种操作系统&#xff0c;今天博主便要在一块832g的香橙派AI香橙派AIpro进行YoloV8s算法的部署并使用一个外接的鱼眼USB摄像头…...

大华设备接入GB28181视频汇聚管理平台EasyCVR安防监控系统的具体操作步骤

智慧城市/视频汇聚/安防监控平台EasyCVR兼容性强&#xff0c;支持多协议接入&#xff0c;包括国标GB/T 28181协议、GA/T 1400协议、部标JT808协议、RTMP、RTSP/Onvif协议、海康Ehome、海康SDK、大华SDK、华为SDK、宇视SDK、乐橙SDK、萤石云SDK等&#xff0c;并能对外分发RTMP、…...

Laravel包开发指南:构建可重用组件的艺术

标题&#xff1a;Laravel包开发指南&#xff1a;构建可重用组件的艺术 Laravel不仅是一个强大的Web应用框架&#xff0c;它的包&#xff08;Package&#xff09;系统也为开发者提供了构建和共享可重用组件的能力。通过包开发&#xff0c;开发者可以轻松地扩展Laravel的功能&am…...

JavaDS预备知识

集合框架 Java 集合框架 Java Collection Framework &#xff0c;又被称为容器 container &#xff0c;是定义在 java.util 包下的一组接口 interfaces和其实现类 classes 。 其主要表现为将多个元素 element 置于一个单元中&#xff0c;对数据进行创建(Create)、读取(Retrieve…...

日常学习--20240705

1、IO流 按照IO操作的数据类型分为字节流和字符流&#xff1a; 字节流&#xff1a;又分为输入流&#xff08;其他程序传递过来的数据&#xff0c;读取流中的数据&#xff09;和输出流&#xff08;往流中写数据&#xff0c;传递给其他程序&#xff09;;可以操作二进制文件&…...

Java中初始化一个List的多种方式

1.最原始的方式&#xff1a;先创建&#xff0c;然后再添加元素 List<String> list new ArrayList<>(); list.add("apple"); list.add("banana"); list.add("cherry");2.使用Arrays.asList 这是一种快速方便的方式&#xff0c;直接…...

BeikeShop多国语言多货币商城系统源码基于Laravel框架

BeikeShop是基于 Laravel 开发的一款开源商城系统&#xff0c;支持多语言商城 多货币商城 100%全开源 ChatGPT OpenAI B2C商城系统 H5商城 PHP商城系统 商城源码 PC商城 跨境电商系统 跨境商城系统 电商商城系统 Laravel 10 框架开发系统&#xff0c;支持插件市场。 Event 机制…...

gradle构建工具

setting.gradle // settings.gradle rootProject.name my-project // 指定根项目名称include subproject1, subproject2 // 指定子项目名称&#xff0c;可选jar包名称 方式一 jar {archiveBaseName my-application // 设置 JAR 文件的基本名称archiveVersion 1.0 // 设置…...

Java需要英语基础吗?

Java编程语言本身并不要求必须有很强的英语基础&#xff0c;因为Java的语法和逻辑是独立于任何特定语言的。我收集归类了一份嵌入式学习包&#xff0c;对于新手而言简直不要太棒&#xff0c;里面包括了新手各个时期的学习方向编程教学、问题视频讲解、毕设800套和语言类教学&am…...

14-36 剑和诗人10 - 用LLM构建 AI 代理平台

介绍 在当今快速发展的技术环境中&#xff0c;大型语言模型 (LLM) 和 AI 代理正在改变我们与信息交互、实现流程自动化以及应对不同行业复杂挑战的方式。随着这些强大的模型不断发展&#xff0c;对能够无缝集成和协调它们的强大平台的需求变得越来越重要。 让我们深入研究设计…...

如何在Java中实现批量数据处理

如何在Java中实现批量数据处理 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 1. 引言 在大数据时代&#xff0c;处理大量数据是许多应用程序中必不可少的需…...

项目部署_持续集成_Jenkins

1 今日内容介绍 1.1 什么是持续集成 持续集成&#xff08; Continuous integration &#xff0c; 简称 CI &#xff09;指的是&#xff0c;频繁地&#xff08;一天多次&#xff09;将代码集成到主干 持续集成的组成要素 一个自动构建过程&#xff0c; 从检出代码、 编译构建…...

如何选择TikTok菲律宾直播网络?

为了满足用户对于实时互动的需求&#xff0c;TikTok推出了直播功能&#xff0c;让用户能够与粉丝即时交流。本文将探讨如何选择适合的TikTok菲律宾直播网络&#xff0c;并分析OgLive是否是值得信赖的选择。 TikTok菲律宾直播网络面临的挑战 作为全球领先的短视频平台&#xff…...

Pseudo-Label : The Simple and Efficient Semi-Supervised Learning Method--论文笔记

论文笔记 资料 1.代码地址 https://github.com/iBelieveCJM/pseudo_label-pytorch 2.论文地址 3.数据集地址 论文摘要的翻译 本文提出了一种简单有效的深度神经网络半监督学习方法。基本上&#xff0c;所提出的网络是以有监督的方式同时使用标记数据和未标记数据来训练的…...

信息收集-arping

信息收集-arping 简介 arping 是一个用于发送 ARP 请求和接收 ARP 回复的工具。它通常用于检查网络中的 IP 地址是否被使用&#xff0c;或发现网络中的重复 IP 地址。arping 工具类似于 ping 命令&#xff0c;但它使用的是 ARP 协议而不是 ICMP 协议。在 Kali Linux 中&#…...

一文了解常见DNS问题

当企业的DNS出现故障时&#xff0c;为不影响企业的正常运行&#xff0c;团队需要能够快速确定问题的性质和范围。那么有哪些常见的DNS问题呢&#xff1f; 域名解析失败&#xff1a; 当您输入一个域名&#xff0c;但无法获取到与之对应的IP地址&#xff0c;导致无法访问相应的网…...

TCP/IP 网络协议族分层

TCP/IP协议族 TCP/IP不单是TCP和IP两个协议&#xff0c;TCP/IP实际上是一组协议&#xff0c;它包括上百个各种功能的协议&#xff0c;如&#xff1a;远程登录、文件传输和电子邮件等&#xff0c;当然&#xff0c;也包括TCP、IP协议 它将软件通信过程抽象化为四个抽象层&#…...

Qt:5.QWidget属性介绍(Enabled属性-控件可用性设置、geometry属性-控件位置/大小设置)

目录 一、 QWidget属性的介绍&#xff1a; 二、Enabled属性-控件可用性设置&#xff1a; 2.1Enabled属性的介绍&#xff1a; 2.2获取控件当前可用状态的api——isEnabled()&#xff1a; 2.3设置控件当前的可用状态的api—— setEnabled() &#xff1a; 2.4 实例&#xff…...

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

vscode(仍待补充)

写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh&#xff1f; debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...

STM32F4基本定时器使用和原理详解

STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?

论文网址&#xff1a;pdf 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

数据链路层的主要功能是什么

数据链路层&#xff08;OSI模型第2层&#xff09;的核心功能是在相邻网络节点&#xff08;如交换机、主机&#xff09;间提供可靠的数据帧传输服务&#xff0c;主要职责包括&#xff1a; &#x1f511; 核心功能详解&#xff1a; 帧封装与解封装 封装&#xff1a; 将网络层下发…...

【JavaSE】绘图与事件入门学习笔记

-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角&#xff0c;以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向&#xff0c;距离坐标原点x个像素;第二个是y坐标&#xff0c;表示当前位置为垂直方向&#xff0c;距离坐标原点y个像素。 坐标体系-像素 …...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)

推荐 github 项目:GeminiImageApp(图片生成方向&#xff0c;可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...

push [特殊字符] present

push &#x1f19a; present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中&#xff0c;push 和 present 是两种不同的视图控制器切换方式&#xff0c;它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

OCR MLLM Evaluation

为什么需要评测体系&#xff1f;——背景与矛盾 ​​ 能干的事&#xff1a;​​ 看清楚发票、身份证上的字&#xff08;准确率>90%&#xff09;&#xff0c;速度飞快&#xff08;眨眼间完成&#xff09;。​​干不了的事&#xff1a;​​ 碰到复杂表格&#xff08;合并单元…...