一、Zygote, 意为“受精卵”,Android系统中几乎所有的应用进程都是由Zygote进程孵化出来的,Java环境也是由Zygote创建起来的,它建立了我们app运行所需要的环境,是app的祖先,因此,分析它的启动以及内部逻辑显得非常有必要。
    *Android系统是基于Linux内核的,而在Linux系统中,所有的进程都是init进程的子孙进程,也就是说,所有的进程都是直接或者间接地由init进程fork出来的。Zygote进程也不例外,它是在系统启动的过程,由init进程创建的。在系统启动脚本system/core/rootdir/init.rc文件中,我们可以看到启动Zygote进程的脚本命令:

<span style="font-size:14px;">service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-serverclass mainsocket zygote stream 660 root system  # zygote需要一个套接字onrestart write /sys/android_power/request_state wake  # zygote重启的话,需要执行这个操作onrestart write /sys/power/state ononrestart restart mediaonrestart restart netd</span>

*上述脚本表示要启动一个进程,名称为zygote, 可执行文件为/system/bin/app_process,  --Xzygote /system/bin --zygote --start-system-server这些是传给zygote的参数,其余部分的作用见注释。

二、app_process对应的源码在frameworks/base/cmds/app_process目录下,其入口函数main在文件app_main.cpp中,接下来我们就从这个main函数入手来分析zygote的内部逻辑。

<span style="font-size:14px;">int main(int argc, char* const argv[])
{// These are global variables in ProcessState.cppmArgC = argc;mArgV = argv;mArgLen = 0;for (int i=0; i<argc; i++) {mArgLen += strlen(argv[i]) + 1;}mArgLen--;AppRuntime runtime;const char* argv0 = argv[0];// Process command line arguments// ignore argv[0]argc--;argv++;// Everything up to '--' or first non '-' arg goes to the vmint i = runtime.addVmArguments(argc, argv);// Parse runtime arguments.  Stop at first unrecognized option.bool zygote = false;bool startSystemServer = false;bool application = false;const char* parentDir = NULL;const char* niceName = NULL;const char* className = NULL;while (i < argc) {const char* arg = argv[i++];if (!parentDir) {parentDir = arg;} else if (strcmp(arg, "--zygote") == 0) {zygote = true;niceName = "zygote";} else if (strcmp(arg, "--start-system-server") == 0) {startSystemServer = true;} else if (strcmp(arg, "--application") == 0) {application = true;} else if (strncmp(arg, "--nice-name=", 12) == 0) {niceName = arg + 12;} else {className = arg;break;}}if (niceName && *niceName) {setArgv0(argv0, niceName);set_process_name(niceName);}runtime.mParentDir = parentDir;if (zygote) {runtime.start("com.android.internal.os.ZygoteInit",startSystemServer ? "start-system-server" : "");} else if (className) {// Remainder of args get passed to startup class main()runtime.mClassName = className;runtime.mArgC = argc - i;runtime.mArgV = argv + i;runtime.start("com.android.internal.os.RuntimeInit",application ? "application" : "tool");} else {fprintf(stderr, "Error: no class name or --zygote supplied.\n");app_usage();LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");return 10;}
}</span>

三、main函数主要就是创建了runtime实例,并且解析参数,然后调用runtime的start函数,接着我们分析AppRuntime的start函数
    这个函数定义在frameworks/base/core/jni/AndroidRuntime.cpp文件中:

