计算机启动过程Linux内核Kernel启动过程介绍了计算机启动和内核加载,本篇文章主要介绍Android系统是如何启动的。

一、Android启动流程

Android系统的启动流程与Linux接近:

sequenceDiagram
participant Bootloader as 引导加载程序
participant Kernel as 内核
participant Init as init 进程
participant Zygote as Zygote 进程
participant SystemServices as 系统服务
participant Launcher as 应用程序(桌面)

Bootloader->>Kernel: 加载内核
Kernel->>Kernel: 初始化
Kernel->>Init: 启动 init 进程
Init->>Init: 读取系统配置文件
Init->>Zygote: 启动 Zygote 进程
Zygote->>Zygote: 预加载常用的 Java 类和资源
Zygote->>SystemServices: 启动系统服务
SystemServices->>Launcher: 启动桌面程序

  • 1.引导加载程序(Bootloader)启动: 当设备上电或者重启时,首先会由引导加载程序负责启动。引导加载程序通常存储在设备的固件中,它的主要任务是初始化硬件,并加载并启动操作系统内核。引导加载程序会首先运行自身的初始化代码,然后加载操作系统内核到内存中。
  • 2.内核加载: 引导加载程序会根据预定义的配置从设备存储中加载操作系统内核。在Android设备中,通常使用的是Linux内核。引导加载程序将内核加载到内存中的指定位置。
  • 3.内核初始化: 一旦内核加载到内存中,引导加载程序会将控制权转交给内核。内核开始执行初始化过程,包括对硬件进行初始化、建立虚拟文件系统、创建进程和线程等。
  • 4.启动 init 进程: 内核初始化完成后,会启动名为init的用户空间进程。init进程是Android系统的第一个用户空间进程,它负责系统的进一步初始化和启动。init进程会读取系统配置文件(例如 init.rc),并根据其中的指令启动系统服务和应用程序。
  • 5.启动 Zygote 进程: 在init进程启动后,会启动名为Zygote的进程。Zygote进程是Android应用程序的孵化器,它会预加载常用的Java类和资源,以加速应用程序的启动。
  • 6.启动系统服务: 在Zygote进程启动后,还会启动一系列系统服务,例如SurfaceFlinger、ActivityManager、PackageManager等。这些系统服务负责管理系统的各个方面,包括显示、应用程序生命周期、包管理等。
  • 7.启动桌面程序: 一旦系统服务启动完成,Android系统就处于可用状态。就会启动桌面程序,用户可以开始启动应用程序并使用设备进行各种操作了。

计算机启动过程Linux内核Kernel启动过程已经介绍了计算机启动和内核加载,所以本篇文章从Zygote进程开始介绍。

二、Zygote进程(孵化器进程)

1.Zygote简介

  • Zygote进程是一个用户进程,由init进程(1号进程)fork而来。
  • Zygote进程的主要任务是加载系统的核心类库(如Java核心库和Android核心库)和安卓系统服务(SystemService),然后进入一个循环,等待请求来创建新的 Android 应用程序进程。
  • Zygote进程通过fork的方式创建新的应用程序进程。

2.Zygote进程的创建

Zygote进程在系统启动时由init进程创建。init进程是Linux系统中的第一个用户空间进程,它通过解析init.rc文件来启动各种服务和进程,包括Zygote。具体流程如下:

2.1 启动init进程:

  • 系统启动后,内核会加载并运行init进程
  • init进程读取并解析init.rc配置文件。

init进程的启动可参考Linux内核Kernel启动过程

2.2 init.rc脚本:

init.rc文件中包含启动Zygote的指令脚本的主要代码:

// 创建zygote服务
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
class main
// 创建zygote socket,与系统和应用程序做通信
socket zygote stream 660 root system
// 定义了zygote服务重启时的一些操作
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server:

  • service zygote: 定义一个名为zygote的服务
  • /system/bin/app_process:这是启动Zygote进程的可执行文件,64位系统为app_process64
  • -Xzygote:标志表明这是一个Zygote进程启动的特殊模式。
  • /system/bin:指定进程的工作目录。
  • --zygote:告诉app_process以Zygote模式启动。
  • --start-system-server:Zygote启动时还要启动System Server进程,这是Android系统中管理关键系统服务的核心进程。

