前言

我们知道当startActivity的时候,除了创建Activity相关实体,系统会根据需要是否创建进程,此进程就是指的Linux层面的进程实体;后面会说到,其实创建的过程是fork,意为从父进程“copy”出一个新的进程。此过程必定经历启动者、ams、zygote等相互的进程间通信,本文主要梳理ams在此过程的重要环节,涉及虚拟机的继承、Binder线程池、消息循环是怎么建立的等等,而Activity的启动细节则放在其他章节另作探讨。

步骤

step1 Ams发起请求startProcessLocked

ActivityManagerService.java

直接从Ams中向zygote发起请求的入口开始分析,当然真正的开始是Ams收到其他应用的Binder请求,所以应该在onTransact里面才是真正的开始,待会儿会打出trace看。(这儿一个小细节,源码中有很多方法带Locked后缀,说明使用该方法需要加锁,因为其是非线程安全的)。

            // Start the process.  It will either succeed and return a result containing
// the PID of the new process, or else throw a RuntimeException.
boolean isActivityProcess = (entryPoint == null);
if (entryPoint == null) entryPoint = "android.app.ActivityThread";
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +
app.processName);
checkTime(startTime, "startProcess: asking zygote to start proc");
Process.ProcessStartResult startResult = Process.start(entryPoint,
app.processName, uid, uid, gids, debugFlags, mountExternal,
app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet,
app.info.dataDir, entryPointArgs);
checkTime(startTime, "startProcess: returned from zygote!");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

比较重要的是这一段话,调用Process类的start方法,并指定了入口类为ActivityThread。

system    671   641   2348412 179992          0 0000000000 S Binder:641_1

641-671/system_process W:     at android.os.Process.zygoteSendArgsAndGetResult(Process.java:605)
641-671/system_process W: at android.os.Process.startViaZygote(Process.java:738)
641-671/system_process W: at android.os.Process.start(Process.java:521)
641-671/system_process W: at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:4214)
641-671/system_process W: at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:4069)
641-671/system_process W: at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:3909)
641-671/system_process W: at com.android.server.am.ActivityStackSupervisor.startSpecificActivityLocked(ActivityStackSupervisor.java:1441)
641-671/system_process W: at com.android.server.am.ActivityStack.resumeTopActivityInnerLocked(ActivityStack.java:2741)
641-671/system_process W: at com.android.server.am.ActivityStack.resumeTopActivityUncheckedLocked(ActivityStack.java:2213)
641-671/system_process W: at com.android.server.am.ActivityStackSupervisor.resumeFocusedStackTopActivityLocked(ActivityStackSupervisor.java:1859)
641-671/system_process W: at com.android.server.am.ActivityStack.completePauseLocked(ActivityStack.java:1393)
641-671/system_process W: at com.android.server.am.ActivityStack.activityPausedLocked(ActivityStack.java:1237)
641-671/system_process W: at com.android.server.am.ActivityManagerService.activityPaused(ActivityManagerService.java:7393)
641-671/system_process W: at android.app.ActivityManagerNative.onTransact(ActivityManagerNative.java:571)
641-671/system_process W: at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3169)
641-671/system_process W: at android.os.Binder.execTransact(Binder.java:565)

从如上的trace可以直接看出,在收到startActivity的请求后,执行了对当前Activity的pause操作,然后通过Process.java这个类对Zygote进程发起请求。注意,通过对线程号的观察可以看到,这一切都在一次binder线程中操作的。

注意Ams与Zygote之间是通过socket连接的,封装了zygoteState类去做通信相关的任务。从下图fd的占用可以明确地看到,zygote进程是不使用binder的。

