Android系统的启动流程(一):进入Zygote进程的初始化

news2025/1/11 18:16:14

Android系统的启动流程

概要

本篇文章主要是从Android系统启动电源开始介绍到程序运行到Java框架层并且完成Zygote进程的启动为止。下面先给出一张简单的概要图,本篇文章将会从源码进行分析Android的部分启动流程,这里的源码来自于AndroidCodeSearch,截止至2023年6月7日的最新代码。
在这里插入图片描述

引入init进程

很显然Android系统从上电开始也不是一下子就会跳入init进程的,与电脑一样,需要借助引导程序来进入,我们首先来了解Android系统启动流程的前几步:

  1. 启动电源以及系统启动:从ROM中加载引导程序BootLoader进RAM中运行。
  2. 引导程序BootLoader:引导程序BootLoader将系统OS拉起来运行。
  3. Linux内核启动:启动Linux内核,进行设置缓存等操作。完成内核设置后,它会在系统文件中寻找init.rc文件,然后启动init进程。
  4. init进程启动:init进程做的工作比较多,主要是初始化,启动属性服务,然后启动init进程。

init进程启动过程

什么是init进程

init进程是Android系统中用户空间的第一个进程,进程号为1,是Android系统启动流程中一个关键的步骤,作为第一个进程,他被赋予了许多重要的工作流程。init进程是由多个源文件共同组成的。

init进程的入口函数

我们可以进入镜像网站查看Android系统的源码,init目录下的所有文件就组成了init进程的整体,它的main.cpp文件就是它的入口函数:

using namespace android::init;

int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
#elif __has_feature(hwaddress_sanitizer)
    __hwasan_set_error_report_callback(AsanReportCallback);
#endif
    // Boost prio which will be restored later
    setpriority(PRIO_PROCESS, 0, -20);
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }

    if (argc > 1) {
        if (!strcmp(argv[1], "subcontext")) {
            android::base::InitLogging(argv, &android::base::KernelLogger);
            const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();

            return SubcontextMain(argc, argv, &function_map);
        }

        if (!strcmp(argv[1], "selinux_setup")) {
            return SetupSelinux(argv);
        }

        if (!strcmp(argv[1], "second_stage")) {
            return SecondStageMain(argc, argv);
        }
    }

    return FirstStageMain(argc, argv);
}

这里截取了部分代码,main函数里做的就是根据传入的参数来决定进入哪个初始化函数,最终会先调用FirstStageMain,然后调用SetupSelinux,最后是调用SecondStageMain函数,完成了这整个入口函数。