class main:

  • 将Zygote服务归类为main类别。
  • Android系统在启动过程中会启动所有“main”类别的服务。

socket zygote stream 660 root system:

创建了一个名为zygote的UNIX域Socket套接字,用于其他进程与Zygote进程通信。

onrestart write /sys/android_power/request_state wake

当zygote服务重启时,系统应该将“/sys/android_power/request_state”文件的内容设置为“wake”,以唤醒设备。

onrestart write /sys/power/state on

当zygote服务重启时,系统应该将“/sys/power/state”文件的内容设置为 “on”,以打开电源。

onrestart restart media

当zygote服务重启时,系统应该重启媒体服务(如音频、视频等),以恢复媒体功能。

onrestart restart netd

当zygote服务重启时,系统应该重启网络守护进程(netd),以恢复网络功能。

2.3 app_process文件

/system/bin/app_process是Android中的一个关键可执行文件,负责启动Zygote进程和应用进程。

Android14的app_process源码地址

2.3.1 main方法

app_process主入口点是main方法,它是整个进程启动流程的起点。以下是其主要代码和解释:

以下是关键代码说明:

int main(int argc, char* const argv[])
{
// 创建并初始化AppRuntime对象runtime
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv)); // 初始化参数zygote,startSystemServer,application,niceName,className
// 代码见源码,此处略 // 解析命令行参数
// 代码见源码,此处略 // 构建传递给 Java 初始化类的参数列表
// 代码见源码,此处略 if (zygote) {
// 调用AppRuntime的start方法,开始加载ZygoteInit类
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.");
}
}

2.3.2 AppRuntime类(AndroidRuntime)

AppRuntime继承自AndroidRuntime(ART),是Android中的一个关键类,负责管理和启动 Android 应用程序或系统服务的 Java 虚拟机 (JVM)。

Android14的AndroidRuntime源码地址

2.3.2.1 AndroidRuntime类的start方法

app_process的main方法调用了AppRuntime的start方法,也就是AppRuntime的父类AndroidRuntime的start方法

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
// 初始化Java Native Interface (JNI)。JNI是Java和C/C++之间的接口,它允许Java代码和C/C++代码相互调用
JniInvocation jni_invocation;
jni_invocation.Init(NULL); JNIEnv* env; // JNIEnv环境指针
// 初始化虚拟机
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
return;
} // 注册JNI方法
if (startReg(env) < 0) {
return;
} /*
* 以下代码执行后,当前线程(即运行 AndroidRuntime::start 方法的线程)将成为Java虚拟机(JVM)的主线程,并且在调用env->CallStaticVoidMethod启动指定的Java类的 main 方法后,这个方法不会返回,直到 JVM 退出为止。(官方文档说明)
*/ // 将"com.android.internal.os.ZygoteInit"转换为"com/android/internal/os/ZygoteInit"
char* slashClassName = toSlashClassName(className != NULL ? className : "");
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
// 没有找到ZygoteInit.main()方法
} else {
// 通过JNI调用ZygoteInit.main()方法
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
}
}
  • 创建虚拟机
  • 注册JNI方法
  • 通过JNI调用ZygoteInit.main()

3.Zygote进程

AndroidRuntimestart方法,通过JNI调用ZygoteInit.main(),系统第一次进入Java层(ZygoteInit是系统运行的第一个Java类),当前线程也正式成为Java虚拟机(JVM)的主线程。

Android14的ZygoteInit源码地址

3.1 ZygoteInit.main()

通过main方法完成资源预加载、启动系统服务等功能,为Launcher桌面程序做准备。

public static void main(String[] argv) {
// 创建ZygoteServer
ZygoteServer zygoteServer = null;
...
// 预加载资源
preload(bootTimingsTraceLog);
...
// 初始化ZygoteServer
zygoteServer = new ZygoteServer(isPrimaryZygote);
...
// 通过fork的形式初始化SystemServer
Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);
if (r != null) {
r.run();
return;
}
...
// 启动Loop,监听消息
caller = zygoteServer.runSelectLoop(abiList);
...
}