device:/proc/341/fd # ls -l
ls -l
total 0
lrwx------ 1 root root 64 2021-03-16 17:49 0 -> /dev/null
lrwx------ 1 root root 64 2021-03-16 17:49 1 -> /dev/null
lr-x------ 1 root root 64 2021-03-16 17:49 10 -> /system/framework/core-oj.jar
lr-x------ 1 root root 64 2021-03-16 17:49 11 -> /system/framework/core-libart.jar
lr-x------ 1 root root 64 2021-03-16 17:49 12 -> /system/framework/conscrypt.jar
lr-x------ 1 root root 64 2021-03-16 17:49 13 -> /system/framework/okhttp.jar
lr-x------ 1 root root 64 2021-03-16 17:49 14 -> /system/framework/core-junit.jar
lr-x------ 1 root root 64 2021-03-16 17:49 15 -> /system/framework/bouncycastle.jar
lr-x------ 1 root root 64 2021-03-16 17:49 16 -> /system/framework/ext.jar
lr-x------ 1 root root 64 2021-03-16 17:49 17 -> /system/framework/framework.jar
lr-x------ 1 root root 64 2021-03-16 17:49 18 -> /system/framework/telephony-common.jar
lr-x------ 1 root root 64 2021-03-16 17:49 19 -> /system/framework/voip-common.jar
lrwx------ 1 root root 64 2021-03-16 17:49 2 -> /dev/null
lr-x------ 1 root root 64 2021-03-16 17:49 20 -> /system/framework/ims-common.jar
lr-x------ 1 root root 64 2021-03-16 17:49 21 -> /system/framework/apache-xml.jar
lr-x------ 1 root root 64 2021-03-16 17:49 22 -> /system/framework/org.apache.http.legacy.boot.jar
lrwx------ 1 root root 64 2021-03-16 17:49 23 -> socket:[9842]
lr-x------ 1 root root 64 2021-03-16 17:49 24 -> /system/framework/framework-res.apk
lr-x------ 1 root root 64 2021-03-16 17:49 25 -> /dev/urandom
l-wx------ 1 root root 64 2021-03-16 17:49 5 -> /sys/kernel/debug/tracing/trace_marker
lrwx------ 1 root root 64 2021-03-16 17:49 6 -> /dev/null
lrwx------ 1 root root 64 2021-03-16 17:49 7 -> /dev/null
lrwx------ 1 root root 64 2021-03-16 17:49 8 -> /dev/null
lrwx------ 1 root root 64 2021-03-16 17:49 9 -> socket:[9289]

Ams的调用栈到上面就截止了,接下来进入到Zygote进程中。

step2 Zygote收到请求

猜想Zygote进程初始化完成后,一定有个无限循环在等待其他进程的请求,直接定位到代码。

 private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {
ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>(); fds.add(sServerSocket.getFileDescriptor());
peers.add(null); while (true) {
StructPollfd[] pollFds = new StructPollfd[fds.size()];
for (int i = 0; i < pollFds.length; ++i) {
pollFds[i] = new StructPollfd();
pollFds[i].fd = fds.get(i);
pollFds[i].events = (short) POLLIN;
}
try {
Os.poll(pollFds, -1);
} catch (ErrnoException ex) {
throw new RuntimeException("poll failed", ex);
}
for (int i = pollFds.length - 1; i >= 0; --i) {
if ((pollFds[i].revents & POLLIN) == 0) {
continue;
}
if (i == 0) {
ZygoteConnection newPeer = acceptCommandPeer(abiList);
peers.add(newPeer);
fds.add(newPeer.getFileDesciptor());
} else {
boolean done = peers.get(i).runOnce();
if (done) {
peers.remove(i);
fds.remove(i);
}
}
}
}
}

可以看到,当接收到请求后,调用ZygoteConnection类的runOnce来处理。

 boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {

        .......省略代码

        pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet,
parsedArgs.appDataDir); ......省略代码 try {
/*子进程执行pid==0情况,父进程执行else情况*/
if (pid == 0) {
/*子进程*/
// in child
IoUtils.closeQuietly(serverPipeFd);
serverPipeFd = null;
handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr); // should never get here, the child is expected to either
// throw ZygoteInit.MethodAndArgsCaller or exec().
return true;
} else {
// in parent...pid of < 0 means failure
IoUtils.closeQuietly(childPipeFd);
childPipeFd = null;
/*Process中的io流监听的pid等信息都是通过下面的代码发出去的*/
return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs);
}
} finally {
IoUtils.closeQuietly(childPipeFd);
IoUtils.closeQuietly(serverPipeFd);
}
}

主要玄机是forkAndSpecialize这个方法,能fork出native的子进程,然后再返回到调用的地方;但是会返回两次,一次是子进程,一次是父进程,这个通过返回值来判断,和linux的fork方法类似;如果是子进程,那么执行handleChildProc,注意下面的注释,意味着不会执行到return的语句,也就是上层的selectloop(因为子进程不用管zygote的接受命令循环了,直接去执行app进程的任务了);而父进程会调用handleParentProc后返回。

