Android zygote访谈录

news2024/11/24 18:26:30

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

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

本文摘要

本文以访谈的方式来带大家了解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 main
    priority -20
    //user是root级别的
    user root
    group root readproc reserved_disk
    socket zygote stream 660 root system
    socket usap_pool_primary stream 660 root system
    
    //在重启的时候会重新启动下面这些服务
    onrestart exec_background - system system -- /system/bin/vdc volume abort_fuse
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart media.tuner
    onrestart restart netd
    onrestart restart wificond
    task_profiles ProcessCapacityHigh MaxPerformance
    critical 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-preload
    class main
    priority -20
    user root
    group root readproc reserved_disk
    socket zygote_secondary stream 660 root system
    socket usap_pool_secondary stream 660 root system
    onrestart restart zygote
    task_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=unencrypted
    wait_for_prop odsign.verification.done 1
    # A/B update verifier that marks a successful boot.
    exec_start update_verifier_nonencrypted
    start statsd
    start netd
    start zygote
    start 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值为true
    if (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.h

struct _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.h

struct _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];
    #else
    char* buffer = NULL;
    #endif
    //获取library_name,这时候它的值是 libart.so
    library_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的相关方法赋值给instance
  instance->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.cpp
  
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为true
        return;
    }
                                          
    省略代码......
}
                                          
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创建JVM
    if (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为true
        return;
    }
         
     /*
     * 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.h

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

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

  
public class ZygoteInit{
    省略代码......
  
    private static native void nativeZygoteInit();
  
    省略代码......
}

//文件路径:core/jni/AndroidRuntime.cpp

const 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 NDEBUG
            ALOGD("----------!!! %s failed to load\n", array[i].mName);
#endif
            return -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的class
    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    
    //className就是 com.android.internal.os.ZygoteInit
    classNameStr = 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 0
            if (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-v8a
                    abiList = argv[i].substring(ABI_LIST_ARG.length());
                } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
                    //app_process64为zygote, app_process32为zygote_secondary
                    zygoteSocketName = 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进程,该值为false
            if (!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(); // BeginPreload
        bootTimingsTraceLog.traceBegin("PreloadClasses");
        //虚拟机预加载各种类
        preloadClasses();
        bootTimingsTraceLog.traceEnd(); // PreloadClasses
        bootTimingsTraceLog.traceBegin("CacheNonBootClasspathClassLoaders");
        cacheNonBootClasspathClassLoaders();
        bootTimingsTraceLog.traceEnd(); // CacheNonBootClasspathClassLoaders
        bootTimingsTraceLog.traceBegin("PreloadResources");
        //预加载Resource
        preloadResources();
        bootTimingsTraceLog.traceEnd(); // PreloadResources
        Trace.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);
        //预加载共享lib
        preloadSharedLibraries();
        //预加载字体资源
        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.coloradmin.cn/o/1893268.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于一些数据资源入表事项

一、入表条件&#xff1a; 2024年1月1日《企业数据资源相关会计处理暂行规定》开始执行&#xff0c;以上简称《企业会计准则》&#xff0c;它将资产定义为“企业过去的交易或者事项形成的、由企业拥有或者控制的、预期会给企业带来经济利益的资源”。需要说明的是&#xff0c;…

3D问界—法线空间与应用举例

问题提出&#xff1a;什么是法线空间&#xff0c;有哪些应用的场景&#xff1f; 法线空间&#xff08;Normal Space&#xff09;在计算机图形学中是一个重要的概念&#xff0c;特别是在处理表面法线&#xff08;Normal&#xff09;时常被提及。 1. 法线空间的分类 法线空间是相…

nanodiffusion代码逐行理解之Attention

目录 一、注意力中的QKV二、注意力中的位置嵌入三、注意力中的多头四、注意力和自注意力五、注意力中的encode和decoder 一、注意力中的QKV 简单来说&#xff1a; Q: 要查询的信息 K: 一个索引&#xff0c;要查询的向量 V: 我们查询得到的值 复杂一点的解释&#xff1a; Query…

Hadoop的namenode启动不起来

1、 排查原因 Initialization failed for Block pool (Datanode Uuid a5d441af-d074-4758-a3ff-e1563b709267) service to node1/192.168.88.101:8020. Exiting. java.io.IOException: Incompatible clusterIDs in /data/dn: namenode clusterID CID-674c5515-3fe1-4a9c-881d…

Winform中使用HttpClient实现调用http的post接口并设置传参content-type为application/json示例

场景 Winform中怎样使用HttpClient调用http的get和post接口并将接口返回json数据解析为实体类&#xff1a; Winform中怎样使用HttpClient调用http的get和post接口并将接口返回json数据解析为实体类_winform解析json-CSDN博客 上面使用HttpClient调用post接口时使用的HttpCon…

一文搞懂MySsql的Buffer Pool

Buffer Pool是什么 Buffer Pool是MySQL数据库中一个非常关键的组件。数据库中的数据最终都是存放在磁盘文件上的。但是在对数据库执行增删改查操作时&#xff0c;不可能直接更新磁盘上的数据。因为如果直接对磁盘进行随机读写操作&#xff0c;那速度是相当的慢的。随便一个大磁…

技术驱动:探索SpringBoot的大文件上传策略

1.分片上传技术 为了处理大文件上传并保证性能&#xff0c;前后端可以使用分片上传&#xff08;也称为分块上传&#xff09;技术。 1.选择原因 分片上传&#xff08;也称为分块上传&#xff09;是一种处理大文件上传的技术&#xff0c;主要目的是提高上传的可靠性和效率。 网…

Canvas合集更更更之实现由画布中心向外随机不断发散的粒子效果

实现效果 1.支持颜色设置 2.支持粒子数量设置 3.支持粒子大小设置 写在最后&#x1f352; 源码&#xff0c;关注&#x1f365;苏苏的bug&#xff0c;&#x1f361;苏苏的github&#xff0c;&#x1f36a;苏苏的码云

Python脚本:将Word文档转换为Excel文件

引言 在文档处理中&#xff0c;我们经常需要将Word文档中的内容转换成其他格式&#xff0c;如Excel&#xff0c;以便更好地进行数据分析和报告。针对这一需求&#xff0c;我编写了一个Python脚本&#xff0c;能够批量处理指定目录下的Word文档&#xff0c;将其内容结构化并转换…

从零开始使用WordPress搭建个人网站并一键发布公网详细教程

文章目录 前言1. 搭建网站&#xff1a;安装WordPress2. 搭建网站&#xff1a;创建WordPress数据库3. 搭建网站&#xff1a;安装相对URL插件4. 搭建网站&#xff1a;内网穿透发布网站4.1 命令行方式&#xff1a;4.2. 配置wordpress公网地址 5. 固定WordPress公网地址5.1. 固定地…

零知识学习之DPDK与RDMA(3)—— 认识DPDK(3)

接前一篇文章&#xff1a;零知识学习之DPDK与RDMA&#xff08;2&#xff09;—— 认识DPDK&#xff08;2&#xff09; 本文内容参考&#xff1a; 《Linux高性能网络详解 从DPDK、RDMA到XDP》 刘伟著 人民邮电出版社 https://blog.51cto.com/u_15301988/5181201 特此致谢&…

vite-ts-cesium项目集成mars3d修改相关的包和配置参考

如果vite技术栈下使用原生cesium&#xff0c;请参考下面文件的包和配置修改&#xff0c;想用原生创建的viewer结合我们mars3d的功能的话。 1. package.json文件 "dependencies": {"cesium": "^1.103.0","mars3d": "^3.7.18&quo…

2024微信小程序期末大作业-点奶茶微信小程序(后端nodejs-server)(附下载链接)_微信小程序期末大作业百度网盘下载

菜单展示 购物车展示&#xff1a; 提交订单&#xff1a; 支付详情页展示&#xff1a; 订单查看&#xff1a; 查看历史消费&#xff1a; 部分代码展示&#xff1a; <!--pages/home/home.wxml--> <block wx:for"{{listData}}" wx:key"itemlist&qu…

redis学习(002 安装redis和客户端)

黑马程序员Redis入门到实战教程&#xff0c;深度透析redis底层原理redis分布式锁企业解决方案黑马点评实战项目 总时长 42:48:00 共175P 此文章包含第5p-第p7的内容 文章目录 安装redis启动启动方式1&#xff1a;可执行文件启动启动方式2 基于配置文件启动修改redis配置文件 …

Python使用isinstance和issubclass,类型检查不求人!

目录 1、isinstance基础用法 &#x1f575;️ 1.1 isinstance概念简介 1.2 检查对象类型 1.3 类型与继承结构 1.4 实战&#xff1a;类型判断提升代码灵活性 2、issubclass深入理解 &#x1f50e; 2.1 issubclass概念解析 2.2 判断类的继承关系 2.3 高级应用&#xff1a…

【重磅】“一招”解决智能算法中不满足“预期”的问题【以微电网优化调度为例】

1主要内容 之前完整复现了微电网优化调度的模型&#xff0c;具体链接为&#xff1a;【完全复现】基于改进粒子群算法的微电网多目标优化调度&#xff0c;这是一个并不复杂的模型&#xff0c;甚至不借助智能算法&#xff0c;我们也能大致分析出电网、柴油发电机、微型燃气轮机等…

【Leetcode 566】【Easy】重塑矩阵

目录 题目描述 整体思路 具体代码 题目描述&#xff1a; 原题链接 整体思路 首先要确保重塑后的矩阵内元素个数和原矩阵元素个数要相同&#xff0c;如果不同则原样返回原矩阵。 按行遍历顺序遍历原矩阵&#xff0c;设一个临时vector<int>存放新矩阵的每一行的元素…

【抽代复习笔记】26-群(二十):子群的定义以及第一、第二判定定理

子群 定义1&#xff1a;(G,o)是一个群&#xff0c;H是G的非空子集&#xff0c;若H关于G的乘法o也能作成群&#xff08;满足群的判定定理&#xff1a;封闭性、结合律、单位元、逆元&#xff09;&#xff0c;则称H为G的子群&#xff0c;记作H ≤ G&#xff1b;若H是G的真子集&am…

致远OA同步组织架构到企业微信

致远OA同步组织架构到企业微信 可适配任何系统 背景 原有的微协同无法满足人员同步&#xff0c;因为在启用微协同的时候&#xff0c;企业微信已经存在人员&#xff0c;所以配置微协同之后&#xff0c;人员会出现新增而不会同步修改 方案 重写同步&#xff0c;针对已经存在…

单点登录(cookie+Redis)

1、什么是单点登录&#xff1f; Single Sign On简称SSo&#xff0c;只需要登录一次就可以在整个系统实现访问。 因为session的特性&#xff0c;是没有办法在多个服务系统之间实现数据的共享。 解决一个分布式session的问题。目前我们使用redis来实现分布式session。 1.1、新问题…