type
status
date
slug
summary
tags
category
icon
password
Boot ROM
当按下电源键,硬件上电之后,会从一个固定的内存区域读取程序.这个程序是烧写到硬件上的(ROM),用于将bootloader加载到RAM中,并开始执行它.
bootloader
bootloader用于告诉设备如何找到系统内核,和启动内核.
手机厂商一般会在bootloader中加上密钥锁和一些限制.
bootloader执行一般分为两个阶段:
- 检测外部RAM内存,并加载一段bootloader代码用于第二阶段的执行
- 设置运行内核所需要的网络和内存等.
高通芯片提供的LK,就可以作为一个Android的bootloader.
常见的bootloader有:
- U-boot
- LK
kernel
Android使用的linux kernel,当kernel启动时,会执行一系列的初始化操作,比如设置缓存,内存,加载驱动程序,挂载根文件系统,初始化输入输出等.
当内核启动完成之后,第一件要做的事就是在系统文件中找一个“init”,作为根进程或者第一个系统进程.
看一下linux kernel的源码,找一个arm64架构开始分析:
- 入口在
arch/arm64/kernel/head.S
中
__INIT /* * The following callee saved general purpose registers are used on the * primary lowlevel boot path: * * Register Scope Purpose * x21 stext() .. start_kernel() FDT pointer passed at boot in x0 * x23 stext() .. start_kernel() physical misalignment/KASLR offset * x28 __create_page_tables() callee preserved temp register * x19/x20 __primary_switch() callee preserved temp registers * x24 __primary_switch() .. relocate_kernel() * current RELR displacement */ SYM_CODE_START(stext) bl preserve_boot_args bl el2_setup // Drop to EL1, w0=cpu_boot_mode adrp x23, __PHYS_OFFSET and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0 bl set_cpu_boot_mode_flag bl __create_page_tables /* * The following calls CPU setup code, see arch/arm64/mm/proc.S for * details. * On return, the CPU will be ready for the MMU to be turned on and * the TCR will have been set. */ mov x0, #ARM64_CPU_BOOT_PRIMARY bl __cpu_setup // initialise processor b __primary_switch SYM_CODE_END(stext) /* * The following fragment of code is executed with the MMU enabled. * * x0 = __PHYS_OFFSET */ SYM_FUNC_START_LOCAL(__primary_switched) adrp x4, init_thread_union add sp, x4, #THREAD_SIZE adr_l x5, init_task msr sp_el0, x5 // Save thread_info adr_l x8, vectors // load VBAR_EL1 with virtual msr vbar_el1, x8 // vector table address isb stp xzr, x30, [sp, #-16]! mov x29, sp str_l x21, __fdt_pointer, x5 // Save FDT pointer ldr_l x4, kimage_vaddr // Save the offset between sub x4, x4, x0 // the kernel virtual and str_l x4, kimage_voffset, x5 // physical mappings // Clear BSS adr_l x0, __bss_start mov x1, xzr adr_l x2, __bss_stop sub x2, x2, x0 bl __pi_memset dsb ishst // Make zero page visible to PTW #ifdef CONFIG_KASAN bl kasan_early_init #endif #ifdef CONFIG_RANDOMIZE_BASE tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized? b.ne 0f mov x0, x21 // pass FDT address in x0 bl kaslr_early_init // parse FDT for KASLR options cbz x0, 0f // KASLR disabled? just proceed orr x23, x23, x0 // record KASLR offset ldp x29, x30, [sp], #16 // we must enable KASLR, return ret // to __primary_switch() 0: #endif add sp, sp, #16 mov x29, #0 mov x30, #0 b start_kernel SYM_FUNC_END(__primary_switched)
_INIT
步骤下,执行stext
,会执行到__primary_switched
,然后执行到start_kernel
,这里的start_kernel
就是内核C代码的入口.这个时候“0号”进程“swapper”(一个Idle进程)已经启动了(init_task.c中调用).
- 启动内核(C代码层面)
start_kernel
是在init/main.c
中定义的:asmlinkage __visible void __init start_kernel(void)
这里会进行很多的初始化操作.最后会执行内核初始化操作,创建init进程:
static int __ref kernel_init(void *unused) { int ret; kernel_init_freeable(); /* need to finish all async __init code before freeing the memory */ async_synchronize_full(); ftrace_free_init_mem(); free_initmem(); mark_readonly(); /* * Kernel mappings are now finalized - update the userspace page-table * to finalize PTI. */ pti_finalize(); system_state = SYSTEM_RUNNING; numa_default_policy(); rcu_end_inkernel_boot(); if (ramdisk_execute_command) { ret = run_init_process(ramdisk_execute_command); if (!ret) return 0; pr_err("Failed to execute %s (error %d)\\n", ramdisk_execute_command, ret); } /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); } if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/admin-guide/init.rst for guidance."); }
这个时候init进程就已经起来了.
最后执行的那几个
try_to_run_init_process
会将init进程从内核态转换成用户态.(从核心线程变成了/sbin/init的普通进程)init
源码在:
system/core/init/main.cpp
进程位置:
/system/core/init
init进程是用户空间的第一个进程,也是所有进程的“祖父”进程.
init进程主要负责两件事:
- 挂载(mount) /sys, /dev 或者/proc等文件
- 运行/init.rc脚本,
init.rc
负责系统的初始化设置.
init.rc位置:
/system/core/rootdir/init.rc
init.rc
脚本会启动servicemanager,创建zygote进程(fork方式).init.rc
中会引用import /system/etc/init/hw/init.${ro.zygote}.rc
,这里根据架构不同,分为三个文件:- init.zygote32.rc
- init.zygote64.rc
- init.zygote64_32.rc
其中定义了zygote服务:
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server 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 write /sys/power/state on onrestart restart audioserver onrestart restart cameraserver onrestart restart media onrestart restart netd onrestart restart wificond writepid /dev/cpuset/foreground/tasks
servicemanager.rc
则定义了servicemanager服务:service servicemanager /system/bin/servicemanager class core animation user system group system readproc critical onrestart restart healthd onrestart restart zygote onrestart restart audioserver onrestart restart media onrestart restart surfaceflinger onrestart restart inputflinger onrestart restart drm onrestart restart cameraserver onrestart restart keystore onrestart restart gatekeeperd onrestart restart thermalservice writepid /dev/cpuset/system-background/tasks shutdown critical
Zygote and VM
从上面的zygote服务定义来看,会通过app_process来创建zygote进程:
/system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
这个命令行的格式如下:
app_process [java-options] cmd-dir start-class-name [options]
可以看出来,
-Xzygote
是java-options,命令行程序位置在/system/bin下,--zygote --start-system-server
则为传入的选项.app_process64
实际上是一个单独的程序,源码入口在frameworks/base/cmds/app_process/app_main.cpp
其中的main方法中会执行:
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
这个命令首先会创建一个VM,然后调用ZygoteInit.java的main方法.Zygote接收到一个请求,会通过
/dev/socket/zygote
启动一个进程,即触发fork
调用,创建Zygote进程.runtime实际上就是
AndroidRuntime
,源码在frameworks/base/core/jni/AndroidRuntime.cpp
,在它的start方法中,主要处理两件事:- 启动虚拟机
/* start the virtual machine */ JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv* env; if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) { return; } onVmCreated(env); /* * Register android functions. */ if (startReg(env) < 0) { ALOGE("Unable to register all android natives\\n"); return; }
startVM
中会启动Dalvik虚拟机(为什么不是ART虚拟机),并初始化JNI环境:/* * 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. */ if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) { ALOGE("JNI_CreateJavaVM failed\\n"); return -1; }
然后就是注册系统库中的JNI方法:/* * 注册Android JNI方法 */ /*static*/ int AndroidRuntime::startReg(JNIEnv* env) { if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) { env->PopLocalFrame(NULL); return -1; } return 0; } // 这里就是要注册的系统JNI方法 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_SystemClock), REG_JNI(register_android_util_EventLog), REG_JNI(register_android_util_Log), REG_JNI(register_android_util_MemoryIntArray), REG_JNI(register_android_util_PathParser), REG_JNI(register_android_util_StatsLog), REG_JNI(register_android_util_StatsLogInternal), REG_JNI(register_android_app_admin_SecurityLog), REG_JNI(register_android_content_AssetManager), REG_JNI(register_android_content_StringBlock), REG_JNI(register_android_content_XmlBlock), REG_JNI(register_android_content_res_ApkAssets), REG_JNI(register_android_text_AndroidCharacter), REG_JNI(register_android_text_Hyphenator), REG_JNI(register_android_view_InputDevice), REG_JNI(register_android_view_KeyCharacterMap), REG_JNI(register_android_os_Process), REG_JNI(register_android_os_SystemProperties), REG_JNI(register_android_os_Binder), REG_JNI(register_android_os_Parcel), REG_JNI(register_android_os_HidlMemory), REG_JNI(register_android_os_HidlSupport), REG_JNI(register_android_os_HwBinder), REG_JNI(register_android_os_HwBlob), REG_JNI(register_android_os_HwParcel), REG_JNI(register_android_os_HwRemoteBinder), REG_JNI(register_android_os_NativeHandle), REG_JNI(register_android_os_VintfObject), REG_JNI(register_android_os_VintfRuntimeInfo), REG_JNI(register_android_graphics_Canvas), // This needs to be before register_android_graphics_Graphics, or the latter // will not be able to find the jmethodID for ColorSpace.get(). REG_JNI(register_android_graphics_ColorSpace), REG_JNI(register_android_graphics_Graphics), REG_JNI(register_android_view_DisplayEventReceiver), REG_JNI(register_android_view_RenderNode), REG_JNI(register_android_view_RenderNodeAnimator), REG_JNI(register_android_view_DisplayListCanvas), REG_JNI(register_android_view_InputApplicationHandle), REG_JNI(register_android_view_InputWindowHandle), REG_JNI(register_android_view_TextureLayer), REG_JNI(register_android_view_ThreadedRenderer), REG_JNI(register_android_view_Surface), REG_JNI(register_android_view_SurfaceControl), REG_JNI(register_android_view_SurfaceSession), REG_JNI(register_android_view_CompositionSamplingListener), REG_JNI(register_android_view_TextureView), REG_JNI(register_com_android_internal_view_animation_NativeInterpolatorFactoryHelper), REG_JNI(register_com_google_android_gles_jni_EGLImpl), REG_JNI(register_com_google_android_gles_jni_GLImpl), REG_JNI(register_android_opengl_jni_EGL14), REG_JNI(register_android_opengl_jni_EGL15), REG_JNI(register_android_opengl_jni_EGLExt), REG_JNI(register_android_opengl_jni_GLES10), REG_JNI(register_android_opengl_jni_GLES10Ext), REG_JNI(register_android_opengl_jni_GLES11), REG_JNI(register_android_opengl_jni_GLES11Ext), REG_JNI(register_android_opengl_jni_GLES20), REG_JNI(register_android_opengl_jni_GLES30), REG_JNI(register_android_opengl_jni_GLES31), REG_JNI(register_android_opengl_jni_GLES31Ext), REG_JNI(register_android_opengl_jni_GLES32), REG_JNI(register_android_graphics_Bitmap), REG_JNI(register_android_graphics_BitmapFactory), REG_JNI(register_android_graphics_BitmapRegionDecoder), REG_JNI(register_android_graphics_ByteBufferStreamAdaptor), REG_JNI(register_android_graphics_Camera), REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor), REG_JNI(register_android_graphics_CanvasProperty), REG_JNI(register_android_graphics_ColorFilter), REG_JNI(register_android_graphics_DrawFilter), REG_JNI(register_android_graphics_FontFamily), REG_JNI(register_android_graphics_GraphicBuffer), REG_JNI(register_android_graphics_ImageDecoder), REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable), REG_JNI(register_android_graphics_Interpolator), REG_JNI(register_android_graphics_MaskFilter), REG_JNI(register_android_graphics_Matrix), REG_JNI(register_android_graphics_Movie), REG_JNI(register_android_graphics_NinePatch), REG_JNI(register_android_graphics_Paint), REG_JNI(register_android_graphics_Path), REG_JNI(register_android_graphics_PathMeasure), REG_JNI(register_android_graphics_PathEffect), REG_JNI(register_android_graphics_Picture), REG_JNI(register_android_graphics_Region), REG_JNI(register_android_graphics_Shader), REG_JNI(register_android_graphics_SurfaceTexture), REG_JNI(register_android_graphics_Typeface), REG_JNI(register_android_graphics_YuvImage), REG_JNI(register_android_graphics_drawable_AnimatedVectorDrawable), REG_JNI(register_android_graphics_drawable_VectorDrawable), REG_JNI(register_android_graphics_fonts_Font), REG_JNI(register_android_graphics_fonts_FontFamily), REG_JNI(register_android_graphics_pdf_PdfDocument), REG_JNI(register_android_graphics_pdf_PdfEditor), REG_JNI(register_android_graphics_pdf_PdfRenderer), REG_JNI(register_android_graphics_text_MeasuredText), REG_JNI(register_android_graphics_text_LineBreaker), REG_JNI(register_android_database_CursorWindow), REG_JNI(register_android_database_SQLiteConnection), REG_JNI(register_android_database_SQLiteGlobal), REG_JNI(register_android_database_SQLiteDebug), REG_JNI(register_android_os_Debug), REG_JNI(register_android_os_FileObserver), REG_JNI(register_android_os_GraphicsEnvironment), REG_JNI(register_android_os_MessageQueue), REG_JNI(register_android_os_SELinux), REG_JNI(register_android_os_Trace), REG_JNI(register_android_os_UEventObserver), REG_JNI(register_android_net_LocalSocketImpl), REG_JNI(register_android_net_NetworkUtils), REG_JNI(register_android_os_MemoryFile), REG_JNI(register_android_os_SharedMemory), REG_JNI(register_com_android_internal_os_ClassLoaderFactory), REG_JNI(register_com_android_internal_os_Zygote), REG_JNI(register_com_android_internal_os_ZygoteInit), REG_JNI(register_com_android_internal_util_VirtualRefBasePtr), REG_JNI(register_android_hardware_Camera), REG_JNI(register_android_hardware_camera2_CameraMetadata), REG_JNI(register_android_hardware_camera2_legacy_LegacyCameraDevice), REG_JNI(register_android_hardware_camera2_legacy_PerfMeasurement), REG_JNI(register_android_hardware_camera2_DngCreator), REG_JNI(register_android_hardware_HardwareBuffer), REG_JNI(register_android_hardware_SensorManager), REG_JNI(register_android_hardware_SerialPort), REG_JNI(register_android_hardware_SoundTrigger), REG_JNI(register_android_hardware_UsbDevice), REG_JNI(register_android_hardware_UsbDeviceConnection), REG_JNI(register_android_hardware_UsbRequest), REG_JNI(register_android_hardware_location_ActivityRecognitionHardware), REG_JNI(register_android_media_AudioEffectDescriptor), REG_JNI(register_android_media_AudioSystem), REG_JNI(register_android_media_AudioRecord), REG_JNI(register_android_media_AudioTrack), REG_JNI(register_android_media_AudioAttributes), REG_JNI(register_android_media_AudioProductStrategies), REG_JNI(register_android_media_AudioVolumeGroups), REG_JNI(register_android_media_AudioVolumeGroupChangeHandler), REG_JNI(register_android_media_MicrophoneInfo), REG_JNI(register_android_media_RemoteDisplay), REG_JNI(register_android_media_ToneGenerator), REG_JNI(register_android_media_midi), REG_JNI(register_android_opengl_classes), REG_JNI(register_android_server_NetworkManagementSocketTagger), REG_JNI(register_android_ddm_DdmHandleNativeHeap), REG_JNI(register_android_backup_BackupDataInput), REG_JNI(register_android_backup_BackupDataOutput), REG_JNI(register_android_backup_FileBackupHelperBase), REG_JNI(register_android_backup_BackupHelperDispatcher), REG_JNI(register_android_app_backup_FullBackup), REG_JNI(register_android_app_Activity), REG_JNI(register_android_app_ActivityThread), REG_JNI(register_android_app_NativeActivity), REG_JNI(register_android_util_jar_StrictJarFile), REG_JNI(register_android_view_InputChannel), REG_JNI(register_android_view_InputEventReceiver), REG_JNI(register_android_view_InputEventSender), REG_JNI(register_android_view_InputQueue), REG_JNI(register_android_view_KeyEvent), REG_JNI(register_android_view_MotionEvent), REG_JNI(register_android_view_PointerIcon), REG_JNI(register_android_view_VelocityTracker), REG_JNI(register_android_content_res_ObbScanner), REG_JNI(register_android_content_res_Configuration), REG_JNI(register_android_animation_PropertyValuesHolder), REG_JNI(register_android_security_Scrypt), REG_JNI(register_com_android_internal_content_NativeLibraryHelper), REG_JNI(register_com_android_internal_os_AtomicDirectory), REG_JNI(register_com_android_internal_os_FuseAppLoop), REG_JNI(register_com_android_internal_os_KernelCpuUidBpfMapReader), REG_JNI(register_com_android_internal_os_KernelSingleUidTimeReader), };
调用ZygoteInit.java的main方法,创建zygote进程,和systemserver进程.先看AndroidRuntime.cpp中是如何调用ZygoteInit.java中的main()方法的:// 拿到main方法 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 { // 调用main方法 env->CallStaticVoidMethod(startClass, startMeth, strArray); #if 0 if (env->ExceptionCheck()) threadExitUncaughtException(env); #endif }
可以看出来同样是通过jni调用.进入ZygoteInit.java看一下main()方法:// Zygote goes into its own process group. try { Os.setpgid(0, 0); } catch (ErrnoException ex) { throw new RuntimeException("Failed to setpgid(0,0)", ex); } Runnable caller; zygoteServer = new ZygoteServer(isPrimaryZygote); 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; } } // 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(); }
可以看到,这里会创建ZygoteServer,进入Zygote进程,并通过zygote进程fork System Server进程:/* Request to fork the system server process */ pid = Zygote.forkSystemServer( parsedArgs.mUid, parsedArgs.mGid, parsedArgs.mGids, parsedArgs.mRuntimeFlags, null, parsedArgs.mPermittedCapabilities, parsedArgs.mEffectiveCapabilities); // Zygote.java 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; }
Zygote是一个VM进程,这个时候就进入了Java世界了.System Server进程也创建了.
System Servers
既然system server是一个进程,那么通常也有一个main()方法作为入口:
/** * The main entry point from zygote. */ public static void main(String[] args) { new SystemServer().run(); }
主要做的工作都在
run
方法中:// 切换runtime,即dalvik 还是 art SystemProperties.set("persist.sys.dalvik.vm.lib.2", VMRuntime.getRuntime().vmLibrary()); Looper.prepareMainLooper(); // 初始化native service System.loadLibrary("android_servers"); // 初始化系统context. createSystemContext(); // 创建 system service manager. mSystemServiceManager = new SystemServiceManager(mSystemContext); mSystemServiceManager.setStartInfo(mRuntimeRestart, mRuntimeStartElapsedTime, mRuntimeStartUptime); LocalServices.addService(SystemServiceManager.class, mSystemServiceManager); // 启动service startBootstrapServices(); startCoreServices(); startOtherServices(); Looper.loop();
上面的服务分为三种:
- Bootstrap Service
- Core Service
- Other Service
每个类型的服务数量都非常之多,并且服务直接的启动顺序是有讲究的.
启动完成
在一些基础的Service启动完成之后,会执行一些Service的systemReady()方法,进一步执行app进程可以干的事.
在ActivityManagerService中,调用finishBooting()方法,也就意味着启动完成.
final void finishBooting() { // Let the ART runtime in zygote and system_server know that the boot completed. // 让zygote中的ART虚拟机 和system server进程知道启动已经完成 ZYGOTE_PROCESS.bootCompleted(); VMRuntime.bootCompleted(); // 让系统Service知道已经启动完成 mSystemServiceManager.startBootPhase(SystemService.PHASE_BOOT_COMPLETED); }
启动Launch App(桌面)
ActivityManagerService启动完毕之后,会通过默认的"homeIntent"启动Launch App.
LaunchAPP启动完毕之后,会发送启动完成的广播,供应用监听.
总结
- 启动流程分为以下几个步骤:
- 按下电源开关,CPU上电,开始读取固化在ROM中的一段代码,将bootloader代码载入内存,并开始执行
- bootloader执行一些硬件初始化操作,并启动Linux kernel
- Linux kernel 进行各种初始化之后,创建一个init进程,并转移到用户态
- init进程会读取init.rc中的配置文件,启动各种服务,包括使用bootannimation程序开启开机动画,使用app_process程序创建虚拟机和Zygote进程,Zygote进程是VM中第一个进程.
- Zygote会初始化JNI环境,注册一些预置的JNI方法,并创建System Server进程
- System Server进程会加载"libandroid_servers.so",会直接创建启动或者使用
SystemServiceManager
创建并启动各种服务,包括ActivityManagerService.而且这些服务都必须继承SystemService
类. - ActivityManagerService中会发送启动完成的标记,然后启动Launch App
- Launch App启动完成之后会发送启动完成的广播
- 系统程序都在
/system/bin
下,包括init
bootanimation
等,并不是什么很玄的东西,都只是个普通程序而已.
- 最无效的方式就是看各种源码分析文章(如果有人在看我写的这篇,赶紧关掉吧),自己下载个源码从入口开始分析,会让你更加清楚启动的流程.
- 代码本身并没有什么意义,Android源码也是人写的,而且写的还不怎么样,关键的是在阅读源码的过程中,深入了解一个嵌入式操作系统的实现方式,遇到的问题,以及解决问题的方案,毕竟一个市场成功的操作系统在设计上肯定有很多可取之处的.
- 不要一叶障目,也不要只见树木,不见森林,几十个G的代码可能看一辈子也不可能看完,重要的是知道关键之处如何利用现有的工具去设计实现我们想要的功能.
- 这篇文章只是大概的分析了下启动流程,而且很多地方不够详细,高度也不够(充斥了大量源码),希望后面自己软件功底深了之后会站在更高的层次来看这个吧.
- 作者:姜康
- 链接:https://jiangkang.tech/article/901e18a5-0c63-4202-9045-3d0ff241de85
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章