子进程的返回有个小技巧,是通过抛一个异常达到的,那么回到了哪里?我们再看一下ZygoteInit.java的main方法。

    public static void main(String argv[]) {
// Mark zygote start. This ensures that thread creation will throw
// an error.
...
try {
Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ZygoteInit");
RuntimeInit.enableDdms();
// Start profiling the zygote initialization.
SamplingProfilerIntegration.start();
registerZygoteSocket(socketName);
preload();
gcAndFinalize();
// Zygote process unmounts root storage spaces.
Zygote.nativeUnmountStorageOnInit();
...
Log.i(TAG, "Accepting command socket connections");
/*这儿调用的runSelectLoop开启等待循环的*/
runSelectLoop(abiList); closeServerSocket();
} catch (MethodAndArgsCaller caller) {
caller.run();
} catch (Throwable ex) {
Log.e(TAG, "Zygote died with exception", ex);
closeServerSocket();
throw ex;
}
}

是不是看到了熟悉的身影runSelectLoop,所以是Zygote进入java世界后,先进入main方法做了一些准备工作,然后开启selectLoop进行等待循环,而收到子进程抛出的MethodAndArgsCaller异常后会执行caller.run,这是我们能从调用栈中看到子进程生命的开始,之所以这样做,还可以通过抛异常的方式清空之前的调用栈,去除一些fork、设置等,看起来app的生命更清爽。如下图到onCreate的流程。

3199-3199/com.android.myapplication W:     at com.android.myapplication.MainActivity.onCreate(MainActivity.java:42)
3199-3199/com.android.myapplication W: at android.app.Activity.performCreate(Activity.java:6709)
3199-3199/com.android.myapplication W: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
3199-3199/com.android.myapplication W: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2624)
3199-3199/com.android.myapplication W: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2732)
3199-3199/com.android.myapplication W: at android.app.ActivityThread.-wrap12(ActivityThread.java)
3199-3199/com.android.myapplication W: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1483)
3199-3199/com.android.myapplication W: at android.os.Handler.dispatchMessage(Handler.java:102)
3199-3199/com.android.myapplication W: at android.os.Looper.loop(Looper.java:154)
3199-3199/com.android.myapplication W: at android.app.ActivityThread.main(ActivityThread.java:6141)
3199-3199/com.android.myapplication W: at java.lang.reflect.Method.invoke(Native Method)
3199-3199/com.android.myapplication W: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:912)
3199-3199/com.android.myapplication W: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:802)

哈哈,扯远了,继续看一下刚才子进程调用的handleChildProc到底做了什么事儿。

step3 handleChildProc -- 进入子进程的世界

.frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java

按照惯例,先看调用栈。

private void handleChildProc(Arguments parsedArgs,
FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr)
throws ZygoteInit.MethodAndArgsCaller { closeSocket();
ZygoteInit.closeServerSocket(); if (descriptors != null) {
try {
Os.dup2(descriptors[0], STDIN_FILENO);
Os.dup2(descriptors[1], STDOUT_FILENO);
Os.dup2(descriptors[2], STDERR_FILENO); for (FileDescriptor fd: descriptors) {
IoUtils.closeQuietly(fd);
}
newStderr = System.err;
} catch (ErrnoException ex) {
Log.e(TAG, "Error reopening stdio", ex);
}
}
......省略代码 if (parsedArgs.runtimeInit) {
if (parsedArgs.invokeWith != null) {
WrapperInit.execApplication(parsedArgs.invokeWith,
parsedArgs.niceName, parsedArgs.targetSdkVersion,
pipeFd, parsedArgs.remainingArgs);
} else {
RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion,
parsedArgs.remainingArgs, null /* classLoader */);
}
} else {
......省略代码
}
}

一开始进来有一些资源回收的工作,比如关闭socket等,因为子进程用不到了,避免占用fd。dup2那三个操作是把原来的标准输入、标准输出和错误输出(fd分别为0、1、2)用参数传来的descriptors代替,实际上传来的就是“/dev/null”,可以随便进入一个app的fd可以看到0、1、2一定是/dev/null。接下来,Ams传来的参数中有“--runtime-init”并且invokeWith的是ActivityThead,然后执行RuntimeInit.zygoteInit这个方法。

step4 RuntimeInit.zygoteInit -- 子进程环境准备

先看代码

    public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
throws ZygoteInit.MethodAndArgsCaller {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application from zygote"); Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "RuntimeInit");
redirectLogStreams();
/*设置时区、log、http代理等一些信息*/
commonInit();
nativeZygoteInit();
applicationInit(targetSdkVersion, argv, classLoader);
}

