文章目录

  • 深入理解 AndroidFramework 之 Zygote 启动
    • 1. Init 进程
    • 2. init.rc 启动zygote 服务
    • 3. Zygote 进程的入口函数 —— main
      • 3.1 Zygote的可执行库文件
      • 3.2 main
    • 4. Zygote 的第二个门户 —— ZygoteInit
      • 4.1 Zygote的 Mark 阶段
      • 4.2 SystemServer的创建
    • 5. 总结

深入理解 AndroidFramework 之 Zygote 启动

Zygote 作为Android 第一个出道的进程被广大网友所熟知,那么该进程是如何被加载,如何运行的呢?在这里我们就尝试将她神秘的面纱一层层揭开。

1. Init 进程

Linux中PID为0的进程是所有其他进程的祖先, 也称作idle进程或swapper进程,在系统初始化时由kernel自身从无到有创建。进程0的数据成员大部分是静态定义的。

在Android系统中 0号进程会孵化出2个核心进程,一个进程号为2的名为kthreadd的进程,另一个则是进程号为1名为init的进程。kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间, 负责所有内核线程的调度和管理,所有的内核线程都是直接或者间接的以kthreadd为父进程。Init 进程是负责解析并执行 rc 文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V2VfFhGP-1603424070132)(res/picture/ps.png)]

2. init.rc 启动zygote 服务

Zygote 进程是init 进程通过解析init.zygote64.rc 启动的(如果是32位系统就会解析init.zygote32.rc),接下来我们就来捋一捋zygote的前世今生。

  • 涉及的文件:
  1. system/core/rootdir/init.zygote64.rc
  2. frameworks/base/cmds/app_process/Android.bp
  3. frameworks/base/cmds/app_process/app_main.cpp
  4. frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
  • init.zygote64.rc 文件内容如下:
//system/core/rootdir/init.zygote64.rcservice zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-serverclass mainpriority -20user rootgroup root readproc reserved_disksocket zygote stream 660 root system \\创建socketsocket usap_pool_primary stream 660 root system \\创建socketonrestart write /sys/power/state ononrestart restart audioserveronrestart restart cameraserveronrestart restart mediaonrestart restart netdonrestart restart wificondwritepid /dev/cpuset/foreground/tasks

服务名称:zygote

服务路径名:/system/bin/app_process64

启动参数: ‘-Xzygote’、’/system/bin’、’–zygote’、’–start-system-server’

关于 rc 脚步具体可以参照 system/core/init/README.md

3. Zygote 进程的入口函数 —— main

3.1 Zygote的可执行库文件

从 zygot 的rc文件中可以看出,它的执行路径是“/system/bin/app_process64”,执行的库文件是“app_process64”, 我们通过查找编译文件找到了该执行库文件的Android.bp或Android.mk 所在。

代码路径 frameworks/base/cmds/app_process/Android.bp