3.2 ZygoteInit.preload()预加载

通过preload方法预加载系统常用的类、资源和库,能够显著减少应用启动时的延迟,并通过共享这些预加载的内容来降低内存使用,提高系统性能。

static void preload(TimingsTraceLog bootTimingsTraceLog) {
preloadClasses(); //加载常用的Java类
preloadResources(); //加载常用的资源(如布局、图片等)
preloadOpenGL(); //加载OpenGL库
preloadSharedLibraries(); //加载常用的本地共享库
preloadTextResources(); //加载常用的文本资源
...
}

3.2.1 常用类

  • Android框架中的基础类,如Activity、Service、BroadcastReceiver等。
  • 常用的UI组件类,如TextView、Button、ImageView等。

3.2.2 常用资源

常用布局文件(layout)。

常用图片资源(drawable)。

常用字符串(strings.xml)。

3.2.3 常用库

标准C库(libc.so)。

图形处理库(libskia.so)。

OpenGL ES库(libGLESv2.so)。

3.3 启动System Server

System Server是Android系统中的关键进程,负责启动和管理核心系统服务。

启动过程的核心代码:

public static void main(String argv[]) {
// 初始化ZygoteServer
ZygoteServer zygoteServer = new ZygoteServer();
// 启动System Server
if (startSystemServer) {
startSystemServer(abiList, socketName, zygoteServer);
}
// 进入Zygote的主循环,等待新进程的启动请求
zygoteServer.runSelectLoop();
} private static boolean startSystemServer(String abiList, String socketName, ZygoteServer zygoteServer) {
/* 调用native方法fork系统服务器 */
int pid = Zygote.forkSystemServer(...);
if (pid == 0) {
// 在子进程中执行System Server的main方法
handleSystemServerProcess(parsedArgs);
}
} private static void handleSystemServerProcess(ZygoteConnection.Arguments parsedArgs) {
// 通过反射调用SystemServer的main方法
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> clz = cl.loadClass("com.android.server.SystemServer");
Method m = clz.getMethod("main", new Class[] { String[].class });
m.invoke(null, new Object[] { parsedArgs.remainingArgs });
}
  • ZygoteServer是一个Socket,Zygote进程通过这个Socket和SystemService及其他应用程序进程做通信
  • 通过fork创建的SystemServer进程是一个独立运行的进程,避免SystemServer进程受到其他进程的影响。
  • 关于SystemServer,后面还会更详细的介绍

三、系统服务 System Server

Zygote中通过Zygote.forkSystemServer方法创建了System Server进程,然后通过Java的反射机制调用com.android.server.SystemServermain方法来启动System Server。

Android14的System Server源码地址

3.1 SystemServer.java

SystemServer.javamain方法调用了自身的run方法,在run方法中启动了具体的系统服务,代码如下:

public static void main(String[] args) {
new SystemServer().run();
} private void run() {
// 初始化系统属性,时区、语言、环境等,代码略
...
// 加载本地服务
System.loadLibrary("android_servers");
...
// 初始化系统上下文
createSystemContext();
// 初始化主线模块
ActivityThread.initializeMainlineModules();
...
// 创建系统服务管理器
mSystemServiceManager = new SystemServiceManager(mSystemContext);
... /* 启动系统服务 */
// 启动引导服务
startBootstrapServices(t);
// 启动核心服务
startCoreServices(t);
// 启动其他服务
startOtherServices(t);
// 启动 APEX 服务
startApexServices(t);
...
}

3.2 System Server启动的主要服务

以下为System Server启动的主要服务列表,具体实现可在源码中查看。