<span style="font-size:14px;">// 首先明确下传进来的参数  className == "com.android.internal.os.ZygoteInit"  options == "start-system-server"
void AndroidRuntime::start(const char* className, const char* options)
{ALOGD("\n>>>>>> AndroidRuntime START %s <<<<<<\n",className != NULL ? className : "(unknown)");/** 'startSystemServer == true' means runtime is obsolete and not run from* init.rc anymore, so we print out the boot start event here.*/if (strcmp(options, "start-system-server") == 0) {/* track our progress through the boot sequence */const int LOG_BOOT_PROGRESS_START = 3000;LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START,ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));}const char* rootDir = getenv("ANDROID_ROOT");if (rootDir == NULL) {rootDir = "/system";if (!hasDir("/system")) {LOG_FATAL("No root directory specified, and /android does not exist.");return;}setenv("ANDROID_ROOT", rootDir, 1);  //配置ANDROID_ROOT环境变量}//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) != 0) { // 创建虚拟机return;}onVmCreated(env);/** Register android functions.*/if (startReg(env) < 0) {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;jstring optionsStr;stringClass = env->FindClass("java/lang/String");assert(stringClass != NULL);strArray = env->NewObjectArray(2, stringClass, NULL);assert(strArray != NULL);classNameStr = env->NewStringUTF(className);assert(classNameStr != NULL);env->SetObjectArrayElement(strArray, 0, classNameStr);optionsStr = env->NewStringUTF(options);env->SetObjectArrayElement(strArray, 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);jclass startClass = env->FindClass(slashClassName);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");if (startMeth == NULL) {ALOGE("JavaVM unable to find main() in '%s'\n", className);/* keep going */} else {/* 调用com.android.internal.os.ZygoteInit的main函数,strArray是参数,数组里面有两个元素,className == "com.android.internal.os.ZygoteInit"  options == "start-system-server" */env->CallStaticVoidMethod(startClass, startMeth, strArray);#if 0if (env->ExceptionCheck())threadExitUncaughtException(env);
#endif}}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");
}</span>

四、start函数主要做了以下几件事情:
    调用startVm函数创建虚拟机;
    调用startReg函数注册Android Natvie函数;
    让虚拟机去执行com.android.internal.os.ZygoteInit的main函数。
    接下来分析下com.android.internal.os.ZygoteInit的main函数,frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