Aosp Q code:
cc_binary {name: "app_process",srcs: ["app_main.cpp"],multilib: {lib32: {version_script: ":art_sigchain_version_script32.txt",suffix: "32",//编译32位库},lib64: {version_script: ":art_sigchain_version_script64.txt",suffix: "64",//编译64位库},},Aosp P code:LOCAL_MODULE:= app_process
LOCAL_MULTILIB := both //32和64位库都会编译
LOCAL_MODULE_STEM_32 := app_process32 //编译32位库时命名
LOCAL_MODULE_STEM_64 := app_process64 //编译64位库时命名

3.2 main

main 函数的工作内容可以概括为2步:

  1. 解析执行命令参数。
  2. AndroidRuntime 启动进程。

在第二步中会通过jni方式执行 AndroidRuntime.cpp 中的 start 方法的第二个参数(“com.android.internal.os.ZygoteInit”)的main方法。

app_process64可执行库文件的 main 源代码如下:

int main(int argc, char* const argv[])
{...AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));// Process command line arguments// ignore argv[0]argc--;argv++;// Everything up to '--' or first non '-' arg goes to the vm.//// The first argument after the VM args is the "parent dir", which// is currently unused.//// After the parent dir, we expect one or more the following internal// arguments ://// --zygote : Start in zygote mode// --start-system-server : Start the system server.// --application : Start in application (stand alone, non zygote) mode.// --nice-name : The nice name for this process.//// For non zygote starts, these arguments will be followed by// the main class name. All remaining arguments are passed to// the main method of this class.//// For zygote starts, all remaining arguments are passed to the zygote.// main function.//// Note that we must copy argument string values since we will rewrite the// entire argument block when we apply the nice name to argv0.//// As an exception to the above rule, anything in "spaced commands"// goes to the vm even though it has a space in it.const char* spaced_commands[] = { "-cp", "-classpath" };// Allow "spaced commands" to be succeeded by exactly 1 argument (regardless of -s).bool known_command = false;int i;for (i = 0; i < argc; i++) {if (known_command == true) {runtime.addOption(strdup(argv[i]));// The static analyzer gets upset that we don't ever free the above// string. Since the allocation is from main, leaking it doesn't seem// problematic. NOLINTNEXTLINEALOGV("app_process main add known option '%s'", argv[i]);known_command = false;continue;}for (int j = 0;j < static_cast<int>(sizeof(spaced_commands) / sizeof(spaced_commands[0]));++j) {if (strcmp(argv[i], spaced_commands[j]) == 0) {known_command = true;ALOGV("app_process main found known command '%s'", argv[i]);}}if (argv[i][0] != '-') {break;}if (argv[i][1] == '-' && argv[i][2] == 0) {++i; // Skip --.break;}runtime.addOption(strdup(argv[i]));// The static analyzer gets upset that we don't ever free the above// string. Since the allocation is from main, leaking it doesn't seem// problematic. NOLINTNEXTLINEALOGV("app_process main add option '%s'", argv[i]);}// Parse runtime arguments.  Stop at first unrecognized option.bool zygote = false;bool startSystemServer = false;bool application = false;String8 niceName;String8 className;++i;  // Skip unused "parent dir" argument.while (i < argc) {const char* arg = argv[i++];if (strcmp(arg, "--zygote") == 0) {zygote = true;niceName = ZYGOTE_NICE_NAME;} 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.setTo(arg + 12);} else if (strncmp(arg, "--", 2) != 0) {className.setTo(arg);break;} else {--i;break;}}Vector<String8> args;if (!className.isEmpty()) {// We're not in zygote mode, the only argument we need to pass// to RuntimeInit is the application argument.//// The Remainder of args get passed to startup class main(). Make// copies of them before we overwrite them with the process name.args.add(application ? String8("application") : String8("tool"));runtime.setClassNameAndArgs(className, argc - i, argv + i);if (!LOG_NDEBUG) {String8 restOfArgs;char* const* argv_new = argv + i;int argc_new = argc - i;for (int k = 0; k < argc_new; ++k) {restOfArgs.append("\"");restOfArgs.append(argv_new[k]);restOfArgs.append("\" ");}ALOGV("Class name = %s, args = %s", className.string(), restOfArgs.string());}} else {// We're in zygote mode.maybeCreateDalvikCache();if (startSystemServer) {args.add(String8("start-system-server"));}char prop[PROP_VALUE_MAX];if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",ABI_LIST_PROPERTY);return 11;}String8 abiFlag("--abi-list=");abiFlag.append(prop);args.add(abiFlag);// In zygote mode, pass all remaining arguments to the zygote// main() method.for (; i < argc; ++i) {args.add(String8(argv[i]));}}if (!niceName.isEmpty()) {runtime.setArgv0(niceName.string(), true /* setProcName */);}if (zygote) {// frameworks/base/core/jni/AndroidRuntime.cpp start()runtime.start("com.android.internal.os.ZygoteInit", args, zygote);//注意这里是去启动 ZygoteInit的main 函数} else if (className) {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.");}
}> Note: runtime 的 start方法里会去调用startReg()方法去动态注册Framework里的JNI函数。

4. Zygote 的第二个门户 —— ZygoteInit

根据 main 函数的执行过程,可以将main函数分为两部分:

  • zygote 启动的 Mark 阶段。
  • systemserver 的创建。

4.1 Zygote的 Mark 阶段

这里总结 Mark 阶段总共六个步骤:

  1. 通过 ZygoteHooks 的 startZygoteNoThreadCreation 与虚拟机建立连接,标记zygote启动。
  2. 通过Os.setpgid(0, 0)将Zygote加入自己的进程组,第一个0表示当前进程。
  3. Zygote Init 之前的准备工作,如激活Ddms RuntimeInit.preForkInit();
  4. 解析main 函数的参数。
  5. 通过gcAndFinalize()进行一次gc处理
  6. Zygote 的 Native 层的初始化——Zygote.initNativeState(isPrimaryZygote)。

在初始化 zygote 的 Native 状态时,首先会去查找环境变量"ANDROID_SOCKET_zygote",并将找到的值通过atoi()转换成int类型的文件描述符,如果该环境变量为空,就会通过initUnsolSocketToSystemServer()函数去创建一个socket。最后就是检测selinux 权限并初始化selinux 上下文。

//文件路径: frameworks/base/core/java/com/android/internal/os/Zygote.java/*** Initialize the native state of the Zygote.  This inclues*   - Fetching socket FDs from the environment*   - Initializing security properties*   - Unmounting storage as appropriate*   - Loading necessary performance profile information** @param isPrimary  True if this is the zygote process, false if it is zygote_secondary*/static void initNativeState(boolean isPrimary) {nativeInitNativeState(isPrimary);}

//文件路径: frameworks/base/core/jni/com_android_internal_os_Zygote.cpp/*** The prefix string for environmental variables storing socket FDs created by* init.*/static constexpr std::string_view ANDROID_SOCKET_PREFIX("ANDROID_SOCKET_");static void com_android_internal_os_Zygote_nativeInitNativeState(JNIEnv* env, jclass,jboolean is_prima) {/** Obtain file descriptors created by init from the environment.*/std::string android_socket_prefix(ANDROID_SOCKET_PREFIX);std::string env_var_name = android_socket_prefix + (is_primary ? "zygote" : "zygote_secondary");char* env_var_val = getenv(env_var_name.c_str());if (env_var_val != nullptr) {gZygoteSocketFD = atoi(env_var_val);ALOGV("Zygote:zygoteSocketFD = %d", gZygoteSocketFD);} else {ALOGE("Unable to fetch Zygote socket file descriptor");}env_var_name = android_socket_prefix + (is_primary ? "usap_pool_primary" : "usap_pool_secondary");env_var_val = getenv(env_var_name.c_str());if (env_var_val != nullptr) {gUsapPoolSocketFD = atoi(env_var_val);ALOGV("Zygote:usapPoolSocketFD = %d", gUsapPoolSocketFD);} else {ALOGE("Unable to fetch USAP pool socket file descriptor");}initUnsolSocketToSystemServer();/** Security Initialization*/// security_getenforce is not allowed on app process. Initialize and cache// the value before zygote forks.gIsSecurityEnforced = security_getenforce();selinux_android_seapp_context_init();/** Storage Initialization*/UnmountStorageOnInit(env);/** Performance Initialization*/if (!SetTaskProfiles(0, {})) {ZygoteFailure(env, "zygote", nullptr, "Zygote SetTaskProfiles failed");}
}// Create the socket which is going to be used to send unsolicited message
// to system_server, the socket will be closed post forking a child process.
// It's expected to be called at each zygote's initialization.
static void initUnsolSocketToSystemServer() {gSystemServerSocketFd = socket(AF_LOCAL, SOCK_DGRAM | SOCK_NONBLOCK, 0);if (gSystemServerSocketFd >= 0) {ALOGV("Zygote:systemServerSocketFD = %d", gSystemServerSocketFd);} else {ALOGE("Unable to create socket file descriptor to connect to system_server");}
}