服务名称 功能说明
Activity Manager Service (AMS) 管理应用程序的生命周期,包括启动和停止应用、管理任务和活动栈、处理广播等
Package Manager Service (PMS) 管理应用包的安装、卸载、更新、权限分配等
System Config Service 管理系统配置和资源
Power Manager Service 管理设备的电源状态和电源策略,如休眠、唤醒等
Display Manager Service 管理显示设备,如屏幕亮度、显示模式等
User Manager Service 管理用户账户和用户信息
Battery Service 监控和管理电池状态和电池使用情况
Vibrator Service 控制设备的振动功能
Sensor Service 管理设备的传感器,如加速度计、陀螺仪等
Window Manager Service (WMS) 管理窗口和显示内容,包括窗口的创建、删除、布局等
Input Manager Service 管理输入设备,如触摸屏、键盘等
Alarm Manager Service 提供定时任务调度功能
Connectivity Service 管理网络连接,如 Wi-Fi、移动数据等
Network Management Service 管理网络接口和网络连接
Telephony Registry 管理电话和短信服务
Input Method Manager Service (IMMS) 管理输入法框架
Accessibility Manager Service 管理无障碍服务,为有特殊需要的用户提供辅助功能
Mount Service 管理存储设备的挂载和卸载
Location Manager Service 管理位置服务,如 GPS 和网络定位
Search Manager Service 管理系统搜索功能
Clipboard Service 管理剪贴板功能
DevicePolicy Manager Service 管理设备的安全策略和企业管理功能
Status Bar Service 管理状态栏显示和操作
Wallpaper Manager Service 管理壁纸设置和操作
Media Router Service 管理媒体设备路由

在系统服务全部启动完成后,就开始启动系统桌面程序Launcher了。

四、桌面程序Launcher

sequenceDiagram
participant SystemServer
participant ActivityManagerService
participant ActivityTaskManagerService
participant RootWindowContainer
participant ActivityStartController
participant Home

SystemServer->>ActivityManagerService: systemReady()
ActivityManagerService->>ActivityTaskManagerService: startHomeOnAllDisplays
ActivityTaskManagerService->>RootWindowContainer: startHomeOnAllDisplays
RootWindowContainer->>ActivityStartController: startHomeActivity
ActivityStartController->>Home: Home application (Launcher) is started

  • SystemServer 启动所有服务: SystemServer类在run方法中调用startOtherServices方法,启动其他系统服务,包括ActivityManagerService。
  • ActivityManagerService准备系统:ActivityManagerService 在systemReady方法中调用mAtmInternal.startHomeOnAllDisplays方法,开始在所有显示器上启动桌面程序。
  • ActivityTaskManagerService启动 Home Activity:ActivityTaskManagerService 调用RootWindowContainer的startHomeOnAllDisplays方法。
  • RootWindowContainer循环所有显示器:RootWindowContainer 遍历每个显示器,并调用startHomeOnDisplay方法。
  • 启动Home Activity:在每个显示器上,通过TaskDisplayArea调用ActivityStartController的startHomeActivity方法,最终调用ActivityManagerService的startActivity方法启动Home Activity。
  • Home应用启动:ActivityManagerService处理启动请求,启动Home应用的Activity展示桌面界面。

核心代码流转:

4.1 SystemServer.java

Android14的System Server源码地址

private void run() {
...
// 启动其他服务
startOtherServices(t);
...
} private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
...
// 启动ActivityManagerService
mActivityManagerService = mSystemServiceManager.startService(
ActivityManagerService.Lifecycle.class).getService();
...
// 启动Launcher
mActivityManagerService.systemReady(...)
...
}

4.2 桌面程序Launcher(Home)的启动流程

4.2.1 ActivityManagerService.java

Android14的ActivityManagerService源码地址

public void systemReady(final Runnable goingCallback, TimingsTraceAndSlog t) {
...
// 在所有显示器上启动Launcher
mAtmInternal.startHomeOnAllDisplays(currentUserId, "systemReady");
...
}
  • 此行代码最终会调用到ActivityTaskManagerService.javastartHomeOnAllDisplays方法

4.2.2 ActivityTaskManagerService.java

Android14的ActivityTaskManagerService源码地址

void startHomeOnAllDisplays(int userId, String reason) {
synchronized (mGlobalLock) {
// 在所有显示器上启动Launcher
return mRootWindowContainer.startHomeOnAllDisplays(userId, reason);
}
}
  • 此行代码最终会调用到RootWindowContainer.javastartHomeOnAllDisplays方法