commonInit是一些通用的初始化,不用去深究,下面重点看一下nativeZygoteInit(比较重要的方法一般都是放在native去干的)和applicationInit。

nativeZygoteInit

通过一系列继承、重写的技巧,总之最后调用到了app_main.cpp中

.frameworks/base/cmds/app_process/app_main.cpp

virtual void onZygoteInit()
{
sp<ProcessState> proc = ProcessState::self();
ALOGV("App process: starting thread pool.\n");
proc->startThreadPool();
}

如果你熟悉native binder进程的写法的话,对这两句话一定不陌生,又看到了熟悉的套路。这两句话在app启动之前帮忙打开了/dev/binder,同时启动了binder线程池。之后app在使用binder时就不用关心binder驱动以及线程池相关的事儿了。

applicationInit

private static void invokeStaticMain(String className, String[] argv, ClassLoader classLoader)
throws ZygoteInit.MethodAndArgsCaller {
Class<?> cl; try {
cl = Class.forName(className, true, classLoader);
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
"Missing class when invoking static main " + className,
ex);
} Method m;
try {
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
throw new RuntimeException(
"Missing static main on " + className, ex);
} catch (SecurityException ex) {
throw new RuntimeException(
"Problem getting static main on " + className, ex);
} int modifiers = m.getModifiers();
if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {
throw new RuntimeException(
"Main method is not public and static on " + className);
} /*
* 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.
*/
throw new ZygoteInit.MethodAndArgsCaller(m, argv);
}

程序比较简单,主要是通过反射的方式找到ActivityThread的main方法,并且抛出异常把该方法作为参数传递到zygote的main方法中去(前面讲了zygote的main方法除了开启selectLoop还捕获了MethodAndArgsCaller这个异常)。

    /**
* Helper exception 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.
*/
public static class MethodAndArgsCaller extends Exception
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) {
throw new RuntimeException(ex);
} catch (InvocationTargetException ex) {
Throwable cause = ex.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException(ex);
}
}
}

MethodAndArgsCaller的run方法很简单,其实就是去执行ActivityThread的main方法。现在我们来捋一下,zygote饶了这么大一圈,就是为了fork出子进程,做一些binder线程池等的准备工作,最后执行ActivityThread的main方法。

通过调用栈来总结一下子进程创建生命的过程

1896-1896/com.android.myapplication W:     at com.android.internal.os.RuntimeInit.invokeStaticMain(RuntimeInit.java:237)
1896-1896/com.android.myapplication W: at com.android.internal.os.RuntimeInit.applicationInit(RuntimeInit.java:338)
1896-1896/com.android.myapplication W: at com.android.internal.os.RuntimeInit.zygoteInit(RuntimeInit.java:290)
1896-1896/com.android.myapplication W: at com.android.internal.os.ZygoteConnection.handleChildProc(ZygoteConnection.java:757)
1896-1896/com.android.myapplication W: at com.android.internal.os.ZygoteConnection.runOnce(ZygoteConnection.java:243)
1896-1896/com.android.myapplication W: at com.android.internal.os.ZygoteInit.runSelectLoop(ZygoteInit.java:876)
1896-1896/com.android.myapplication W: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:798)

注意虽然看pid这是子进程的pid号,但是继承了父进程的调用栈,从handleChildProc开始是子进程,之前是父进程zygote。

step5 ActivityThread -- app的入口

.frameworks/base/core/java/android/app/ActivityThread.java

这次我们主要看一下ActivityThread的静态main方法,因为整个文件有6000多行,关系到Activity实例的各种生命周期和管理、以及viewroot的创建等等。

    public static void main(String[] args) {
...
Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread();
thread.attach(false); if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
} // End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");
}

主要是调用Looper的静态方法创建消息循环,然后创建ActivityThread实例,最后进入到消息循环中。

总结

通过本文大致理了从Ams开始创建一个app进程的大致流程,如果以执行的进程实体来看,其实主要分三个部分,一个是ams中,一个是zygote父进程中,一个是子进程中。有了这个大致的脉络,再根据调用栈来看,实际还是比较清晰的。ams负责接收binder请求并与zygote联系,zygote父进程收到socket消息后去fork出子进程,子进程负责准备好binder环境和消息循环,然后开始Activity的生命周期。