4.2 SystemServer的创建

ZygoteInit 只是systemserve创建的发起者,创建systemserver启动的核心成员ZygoteServer和ZygoteArguments,然后由Zygote.java 的nativeForkSystemServer()去实施fork并返回子进程pid。

  1. ZygoteServer 初始化
ZygoteServer(boolean isPrimaryZygote) {mUsapPoolEventFD = Zygote.getUsapPoolEventFD();if (isPrimaryZygote) {//根据环境变量 ANDROID_SOCKET_zygote 获取socket文件句柄。mZygoteSocket =  Zygote.createManagedSocketFromInitSocket(Zygote.PRIMARY_SOCKET_NAME);//根据环境变量 ANDROID_SOCKET_usap_pool_primary 获取usap_pool_primary句柄mUsapPoolSocket = Zygote.createManagedSocketFromInitSocket(Zygote.USAP_POOL_PRIMARY_SOCKET_NAME);} else {mZygoteSocket = Zygote.createManagedSocketFromInitSocket(Zygote.SECONDARY_SOCKET_NAME);mUsapPoolSocket =Zygote.createManagedSocketFromInitSocket(Zygote.USAP_POOL_SECONDARY_SOCKET_NAME);}mUsapPoolSupported = true;fetchUsapPoolPolicyProps();
}/*** Creates a managed LocalServerSocket object using a file descriptor* created by an init.rc script.  The init scripts that specify the* sockets name can be found in system/core/rootdir.  The socket is bound* to the file system in the /dev/sockets/ directory, and the file* descriptor is shared via the ANDROID_SOCKET_<socketName> environment* variable.*/static LocalServerSocket createManagedSocketFromInitSocket(String socketName) {int fileDesc;final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;try {String env = System.getenv(fullSocketName);fileDesc = Integer.parseInt(env);} catch (RuntimeException ex) {throw new RuntimeException("Socket unset or invalid: " + fullSocketName, ex);}try {FileDescriptor fd = new FileDescriptor();fd.setInt$(fileDesc);return new LocalServerSocket(fd);} catch (IOException ex) {throw new RuntimeException("Error building socket from file descriptor: " + fileDesc, ex);}}
  1. ZygoteArguments 的创建
/* Hardcoded command line to start the system server */
String args[] = {"--setuid=1000","--setgid=1000","--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,"+ "1024,1032,1065,3001,3002,3003,3006,3007,3009,3010,3011","--capabilities=" + capabilities + "," + capabilities,"--nice-name=system_server","--runtime-args","--target-sdk-version=" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT,"com.android.server.SystemServer",};
  1. 委托 Zygote 创建进程并返回进程ID

父进程中返回的是子进程ID,子进程中返回pid = 0。

    public static int forkSystemServer(int uid, int gid, int[] gids, int runtimeFlags,int[][] rlimits, long permittedCapabilities, long effectiveCapabilities) {ZygoteHooks.preFork();int pid = nativeForkSystemServer(uid, gid, gids, runtimeFlags, rlimits,permittedCapabilities, effectiveCapabilities);// Set the Java Language thread priority to the default value for new apps.Thread.currentThread().setPriority(Thread.NORM_PRIORITY);ZygoteHooks.postForkCommon();return pid;}
  1. 关闭子进程中的 socket 文件句柄。
        /* For child process */if (pid == 0) {if (hasSecondZygote(abiList)) {waitForSecondaryZygote(socketName);}zygoteServer.closeServerSocket();//子进程返回 runable 接口return handleSystemServerProcess(parsedArgs);}//父进程返回空。return null
  1. 子进程中的 handleSystemServerProcess

创建Binder线程,创建main 函数的反射runable 接口。

/*** Finish remaining work for the newly forked system server process.*/private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) {// set umask to 0077 so new files and directories will default to owner-only permissions.Os.umask(S_IRWXG | S_IRWXO);if (parsedArgs.mNiceName != null) {Process.setArgV0(parsedArgs.mNiceName);}final String systemServerClasspath = Os.getenv("SYSTEMSERVERCLASSPATH");...if (parsedArgs.mInvokeWith != null) {...} else {ClassLoader cl = null;if (systemServerClasspath != null) {cl = createPathClassLoader(systemServerClasspath, parsedArgs.mTargetSdkVersion);Thread.currentThread().setContextClassLoader(cl);}/** Pass the remaining arguments to SystemServer.*/return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,parsedArgs.mDisabledCompatChanges,parsedArgs.mRemainingArgs, cl);}/* should never reach here */}/*** The main function called when started through the zygote process. This could be unified with* main(), if the native code in nativeFinishInit() were rationalized with Zygote startup.<p>** Current recognized args:* <ul>* <li> <code> [--] &lt;start class name&gt;  &lt;args&gt;* </ul>*/public static final Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges, String[] argv, ClassLoader classLoader) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");RuntimeInit.redirectLogStreams();RuntimeInit.commonInit();ZygoteInit.nativeZygoteInit();//创建Binder线程池,用于Binder通信return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv, classLoader);}protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges, String[] argv, ClassLoader classLoader) {// If the application calls System.exit(), terminate the process// immediately without running any shutdown hooks.  It is not possible to// shutdown an Android application gracefully.  Among other things, the// Android runtime shutdown hooks close the Binder driver, which can cause// leftover running threads to crash before the process actually exits.nativeSetExitWithoutCleanup(true);VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);VMRuntime.getRuntime().setDisabledCompatChanges(disabledCompatChanges);final Arguments args = new Arguments(argv);// The end of of the RuntimeInit event (see #zygoteInit).Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);// Remaining arguments are passed to the start class's static mainreturn findStaticMain(args.startClass, args.startArgs, classLoader);}/*** Invokes a static "main(argv[]) method on class "className".* Converts various failing exceptions into RuntimeExceptions, with* the assumption that they will then cause the VM instance to exit.*/protected static Runnable findStaticMain(String className, String[] argv,ClassLoader classLoader) {Class<?> cl;try {cl = Class.forName(className, true, classLoader);} catch (ClassNotFoundException ex) {}Method m;try {m = cl.getMethod("main", new Class[] { String[].class });} catch (NoSuchMethodException ex) {}int modifiers = m.getModifiers();/** This throw gets caught in ZygoteInit.main(), which responds* by invoking the exception's run() method. This arrangement* clears up all the stack frames that were required in setting* up the process.*/return new MethodAndArgsCaller(m, argv);}/*** Helper class which holds a method and arguments and can call them. This is used as part of* a trampoline to get rid of the initial process setup stack frames.*/static class MethodAndArgsCaller implements Runnable {/** method to call */private final Method mMethod;/** argument array */private final String[] mArgs;public MethodAndArgsCaller(Method method, String[] args) {mMethod = method;mArgs = args;}public void run() {try {mMethod.invoke(null, new Object[] { mArgs });} catch (IllegalAccessException ex) {}}}
  1. zygote循环监听socket文件变化

创建出systemserver 子进程后,由于systemserver 子进程返回的是非空runable,父进程zygote返回是null,所以子进程会执行run()方法然后return结束。父进程会执行zygoteServer.runSelectLoop(abiList);

public static void main(String argv[]) {if (startSystemServer) {Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);// {@code r == null} in the parent (zygote) process, and {@code r != null} in the// child (system_server) process.if (r != null) {r.run();return;}}Log.i(TAG, "Accepting command socket connections");// The select loop returns early in the child process after a fork and// loops forever in the zygote.caller = zygoteServer.runSelectLoop(abiList);// We're in the child process and have exited the select loop. Proceed to execute the// command.if (caller != null) {caller.run();}
}

runSelectLoop 会去监听两个socket(init.zygote64.rc中的socket)文件句柄的变化,通过acceptCommandPeer()创建socket的接收端ZygoteConnection,一旦文件句柄发生变化就会由ZygoteConnection的processOneCommand(this)来处理。

在processOneCommand方法中通过Zygote.forkAndSpecialize()去处理子进程的创建,通过handleChildProc -> ZygoteInit.zygoteInit 构建main 函数的反射函数的runable 接口,一路返回到ZygoteInit的man函数,由于父进程得到的返回是null,子进程不为空,才会去执行的runable的run 方法,calss的main函数就会被执行到,例如应用创建时的ActivityThread.main()。

5. 总结

ZygoteInit 做初始化
Zygote 用于native层交流
ZygoteServer 永来管理socket
ZygoteConnection 用来接收socket消息并处理。

用了一张图片来总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGMpBEeq-1603424070135)(res/picture/zygote启动.jpg)]

参考原创传送门

深入理解 AndroidFramework 之 Zygote 启动相关推荐

  1. Zygote启动及其作用

    目录 1.Zygote简介 2.Zygote进程如何启动 2.1 init.zygote64_32.rc文件 2.2 查看ps信息 2.3 启动 3.Zygote作用 3.1 启动system_ser ...

  2. Android Framework——zygote 启动 SystemServer

    概述 在Android系统中,所有的应用程序进程以及系统服务进程SystemServer都是由Zygote进程孕育(fork)出来的,这也许就是为什么要把它称为Zygote(受精卵)的原因吧.由于Zy ...

  3. ActivityThread的理解和APP的启动过程

    ActivityThread的理解和APP的启动过程 ActivityThread ActivityThread的初始化 主线程Looper的初始化 主线程Handler的初始化 Applicatio ...

  4. Zygote启动流程及源码分析

    1 Zygote是什么 在Android中,负责孵化新进程的这个进程叫做Zygote,安卓上其他的应用进程都是由zygote孵化的.众所周知,安卓是Linux内核,安卓系统上运行的一切程序都是放在Da ...

  5. android zygote启动流程,Android zygote启动流程详解

    对zygote的理解 在Android系统中,zygote是一个native进程,是所有应用进程的父进程.而zygote则是Linux系统用户空间的第一个进程--init进程,通过fork的方式创建并 ...

  6. zygote启动过程

    作者:贾东风 1. zygote是什么? 在 Android 系统中,JavaVM(Java 虚拟机).应用程序进程以及运行系统关键服务的 SystemServer 进程都是由 Zygote 来创建的 ...

  7. 【Android 10 源码】深入理解 software Codec2 服务启动

    MediaCodec 系列文章: [Android 10 源码]深入理解 MediaCodec 硬解码初始化 [Android 10 源码]深入理解 Omx 初始化 [Android 10 源码]深入 ...

  8. LIC 2022 视频语义理解基线(快速启动版)

    转自AI Studio,原文链接: LIC 2022 视频语义理解基线(快速启动版) - 飞桨AI Studio LIC2022视频语义理解基线 ❗️该版本为快速启动版,训练集取比赛提供的训练集的子集 ...

  9. Zygote启动流程解析

    目录 1.什么是Zygote? 2. Zygote脚本启动 3.Zygote进程启动 1.什么是Zygote? Zygote是Android系统创建的第一个Java进程,它是所有Java进程的父进程. ...

最新文章

  1. BERT在小米NLP业务中的实战探索
  2. springboot2 war页面放在那_成为微服务架构师--SpringBoot2学习笔记
  3. 体验VSTS源代码管理之一
  4. Selenium API-WebDriver 方法
  5. thinkphp连接远程数据库慢_干货分享—Niushop数据库配置
  6. [virtualbox] win10与centos共享目录下,nginx访问问题
  7. 物联网工程课程设计论文
  8. 10K 3435热敏电阻阻值表
  9. 翰文付费打印后还有水印吗_翰文进度计划编制系统去除水印中文增强版
  10. jwplayer.v7.1.4视频播放器的使用
  11. 威胁快报|Nexus Repository Manager 3新漏洞已被用于挖矿木马传播,建议用户尽快修复...
  12. 自动驾驶寻找「商业闭环」
  13. 最实用的chrome插件,助高效开发,加快步伐!
  14. 数据采集的基本方法?
  15. 做一只跑过灰狼的兔子
  16. 什么是代理IP池,如何构建?
  17. Nginx反向代理配置详解
  18. 进程、线程、纤程的区别
  19. SVN本地目录创建及使用
  20. XwareDesktop

热门文章

  1. Windows11,视频教你轻松恢复出厂状态
  2. There is already ‘ ‘ bean method 解决方案
  3. 未明学院:两大互联网热门实战项目 | 掌握数据分析核心技能,互联网热门岗位不再遥远!
  4. css鼠标移上显示红色禁止符号
  5. linux中popen函数,system函数与popen函数
  6. Jenkins: ERROR: Exception when publishing, exception message [Exec timed out or was interrupted aft
  7. 获取(遍历)字符串中每个字符的----两种方法
  8. js读取viewbag的数据
  9. Visio Professional之活动图
  10. Flutter 之ListView实现下拉刷新和上拉加载更多