<span style="font-size:14px;">public static void main(String argv[]) {try {// Start profiling the zygote initialization.SamplingProfilerIntegration.start();registerZygoteSocket();  // 1、创建一个套接字,用于监听ams发过来的fork请求EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,SystemClock.uptimeMillis());preload(); // 2、加载classes 和resources, 后面会详细分析EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,SystemClock.uptimeMillis());// Finish profiling the zygote initialization.SamplingProfilerIntegration.writeZygoteSnapshot();// Do an initial gc to clean up after startupgc();// If requested, start system server directly from Zygoteif (argv.length != 2) {throw new RuntimeException(argv[0] + USAGE_STRING);}if (argv[1].equals("start-system-server")) {startSystemServer(); //3、 创建system server进程,ams wms pms等常见service都在该进程里面} else if (!argv[1].equals("")) {throw new RuntimeException(argv[0] + USAGE_STRING);}Log.i(TAG, "Accepting command socket connections");if (ZYGOTE_FORK_MODE) {runForkMode();} else {runSelectLoopMode(); // 4、进入循环监听模式,监听外来请求}closeServerSocket();} catch (MethodAndArgsCaller caller) {caller.run();} catch (RuntimeException ex) {Log.e(TAG, "Zygote died with exception", ex);closeServerSocket();throw ex;}}</span>

五、com.android.internal.os.ZygoteInit的main函数主要做了四件事情:
    调用 registerZygoteSocket()创建一个套接字,用于监听ams发过来的fork请求;
    调用preload()预加载classes 和resources;
    调用startSystemServer()创建system server进程,ams wms pms等常见service都在该进程里面;
    调用runSelectLoopMode()进入循环监听模式,监听外来请求。

1、 registerZygoteSocket
    fork()创建子进程,父进程返回true。子进程执行RuntimeInit.zygoteInit。
    @frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

<span style="font-size:14px;">public class ZygoteInit {...private static LocalServerSocket sServerSocket;private static final String ANDROID_SOCKET_ENV = "ANDROID_SOCKET_zygote";private static void registerZygoteSocket() {if(sServerSocket == NULL) {int fileDesc;String env = System.getenv(ANDROID_SOCKET_ENV);     // 获取环境变量fileDesc = Integer.parseInt(env);sServerSocket = new LocalServerSocket(createFileDescriptor(fileDesc));}}
}
</span>

*这里通过System.getenv()获取环境变量ANDROID_SOCKET_ENV的值,通过这个文件描述符表示/dev/socket/zygote。那么这个环境变量又是谁设置的呢?执行系统启动脚本init.rc的init.c文件里面有个service_start()函数就是用来分析zygote并创建和设置相应socket的。

<span style="font-size:14px;">void service_start(struct service *svc, const char* dynamic_args)
{...pid = fork();if ( pid == 0) {struct socketinfo *si;...for (si = svc->sockets; si; si = si->next) {int socket_type = (!strcmp(si->type, "stream") ? SOCK_STREAM : (!strcmp(si->type, "dgram") ? SOCK_DGRAM : SOCK_SEQPACKET));int s = create_socket(si->name, socket_type, si->perm, si->uid, si->gid);publish_socket(si->name, s);}}...
}
#define ANDROID_SOCKET_ENV_PREFIX  "ANDROID_SOCKET_"
#define ANDROID_SOCKET_DIR "/dev/socket"
static void publish_socket(const char *name, int fd)
{char key[64] = ANDROID_SOCKET_ENV_PREFIX;char val[64];strlcpy(key + sizeof(ANDROID_SOCKET_ENV_PREFIX) -1, name, sizeof(key) - sizeof(ANDROID_SOCKET_ENV_PREFIX));snprintf(val, sizeof(val), "%d", fd);add_environment(key, val);   // ANDROID_SOCKET_zygote = valfcntl(fd, F_SETFD, 0);
}</span>

*这里每一个service命令都会使init进程调用fork函数创建一个新的进程来分析里面的socket选项,然后调用create_socket()函数在/dev/socket目录下创建一个设备文件,然后获得一个文件描述符并通过publish_socket()写入到环境变量中去。这样就把zygote这个socket的文件描述符写到ANDROID_SOCKET_zygote里面去了,这里创建socket文件描述符的create_socket()函数,最后通过调用execve(svc->args[0], (char**)arg_ptrs, (char**)ENV)去执行zygote的可执行程序,并将环境变量ENV参数传递过去了。所以ZygoteInit.registerZygoteSocket()函数就可以直接从环境变量中取出这个文件描述符来使用。如果其他进程需要打开这个/dev/socket/zygote 文件来和Zygote进程通信就需要通过文件名字连接LocalServerSocket。

2、startSystemServer
    @frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

<span style="font-size:14px;">private static boolean startSystemServer() {String args[] = {"--setuid=1000", "--setgid=1000", "--setgroups=1001,1002,1003,1004,1005,1006...","--capabilities=13010432,130104352","--runtime-init","--nice-name=system_server","com.android.server.SystemServer",};ZygoteConnection.Arguments parsedArgs = null;parsedArgs = new ZygoteConnection.Arguments(args);ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);int pid = Zygote.forkSystemServer(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,parsedArgs.debugFlags, null, parsedArgs.permittedCapabilities,parsedArgs.effectiveCapabilities));if (pid == 0) { // 子进程handleSystemServerProcess(parsedArgs);}return true;   // 父进程直接返回true
}</span>

*这里Zygote进程通过调用Zygote.forkSystemServer()函数创建一个新的进程来启动SystemServer组件。

<span style="font-size:14px;">public class ZygoteInit {....private static void handleSystemServerProcess(ZygoteConnection.Arguments parsedArgs) {closeServerSocket();   // 子进程不需要zygote套接字,直接关闭FileUtils.setUMask(FileUtils.S_IRWXG | FileUtils.S_IRWXO);...RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs);}
}
public class RuntimeInit {...public static final void zygoteInit(int targetSdkVersion, String[] argv) {redirectLogStreams();commonInit();zygoteInitNative();            // 是一个native函数,主要完成Binder通信机制的初始化applicationInit(targetSdkVersion, argv);}public static final native void zygoteInitNative();private static void applicationInit(int targetSdkVersion, String[] argv) {VMRuntime.getRuntime().setTargetHeapUtilizatoin(0.75f);VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);final Arguments args;args = new Arguments(argv);invokeStaticMain(args.startClass, args.startArgs);}// 这个 className = "com.android.server.SystemServer"private static void invokeStaticMain(String className, String[] argv) {Class<?> cl;cl = Class.forName(className);Method m;m = cl.getMethod("main", new Class[] {String[].class});...throw new ZygoteInit.MethodAndArgsCaller(m, argv);}
}</span>

*这个RuntimeInit.ZygoteInit()函数主要执行两个操作:一是调用zygoteInitNative来执行一个Binder进程间通信机制的初始化工作;二是调用com.android.server.SystemServer类的main函数。
    *zygoteInitNative: 
    @frameworks/base/core/jni/AndroidRuntime.cpp

<span style="font-size:14px;">static JNINativeMethod gMethods[] = { ....{ "zygoteInitNative", "()V", (void*) com_android_internal_os_RuntimeInit_zygoteInit },....};
static void com_android_internal_os_RuntimeInit_zygoteInit(JNIEnv* env, jobject clazz)
{gCurRuntime->onZygoteInit();        // 就是启动Binder通信
}
virtual void onZygoteInit()
{sp<ProcessState> proc = ProcessState::self();   // 每个进程一份ProcessState对象,打开Binder驱动if(proc->supportsProcess()){proc->startThreadPool(); // 启动一个线程用于Binder通信}
}</span>

*SystemServer.main :
    @frameworks/base/services/java/com/android/server/SystemServer.java

<span style="font-size:14px;">public class SystemServer {native public static void init1(Strig[] args);public static void main(String[] args) {....VMRuntime.getRuntime().setTargetHeapUtilization(0.8f);System.loadLibrary("android_servers");init1(args);}public static final void init2() {Thread thr = new ServerThread();thr.setName("android.server.ServerThread");thr.start();}
}</span>

*@frameworks/base/services/jni/com_android_server_SystemServer.cpp

<span style="font-size:14px;">static void android_server_SystemServer_init1(JNIEnv* env, jobject clazz)
{system_init();
}
status_t system_init()
{sp<ProcessState> proc(ProcessState::self());sp<IServiceManager> sm = defaultServiceManager();...AndroidRuntime* runtime = AndroidRuntime::getRuntime();JNIEnv* env = rungime->getJNIEnv();jclass clazz = env->FindClass("com/android/server/SystemServer");jmethodID methodId = env->GetStaticMethodID(clazz, "init2", "()V");env->CallStaticVoidMethod(clazz, methodId);ProcessState::self()->startThreadPool();IPCThreadState::self()->joinThreadPool();return NO_ERROR;
}</span>

*这个SystemServer的main()函数首先会调用JNI方法init1,init1()会调用SystemServer的init2,在init2里面创建一个ServerThread线程执行一些系统关键服务的启动操作。

<span style="font-size:14px;">class ServerThread extends Thread {...public void run() {Looper.prepare();// Critical services ...ServiceManager.addService(Context.POWER_SERVICE, new PoperManagerService());ActivityManagerService.main();PackageManagerService.main(context);...}
}</span>

*到现在为止,zygote已经fork()子进程完成了SystemServer组件的初始化启动操作,回到zygote.main()里面调用runSelectLoopMode()进入一个循环再前面创建的socket接口上等待ActivityManagerService请求创建新的应用程序。

3、runSelectLoopMode

<span style="font-size:14px;">public class ZygoteInit {....private static void runSelectLoopMode() throws MethodAndArgsCaller {ArrayList<FileDescriptor> fds = new ArrayList();ArrayList<ZygoteConnection> peers = new ArrayList();FileDescriptor[] fdArray = new FileDescriptor[4];fds.add(sServerSocket.getFileDescriptor());peers.add(NULL);int loopCount = GC_LOOP_COUNT;   // 10while(true) {fdArray = fds.toArray(fdArray);index = selectReadble(fdArray);      // 类似Linux下的select轮询,native 函数if(index < 0) {...  } else if(index == 0) {               // 表示有客户端连接上ZygoteConnection newPeer = acceptCommandPeer();peers.add(newPeer);fds.add(newPeer.getFileDescriptor());} else {                            // 客户端发送了请求过来,交给ZygoteConnection的runOnce()函数完成boolean done = peers.get(index).runOnce();if(done) {peers.remove(index);fds.remove(index);}}}}private static ZygoteConnection acceptCommandPeer() {...return new ZygoteConnection(sServerSocket.accept());}
}</span>

*这里while(true)循环里面调用selectReadble(),这是一个native函数,类似select使用多路复用I/O模型。当返回0的时候表示有客户端连接上,ZygoteConnection表示Zygote的一个客户端,每当客户端连接上的时候acceptCommandPeer()返回一个ZygoteConnection对象,ZygoteConnection里面保存着accpet()返回的LocalSocket对象。然后将相应的文件描述符添加到fds里面,下次再对其调用selelct轮询。当返回值大于0的时候表示有客户端发过来的数据请求,交给ZygoteConnection对象的runOnce()函数处理。
    *selectReadable()函数对应native函数为:com_android_internal_os_ZygoteInit_SelectReadable()

<span style="font-size:14px;">static jint com_android_internal_os_ZygoteInit_selectReadable (JNIEnv *env, jobject clazz, jobjectArray fds)
{if (fds == NULL) {jniThrowNullPointerException(env, "fds == null");return -1;}jsize length = env->GetArrayLength(fds);fd_set fdset;if (env->ExceptionOccurred() != NULL) {return -1;}FD_ZERO(&fdset);int nfds = 0;for (jsize i = 0; i < length; i++) {jobject fdObj = env->GetObjectArrayElement(fds, i);if  (env->ExceptionOccurred() != NULL) {return -1;}if (fdObj == NULL) {continue;}int fd = jniGetFDFromFileDescriptor(env, fdObj);if  (env->ExceptionOccurred() != NULL) {return -1;}FD_SET(fd, &fdset);if (fd >= nfds) {nfds = fd + 1;}}int err;do {err = select (nfds, &fdset, NULL, NULL, NULL);} while (err < 0 && errno == EINTR);if (err < 0) {jniThrowIOException(env, errno);return -1;}for (jsize i = 0; i < length; i++) {jobject fdObj = env->GetObjectArrayElement(fds, i);if  (env->ExceptionOccurred() != NULL) {return -1;}if (fdObj == NULL) {continue;}int fd = jniGetFDFromFileDescriptor(env, fdObj);if  (env->ExceptionOccurred() != NULL) {return -1;}if (FD_ISSET(fd, &fdset)) {return (jint)i;}}return -1;
}</span>

总结:
    *在android中SystemService的启动是在Zygote进程创建好后进行的,并且由Zygote进程建立好DVM运行环境,加载ZygoteInit的main函数,最终调用Zygote的本地方法forkSystemServer,并执行linux的fork方法创建SystemServer进程。
    *应用程序的进程也是由Zygote创建的,在ActivityManagerService中的startProcessLocked中调用了Process.start()方法。并通过连接调用Zygote的native方法forkAndSpecialize,执行fork任务。

*应用进程和服务进程位于不同的进程中,他们之间是通过IPC进行数据传递的。

安卓Zygote详解相关推荐

  1. 安卓Service 详解

     Service全部内容基本会在本篇涉及到,我们将围绕以下主要知识点进行分析: Service简单概述 Service在清单文件中的声明 Service启动服务实现方式及其详解 Service绑定服务 ...

  2. 安卓AsyncTack详解

    我们知道安卓中的UI线程不是线程安全的,即不能在UI线程中进行耗时操作,所以我们通常的做法是开启一个子线程来进行耗时操作,然后将处理后的结果运用Handler机制传递给UI线程,在UI线程中根据处理后 ...

  3. 安卓Activity详解

    进入安卓的第一步就是理解activity运行原理,我最开始的时候公司大佬指了一条明路,下面就来谈谈我对activity的理解吧,希望对你有帮助. onCreate:创建布局界面和初始化 onStart ...

  4. 安卓-Viewpage详解(1)——广告条

    今天老师讲解Viewpage,讲解的不是很好,因为总是不拿安卓其他的东西进行对比,只是拿着谷歌官方文档翻译,当然有个好处,基本使用我们还是会的. 自己思考,学习一个新的东西,我会拿旧东西,进行对比,然 ...

  5. 绿色小安卓机器人 详解Android发展历史

    机器人的开端 Android是一种以Linux为基础的开放源码操作系统,主要应用于便携设备.Android股份有限公司于2003年在美国加州成立,在2005年被Google收购.2010年末数据显示, ...

  6. 安卓 LayoutInflater详解

    导读 在实际开发中LayoutInflater这个类还是非常有用的,它的作用类似于findViewById(). 不同点是: LayoutInflater是用来找res/layout/下的xml布局文 ...

  7. 安卓behavior详解3--自定义behavior详解

    一.前言 官方定义: A Behavior implements one or more interactions that a user can take on a child view. Thes ...

  8. Sonic 开源移动端云真机测试平台 - 设备中心接入安卓设备实例演示,Agent端服务部署过程详解

    Sonic 开源移动端云真机测试平台 - Agent端服务部署与安卓设备接入演示 一加8手机连接效果图展示 第一章:环境准备 ① agent-sources 资源包下载 ② Android SDK安装 ...

  9. 安卓市场和安智市场_安卓市场小米市场ASO详解

    本文由鸟哥笔记春羽计划出品 本文2003字 今天我们要讲的是小米应用市场从0-1的获量的过程,小米市场如何在应用市场通过ASO获得大量的流量呢?下面有这几点. 量词分析 进行应用市场带量词分析,组建自 ...

最新文章

  1. kvm虚拟服务器备份,OpenStack KVM虚拟机实例磁盘的备份脚本
  2. Linux_PXE服务器_RHEL7
  3. python 图表_做自己漂亮的图表,一文学会如何用Python填充图表样式
  4. android 新特性笔记
  5. mysql数据库入门教程(4):查询讲解大全
  6. java的object有show_Java中 Object的方法
  7. 《去哪网编程题》filename extension
  8. [postgresql] postgresql 安装
  9. linux下Bash编程until语句及格式化硬盘分区等编写脚本(十)
  10. centos php安装redis扩展,Centos7编译安装redis、php安装phpredis扩展
  11. Hadoop学习总结(1)——大数据以及Hadoop相关概念介绍
  12. matlab-高数 meshgrid 笛卡尔乘积(直积)
  13. 最简单易懂的C语言代码实现最小二乘法线性拟合直线
  14. 所谓的AI芯片到底指什么?
  15. Python + Django 入门级开发手顺
  16. 【windows8开发】开发平台与开发框架
  17. 【Html】段落排版--行间距(行高)
  18. 为win10配置多个jdk的方法
  19. 央视棒打百度的三个现象
  20. HTML创建12列小屏幕网格,Bootstrap 网格系统布局详解

热门文章

  1. 编译PX4时,报错error ‘i‘ does not name a type __ULong i[2];解决方法
  2. 电机学Matlab仿真代码
  3. RFID固定资产管理系统之茶叶产品资产管理,智能监控防盗管理
  4. oracle grid infrastructure 安装,安装Oracle Grid Infrastructure for a Standalone Server
  5. 物联网目前的应用场景有哪些
  6. 双向可控硅在交流调压电路中的使用
  7. Oracle基础——第一章 Oracle简介
  8. anthony1314的数论小基础
  9. springboot找不到对象(自动注入失败)
  10. 【CSDN】【自用】CSDN操作