Android应用程序的进程创建过程的更多相关文章

  1. Android应用程序内部启动Activity过程(startActivity)的源代码分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6703247 上文介绍了Android应用程序的 ...

  2. Android应用程序的Activity启动过程简要介绍和学习计划

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6685853 在Android系统中,Activ ...

  3. linux内核学习之六 进程创建过程学习

    一 关于linux进程概念的补充 关于进程的基本概念这里不多说,把自己的学习所得作一些补充: 1. 在linux内核中,系统最多可以有64个进程同时存在. 2.linux进程包含的关键要素:一段可执行 ...

  4. Android MediaPlayer架构 -- MediaPlayer的创建过程

    本文系作者自己学习之所用,文章内容仅出自作者拙劣之思考,问题之处烦请不吝指教. MediaPlayer 能被用来控制音/视频文件或流媒体的回放.Android中以MediaPlayer类作为音视频播放 ...

  5. 进程创建过程详解 CreateProcess

    转载请您注明出处:http://www.cnblogs.com/lsh123/p/7405796.html 0x01 CreateProcessW CreateProcess的使用有ANSI版本的Cr ...

  6. Win32之进程创建过程

    0x01. 什么是进程? 进程提供程序所需要的资源,如:数据.代码等等 进程扮演的角色仅仅是为当前程序提供资源,或者代码,这就是进程所提供的,当时程序运行的状态和进程没有关系,进程可以看做空间的概念 ...

  7. Android应用程序与SurfaceFlinger服务之间的共享UI元数据(SharedClient)的创建过程分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/7867340 在前面一篇文章中,我们分析了And ...

  8. Android应用程序窗口(Activity)的窗口对象(Window)的创建过程分析(转)

    在前文中,我们分析了Android应用程序窗口的运行上下文环境的创建过程.由此可知,每一个Activity组件都有一个关联的ContextImpl对象,同时,它还关联有一个Window对象,用来描述一 ...

  9. Android应用程序窗口(Activity)的窗口对象(Window) 的创建过程分析

    每一个Activity组件都有一个关联的ContextImpl对象,同时,它还关联有一个Window对象,用来描述一个具体的应用程序窗口. 每一个Activity组件都有一个关联的ContextImp ...

随机推荐

  1. 大数据开发-linux后台运行,关闭,查看后台任务

    在日常开发过程中,除了例行调度的任务和直接在开发环境下比如Scripts,开发,很多情况下是shell下直接搞起(小公司一般是这样),看一下常见的linux后台运行和关闭的命令,这里做一个总结,主要包 ...

  2. 网易吃鸡 mac 版,没有声音

    网易吃鸡 mac 版,没有声音 bug 声音太小了 客服电话 问题反馈 提交工单 https://gm.163.com/user_help.html?index=5&stypeid=3619 ...

  3. JavaScript var, let, const difference All In One

    JavaScript var, let, const difference All In One js var, let, const 区别 All In One 是否存在 hoisting var ...

  4. JS Object Deep Copy & 深拷贝

    JS Object Deep Copy & 深拷贝 针对深度拷贝,需要使用其他方法 JSON.parse(JSON.stringify(obj));,因为 Object.assign() 拷贝 ...

  5. JavaScript getter and setter All In One

    JavaScript getter and setter All In One getter & setter JavaScript Object Accessors JavaScript A ...

  6. TypeScript 3.7 RC & Nullish Coalescing

    TypeScript 3.7 RC & Nullish Coalescing null, undefined default value https://devblogs.microsoft. ...

  7. 从微信小程序到鸿蒙js开发【06】——swiper&animator&marquee

    目录: 1.swiper轮播图 2.image-animator幻灯片 3.marquee跑马灯 4.nginx动静分离 1.swiper轮播图 微信小程序的swiper组件中只能放置swiper-i ...

  8. 别找了,这可能是全网最全的鸿蒙(OpenHarmony)刷机指南

    目录: 1. 配置编译环境 2. 编译HarmonyOS源代码 3. 烧录HarmonyOS 4.下载文中资源 5.作者文章合集 摘要:相信很多同学都玩过鸿蒙(HarmonyOS)了,不过估计大多数同 ...

  9. 1020 Tree Traversals——PAT甲级真题

    1020 Tree Traversals Suppose that all the keys in a binary tree are distinct positive integers. Give ...

  10. 权限管理整合springsecurity代码执行过程

    (1)输入用户名密码. (2)进入认证过滤器中,执行attemptAuthentication方法, 通过该方法获取输入的用户名和密码. (3)执行实现了UserDetailsService接口的类中 ...