这里由于篇幅原因,我们就不再深究一些细节,总的来说,init的入口函数做了许多事情,我们关注最重要的几点:

  1. 在FirstStageMain中创建和挂载启动所需的文件目录,都是系统运行时目录,顾名思义就是只有在系统运行时才存在的目录,具体代码如下:

    ....
     // Clear the umask.
    umask(0);//清空mask,是为了赋予所有权限
    
    CHECKCALL(clearenv());
    CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
    CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
    CHECKCALL(mkdir("/dev/pts", 0755));//mkdir是创建文件夹的指令
    CHECKCALL(mkdir("/dev/socket", 0755));
    CHECKCALL(mkdir("/dev/dm-user", 0755));
    CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
    ....
    
  2. 在SecondStageMain中通过调用函数来初始化属性系统:

    ....
    PropertyInit();//初始化属性系统
    ....
    
  3. 接着继续在SecondStageMain设置信号处理函数,然后启动属性服务

    ...
    InstallSignalFdHandler(&epoll);//设置信号处理函数
    InstallInitNotifier(&epoll);
    StartPropertyService(&property_fd);//启动属性服务
    ...
    

    这里介绍一下什么是信号处理函数。信号处理函数主要是为了防止init进程的子进程成为僵尸进程,为了防止僵尸进程的出现,系统就会在子进程暂停和终止时发出SIGCHLD信号,而设置的信号处理函数就会接受这个信号。

  4. 然后SecondStageMain中会调用引导函数,解析init.rc配置文件

    ...
    ActionManager& am = ActionManager::GetInstance();
    ServiceList& sm = ServiceList::GetInstance();
    	
    LoadBootScripts(am, sm);//调用引导函数
    ... 
    

    可以看到这里调用了LoadBootScripts函数,实际上这个LoadBootScripts函数会解析init.rc文件:

    static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
    Parser parser = CreateParser(action_manager, service_list);
    
    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    if (bootscript.empty()) {
        parser.ParseConfig("/system/etc/init/hw/init.rc");//1
        if (!parser.ParseConfig("/system/etc/init")) {
            late_import_paths.emplace_back("/system/etc/init");
        }
        // late_import is available only in Q and earlier release. As we don't
        // have system_ext in those versions, skip late_import for system_ext.
        parser.ParseConfig("/system_ext/etc/init");
        if (!parser.ParseConfig("/vendor/etc/init")) {
            late_import_paths.emplace_back("/vendor/etc/init");
        }
        if (!parser.ParseConfig("/odm/etc/init")) {
            late_import_paths.emplace_back("/odm/etc/init");
        }
        if (!parser.ParseConfig("/product/etc/init")) {
            late_import_paths.emplace_back("/product/etc/init");
        }
    } else {
        parser.ParseConfig(bootscript);
    }
    

}
```
这里的注释一处就调用Parse进行了对.rc文件的解析,这个init.rc文件实际上就是AIL(Android Init Language)文件,是用来初始化Android系统的语言,我们会在后面看这个文件做了什么。

其实到这里为止,我们就已经看完了大部分init进程干的事情,接下来看它的解析init.rc文件的流程。

解析init.rc

这里我们打开system/core/rootdir/init.rc文件并截取一部分:

# It is recommended to put unnecessary data/ initialization from post-fs-data
# to start-zygote in device's init.rc to unblock zygote start.
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

on zygote-start && property:ro.crypto.state=unsupported
    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

on zygote-start && property:ro.crypto.state=encrypted && property:ro.crypto.type=file
    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

实际上对这个AIL语言,笔者也不是很了解,这里就只需要了解它大致干了什么就行,可以看到在这里频繁地出现了zygote-start 这个语句,大致就可以分析出这个init.rc文件中将会启动zygote进程,这也是Android系统中一个特殊的进程,所以我们可以理解为解析init.rc这个文件过程中就会启动Zygote进程。

总结init进程

上面就是init进程主要干了什么事情,在这里我们就可以先总结一下:

    1. 首先创建和挂载启动所需的文件目录
    1. 接着初始化和启动属性服务,设置信号处理函数
    1. 最后解析init.rc文件,这个过程中将会启动Zygote进程

什么是Zygote进程

在Android系统中,Zygote进程(Zygote process)是一个特殊的进程,它是应用程序的孵化器(orphanage)和模板进程(template process)。它在系统启动时被初始化,作为应用程序的起点,负责创建和管理其他应用程序进程。

Zygote进程的主要功能包括:

  1. 预加载类和资源:Zygote进程在启动过程中会预加载一些常用的类和资源,以加速应用程序的启动速度。这样,在创建新的应用程序进程时,可以通过复制Zygote进程的内存空间来避免重新加载这些类和资源,从而提高应用程序的启动性能。

  2. 创建应用程序进程:当系统接收到要运行一个新的应用程序的请求时,Zygote进程会作为模板进程,通过复制自身的内存空间来创建一个新的应用程序进程。这样,新的应用程序进程将继承Zygote进程的状态和资源,从而加速应用程序的启动过程。

  3. 分配应用程序的虚拟机(VM):每个应用程序进程都有自己的虚拟机实例。Zygote进程在创建应用程序进程时,会为该进程分配一个独立的虚拟机,以便应用程序可以在自己的虚拟机中执行代码。

  4. 处理系统共享库:Zygote进程会加载和管理系统共享库(shared library),以便应用程序可以共享这些库。这样,多个应用程序可以在内存中共享同一个系统库的实例,节省系统资源并提高运行效率。

通过使用Zygote进程,Android系统可以快速创建和启动应用程序进程,减少资源的重复加载和内存占用,提高应用程序的响应速度和性能。Zygote进程在系统启动时创建,并一直运行在后台,负责处理应用程序的孵化和管理工作。

Zygote进程的启动

Zygote的启动脚本

我们先来看Zygote进程的启动脚本,打开system/core/rootdir/init.zygote64.rc:

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    priority -20
    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
    # NOTE: If the wakelock name here is changed, then also
    # update it in SystemSuspend.cpp
    onrestart write /sys/power/wake_lock zygote_kwl
    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的service,启动路径为/system/bin/app_process64,也就是对应第一行最开始的内容,后面的其与内容都是对这个service进行修饰的内容;除此之外我们还可以发现启动的类名为main,也就是第二行的内容。

这个文件中定义的东西也是AIL,所以也是需要进行解析的,解析之后,实际上就会启动路径下的的文件,再说具体一点,实际上是FrameWorks目录下的文件:frameworks/base/cmds/app_process/app_main.cpp,我们再看这个文件的内容:

	....
    if (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.");
    }
    ....

这里依旧截取最重要的部分,在这里将会先判断当前是否运行在Zygote进程中,如果是,就会调用runtime.start函数,为什么要先判断呢?因为Zygote进程都是通过fork自身来创建子进程的,这样Zygote以及它的子进程都可以进入app_main.cpp的main函数,因此需要区分进程。

接下来我们看这个runtime的start函数做了什么。

AndroidRuntime的start方法

实际上这个start方法主要就是用来进行JNI调用的,即在C++中调用Java方法,我们马上来看这个方法:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
	.....

    //const char* kernelHack = getenv("LD_ASSUME_KERNEL");
    //ALOGD("Found LD_ASSUME_KERNEL='%s'\n", kernelHack);

    /* start the virtual machine */
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) { //1 启动虚拟机
        return;
    }
    onVmCreated(env); //2 创建虚拟机

    /*
     * Register android functions.
     */
    if (startReg(env) < 0) { //3 注册Java方法
        ALOGE("Unable to register all android natives\n");
        return;
    }

    /*
     * 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;

    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env->NewStringUTF(className);//4 获得类名
    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); //5 找到类名对应的类--ZygoteInit
    if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");//6 找到对应类的main方法
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
            /* keep going */
        } else {
            env->CallStaticVoidMethod(startClass, startMeth, strArray);//7 调用找到的main方法,从这里开始也进入了Java框架层

#if 0
            if (env->ExceptionCheck())
                threadExitUncaughtException(env);
#endif
        }
    }
	......
}

依旧是看重点,代码中我已经进行了一定的注释,总的来说之前提到的runtime.start(“com.android.internal.os.ZygoteInit”, args, zygote)干的事情就是:

  1. 启动JVM虚拟机
  2. 注册Java方法,用于JNI调用
  3. 找到对应的类名,也就是ZygoteInit
  4. 找到对应的类和main方法
  5. 调用这个找到的main方法

所以说,这么一套下来最后是会调用ZygoteInit这个Java类,实际上这也意味着启动正式进入到了Java层而不是之前的C++层

进入Java层-ZygoteInit的main方法

上面说到我们正式进入到了Java层了,接下来就来看ZygoteInit的main方法:

public static void main(String[] argv) {
        ....
        try {
           .....
            boolean startSystemServer = false;
            String zygoteSocketName = "zygote";
            String abiList = null;
            boolean enableLazyPreload = false;
            for (int i = 1; i < argv.length; i++) {
                if ("start-system-server".equals(argv[i])) {
                    startSystemServer = true;
                } else if ("--enable-lazy-preload".equals(argv[i])) {
                    enableLazyPreload = true;
                } else if (argv[i].startsWith(ABI_LIST_ARG)) {
                    abiList = argv[i].substring(ABI_LIST_ARG.length());
                } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
                    zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length());
                } else {
                    throw new RuntimeException("Unknown command line argument: " + argv[i]);
                }
            }

 			......
            Zygote.initNativeState(isPrimaryZygote);

            ZygoteHooks.stopZygoteNoThreadCreation();

            zygoteServer = new ZygoteServer(isPrimaryZygote);//1

            if (startSystemServer) {
                Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);//2


                if (r != null) {
                    r.run();
                    return;
                }
            }

            Log.i(TAG, "Accepting command socket connections");


            caller = zygoteServer.runSelectLoop(abiList);//3
        } catch (Throwable ex) {
            Log.e(TAG, "System zygote died with fatal exception", ex);
            throw ex;
        } finally {
            if (zygoteServer != null) {
                zygoteServer.closeServerSocket();
            }
        }
		.....
    }

这里我们还是看重点,主要就是上面标注出来的三处注释处,注释一处调用了:zygoteServer = new ZygoteServer(isPrimaryZygote),这个方法将会创建出一个zygoteServer,就是一个孵化器服务,同时构造方法内部会创建出一个Socket,这个显然是用来给其他进程调用的;注释二处的Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer)就是用来启动各种系统服务的;注释三处,zygoteServer孵化器服务就开始等待请求了,具体来说,是等待AMS(Activity Manger Server)的请求。

我们也对这个ZygoteInit的内容进行一些总结:

  1. 首先调用构造函数创建孵化器服务(Zygote)和端口
  2. 然后开启各种各样的系统服务
  3. 最后孵化器服务(zygoteServer)开始进行等待AMS的请求

总结从开机到进入Zygote进程的初始化部分

那么到这里为止,我们已经进入到了Java框架层的第一个方法了,现在我们开始从开机开始阶段性总结一下目前为止的流程:

  1. 首先开机上电,执行引导程序,完成Linux层的初始化,然后进入到了init流程
  2. init流程中将系统文件进行了创建和挂载,初始化和启动属性服务,设置信号处理函数,最后就开始解析init.rc文件
  3. 解析init.rc文件时将会最终调用到frameworks/base/cmds/app_process/app_main.cpp的main函数
  4. 在这个app_main中最终会调用到AndroidRuntime.start对应的ZygoteInit的main方法,而实际上这个AndroidRuntime.start方法是JNI调用,它将会启动JVM并且注册Java方法,最后调用Java方法,在这里也就是ZygoteInit的main方法,完成了从C++层进入Java层
  5. ZygoteInit的main方法中创建并启动了zygote服务和其他的系统服务,最后zygote服务会等待AMS的请求

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

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

相关文章

基于RK3588的人工智能边缘计算大算力网关

智能运维系统从下至上分为终端层、边缘层、平台层和应用层&#xff0c;如图 1 所示。终端层 是整个系统的神经末梢&#xff0c;负责现场数据的采集&#xff0c;除摄像机外&#xff0c;还包括各类传感器、控制器 等物联网设备。边缘层汇总各个现场终端送来的非结构化视频数据和…

BERT(Transformer Encoder)详解和TensorFlow实现(附源码)

文章目录 一、BERT简介1. 模型2. 训练2.1 Masked Language Model2.2 Next Sentence Prediction2.3 BERT的输出 3. 微调 二、源码1. 加载BERT模型2. 加载预处理模型3. 加载BERT4. 构建BERT微调模型5. 训练6. 推理 一、BERT简介 1. 模型 BERT的全称为Bidirectional Encoder Repr…

java.time 时区详解

from: https://blog.zhjh.top/archives/MFTOJ-jorm4ISK9KXEYFE LocalDateTime 类是不包含时区信息的&#xff0c;可以通过 atZone 方法来设置 ZoneId&#xff0c;返回 ZonedDateTime 类实例&#xff0c;通过 atOffset 方法来设置 ZoneOffset&#xff0c;返回 OffsetDateTime 类…

攻防世界-web-supersqli

1. 题目描述&#xff1a; 2. 思路分析 这里尝试按照基本思路进行验证&#xff0c;先确定注入点&#xff0c;然后通过union查询依次确认数据库名&#xff0c;表名&#xff0c;字段名&#xff0c;最终获取到我们想要的字段信息。 这里只有一个输入框&#xff0c;所以注入点肯定…

【犀牛书】JavaScript 类型、值、变量章节读书笔记

本文为对《JavaScript权威指南》第三章&#xff1a;类型、值、变量精读的读书笔记&#xff0c;对重点进行了记录以及在一些地方添加了自己的理解。 JavaScript类型可以分为两类&#xff1a;原始类型和对象类型。Javascript的原始类型包括数值、文本字符串&#xff08;也称字符串…

驱动操作控制LED灯

控制LED灯&#xff1a; 驱动如何操作寄存器 rgb_led灯的寄存器是物理地址&#xff0c;在linux内核启动之后&#xff0c; 在使用地址的时候&#xff0c;操作的全是虚拟地址。需要将物理地址 转化为虚拟地址。在驱动代码中操作的虚拟地址就相当于 操作实际的物理地址。 物理地址&…

2023年5月榜单丨飞瓜数据B站UP主排行榜(哔哩哔哩)发布!

飞瓜轻数发布2023年5月飞瓜数据UP主排行榜&#xff08;B站平台&#xff09;&#xff0c;通过充电数、涨粉数、成长指数三个维度来体现UP主账号成长的情况&#xff0c;为用户提供B站号综合价值的数据参考&#xff0c;根据UP主成长情况用户能够快速找到运营能力强的B站UP主。 飞…

Git—版本管理工具

作用&#xff1a;分布式版本控制 一句话&#xff1a;在开发的过程中用于管理对文件、目录或工程等内容的修改历史&#xff0c;方便查看历史记录&#xff0c;备份以便恢复以前的版本的软件工程技术 官网下载安装&#xff1a;https://git-scm.com/ 命令大全&#xff1a;https://g…

OceanBase 4.1 全面测评及部署流程,看这篇就够了【建议收藏】

背景 测试 OceanBase 对比 MySQL&#xff0c;TiDB 的性能表现&#xff0c;数据存储压缩&#xff0c;探索多点内部项目一个数据库场景落地 Oceanbase&#xff08;MySQL->OceanBase&#xff09;。 单机测试 准备 OBD 方式部署单机 文件准备 wget https://obbusiness-pri…

Bilinear CNN:细粒度图像分类网络,对Bilinear CNN中矩阵外积的解释。

文章目录 一、Bilinear CNN 的网络结构二、矩阵外积&#xff08;outer product&#xff09;2.1 外积的计算方式2.2 外积的作用 三、PyTorch 网络代码实现 细粒度图像分类&#xff08;fine-grained image recognition&#xff09;的目的是区分类别的子类&#xff0c;如判别一只狗…

【web自动化测试】Web网页测试针对性的流程解析

前言 测试行业现在70%是以手工测试为主&#xff0c;那么只有20%是自动化测试&#xff0c;剩下的10%是性能测试。 有人可能会说&#xff0c;我现在做手工&#xff0c;我为什么要学自动化呢&#xff1f;我去学性能更好性能的人更少&#xff1f; 其实&#xff0c;性能的要求比自动…

蓝桥杯2022年第十三届决赛真题-齿轮

题目描述 这天&#xff0c;小明在组装齿轮。 他一共有 n 个齿轮&#xff0c;第 i 个齿轮的半径为 ri&#xff0c;他需要把这 n 个齿轮按一定顺序从左到右组装起来&#xff0c;这样最左边的齿轮转起来之后&#xff0c;可以传递到最右边的齿轮&#xff0c;并且这些齿轮能够起到提…

小程序容器与PWA是一回事吗?

PWA代表“渐进式网络应用”&#xff08;Progressive Web Application&#xff09;。它是一种结合了网页和移动应用程序功能的技术概念。PWA旨在提供类似于原生应用程序的用户体验&#xff0c;包括离线访问、推送通知、后台同步等功能&#xff0c;同时又具有网页的优势&#xff…

软件验收测试该怎么进行?权威的软件检测机构应该具备哪些资质?

软件测试是软件开发周期中非常重要的一个环节。软件测试的目的是发现软件在不同环境下的各种问题&#xff0c;保证软件在发布前能够达到用户的要求。软件验收测试是软件测试的最后一个环节&#xff0c;该环节主要验证软件是否满足用户需求。那么对于软件验收测试&#xff0c;该…

分布式事务二 Seata使用及其原理剖析

一 Seata 是什么 Seata 介绍 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本…

【Spring源码】Spring源码导入Idea

1.基础环境准备 相关软件、依赖的版本号 Spring源码版本 5.3.x软件 ideaIU-2021.1.2.exeGradle gradle-7.2-bin.zip https://services.gradle.org/distributions/gradle-7.2-bin.zip - 网上说要单独下载gradle并配置环境变量&#xff0c;亲测当前5.3.X版本通过gradlew的方式进…

虚函数详解及应用场景

目录 概述1. 虚函数概述2. 虚函数的声明与重写3. 析构函数与虚函数的关系4. 虚函数的应用场景4.1. 多态性4.2. 接口定义与实现分离4.3. 运行时类型识别4.4. 多级继承与虚函数覆盖 结论 概述 虚函数是C中一种实现多态性的重要机制&#xff0c;它允许在基类中声明一个函数为虚函…

PDCCH monitoring capability

欢迎关注同名微信公众号“modem协议笔记”。 前段时间看search space set group (SSSG) switching相关内容时&#xff0c;注意到R17和R16的描述由于PDCCH monitoring capability的变化&#xff0c;内容有些不一样。于是就顺带看了下R16 R17PDCCH monitoring capability的内容。…

Domino 14.0早期测试版本

大家好&#xff0c;才是真的好。 本篇是超级图片篇&#xff0c;图片多&#xff0c;内容丰富&#xff0c;流量党请勿手残。 前天我们说到Engageug2023正在如火如荼进行&#xff0c;主题是“The Future is Now”。 因为时差的关系&#xff0c;实际上在写这篇公众号时&#xff…

设计模式(七):结构型之适配器模式

设计模式系列文章 设计模式(一)&#xff1a;创建型之单例模式 设计模式(二、三)&#xff1a;创建型之工厂方法和抽象工厂模式 设计模式(四)&#xff1a;创建型之原型模式 设计模式(五)&#xff1a;创建型之建造者模式 设计模式(六)&#xff1a;结构型之代理模式 设计模式…