4.2.3 RootWindowContainer.java

Android14的RootWindowContainer源码地址


boolean startHomeOnAllDisplays(int userId, String reason) {
boolean homeStarted = false;
for (int i = getChildCount() - 1; i >= 0; i--) {
final int displayId = getChildAt(i).mDisplayId;
// 在每一个显示器上启动桌面程序
homeStarted |= startHomeOnDisplay(userId, reason, displayId);
}
return homeStarted;
} boolean startHomeOnDisplay(int userId, String reason, int displayId) {
return startHomeOnDisplay(userId, reason, displayId, false, false);
} boolean startHomeOnDisplay(int userId, String reason, int displayId, boolean allowInstrumenting, boolean fromHomeKey) {
...
// 调用startHomeOnTaskDisplayArea
return display.reduceOnAllTaskDisplayAreas((taskDisplayArea, result) ->
result | startHomeOnTaskDisplayArea(userId, reason, taskDisplayArea,
allowInstrumenting, fromHomeKey),false);
} boolean startHomeOnTaskDisplayArea(int userId, String reason, TaskDisplayArea taskDisplayArea,
boolean allowInstrumenting, boolean fromHomeKey) {
...
Intent homeIntent = mService.getHomeIntent();
mService.getActivityStartController().startHomeActivity(homeIntent, aInfo, myReason,
taskDisplayArea);
...
return true;
}
  • 最终通过Intent和startHomeActivity方法启动了桌面程序

五、总结

通过上述介绍,从用户按下电源键开始,经过Bootloader启动、内核启动、init进程启动、Zygote进程启动、SystemServer进程启动,以及系统应用的启动,最终进入桌面环境。

每个阶段都的核心工作:

  1. Bootloader启动:初始化硬件并加载内核。

  2. 内核启动:内核是操作系统的核心,负责管理系统资源和硬件设备。

  3. init进程启动:init进程通过解析init.rc文件来启动和配置系统服务。

  4. Zygote进程启动:Zygote是Android系统的独有设计,负责创建应用进程。通过预加载资源和共享内存,Zygote大大提高了应用启动的速度和系统资源的利用率。

  5. SystemServer进程启动:SystemServer进程启动了大量系统服务,如Activity Manager和Package Manager等,这些服务构成了Android系统的骨干,管理和协调应用的运行。

  6. 启动系统应用:Launcher应用的启动标志着系统启动的完成。用户进入桌面,可以开始正常使用设备。

通过深入理解Android的启动流程,可以更有效地进行系统开发和维护,从而提供更高性能和更稳定的用户体验。

如有任何疑问或建议,欢迎留言讨论。

Android启动过程-万字长文(Android14)的更多相关文章

  1. Android 核心分析 之八Android 启动过程详解

    Android 启动过程详解 Android从Linux系统启动有4个步骤: (1) init进程启动 (2) Native服务启动 (3) System Server,Android服务启动 (4) ...

  2. Android启动过程以及各个镜像的关系

    Android启动过程 Android在启动的时候,会由UBOOT传入一个init参数,这个init参数指定了开机的时候第一个运行的程序,默认就是init程序,这个程序在ramdisk.img中.可以 ...

  3. Android(java)学习笔记162:Android启动过程(转载)

    转载路径为: http://blog.jobbole.com/67931/ 1. 关于Android启动过程的问题: 当按下Android设备电源键时究竟发生了什么? Android的启动过程是怎么样 ...

  4. Android启动过程深入解析

    本文由 伯乐在线 - 云海之巅 翻译.未经许可,禁止转载!英文出处:kpbird.欢迎加入翻译小组. 当按下Android设备电源键时究竟发生了什么? Android的启动过程是怎么样的? 什么是Li ...

  5. Android启动过程深入解析【转】

    转自:http://www.open-open.com/lib/view/open1403250347934.html 当按下Android设备电源键时究竟发生了什么? Android的启动过程是怎么 ...

  6. Android(java)学习笔记105:Android启动过程(转载)

    转载路径为: http://blog.jobbole.com/67931/ 1. 关于Android启动过程的问题: 当按下Android设备电源键时究竟发生了什么? Android的启动过程是怎么样 ...

  7. Android 启动过程简析

    首先我们先来看android构架图: android系统是构建在linux系统上面的. 所以android设备启动经历3个过程. Boot Loader,Linux Kernel & Andr ...

  8. Android 启动过程总结

    SystemServer的启动 frameworks/base/services/java/com/android/server/SystemServer.java: run() 其中调用Activi ...

  9. Android 启动过程的底层实现

    转载请标明出处:  http://blog.csdn.net/yujun411522/article/details/46367787 本文出自:[yujun411522的博客] 3.1 androi ...

  10. 从Linux启动过程到android启动过程

    Linux启动过程: 1.首先开机给系统供电,此时硬件电路会产生一个确定的复位时序,保证cpu是最后一个被复位的器件.为什么cpu要最后被复位呢?因为 如果cpu第一个被复位,则当cpu复位后开始运行 ...

随机推荐

  1. C#/.NET/.NET Core拾遗补漏合集(24年4月更新)

    前言 在这个快速发展的技术世界中,时常会有一些重要的知识点.信息或细节被忽略或遗漏.<C#/.NET/.NET Core拾遗补漏>专栏我们将探讨一些可能被忽略或遗漏的重要知识点.信息或细节 ...

  2. k8s 深入篇———— docker 镜像是什么[二]

    前言 简单介绍一下docker的镜像. 正文 前面讲到了容器的工作原理了(namespace 限制了时间, cgroup限制了资源),知道docker 历史的也知道,docker 之所以能够称为容器大 ...

  3. JS - JavaScript 主要知识点(基础夯实)

    纲要 基本类型和引用类型 类型判断 强制类型转换 作用域 执行上下文 理解函数的执行过程 this 指向 闭包 原型和原型链 js 的继承 event loop 基本类型和引用类型 js中数据类型分为 ...

  4. JDBC数据库汇总Attack研究

    前言 针对除Mysql的其它数据库的jdbc attack分析 H2 RCE 介绍 H2 是一个用 Java 开发的嵌入式数据库,它本身只是一个类库,即只有一个 jar 文件,可以直接嵌入到应用项目中 ...

  5. 力扣374(java&python)-猜数字大小(简单)

    题目: 猜数字游戏的规则如下: 每轮游戏,我都会从 1 到 n 随机选择一个数字. 请你猜选出的是哪个数字.如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了.你可以通过调用一个预先定 ...

  6. 从MVC到云原生:CBU研发体系演进之路

    简介: 本文对过去十年 CBU 在研发方式和技术架构上的探索做一个简要的回顾总结,以及对未来的展望. 前言 CBU作为集团内最早成立的几个BU之一,有着多年丰富的业务沉淀,而CBU的技术也伴随着业务一 ...

  7. TSDB时序数据库时序数据压缩解压技术浅析

    ​简介: 目前,物联网.工业互联网.车联网等智能互联技术在各个行业场景下快速普及应用,导致联网传感器.智能设备数量急剧增加,随之而来的海量时序监控数据存储.处理问题,也为时序数据库高效压缩.存储数据能 ...

  8. 利用Navicat的历史日志查询表的索引信息(还可以查询很多系统级别的信息)

    1.使用前提 所有的能用Navicat连接的数据库都可以使用这个方法 DDL/DML语句都有 2.Navicat中的历史日志 3.比如查询mysql的表的索引 先打开"历史记录" ...

  9. Windows下绑定线程到指定的CPU核心

    在某些场景下,需要把程序绑定到指定CPU核心提高执行效率.通过微软官方文档查询到Windows提供了两个Win32函数:SetThreadAffinityMask和SetProcessAffinity ...

  10. Kimi:文本解析利器,你相信光么?

    缘起 第一次接触 kimi 是在微信群,开始以为是推广薅羊毛产品,后来在其他渠道也了解到 kimi,据说是"国产之光".我知道很多同学苦不能使用魔法久矣,索性就先踩踩这个" ...