本文基于 Android 9.0 , 代码仓库地址 : android_9.0.0_r45

文中源码链接:

SystemServer.java

ActivityManagerService.java

Process.java

ZygoteProcess.java

ZygoteSystemServer 启动流程还不熟悉的建议阅读下面两篇文章:

Java 世界的盘古和女娲 —— Zygote

Zygote 家的大儿子 —— SystemServer

Zygote 作为 Android 世界的受精卵,在成功繁殖出 system_server 进程之后并没有完全功成身退,仍然承担着受精卵的责任。Zygote 通过调用其持有的 ZygoteServer 对象的 runSelectLoop() 方法开始等待客户端的呼唤,有求必应。客户端的请求无非是创建应用进程,以 startActivity() 为例,假如开启的是一个尚未创建进程的应用,那么就会向 Zygote 请求创建进程。下面将从 客户端发送请求服务端处理请求 两方面来进行解析。

客户端发送请求

startActivity() 的具体流程这里就不分析了,系列后续文章会写到。我们直接看到创建进程的 startProcess() 方法,该方法在 ActivityManagerService 中,后面简称 AMS

Process.startProcess()

> ActivityManagerService.java

private ProcessStartResult startProcess(String hostingType, String entryPoint,
ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
String seInfo, String requiredAbi, String instructionSet, String invokeWith,
long startTime) {
try {
checkTime(startTime, "startProcess: asking zygote to start proc");
final ProcessStartResult startResult;
if (hostingType.equals("webview_service")) {
startResult = startWebView(entryPoint,
app.processName, uid, uid, gids, runtimeFlags, mountExternal,
app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
app.info.dataDir, null,
new String[] {PROC_START_SEQ_IDENT + app.startSeq});
} else {
// 新建进程
startResult = Process.start(entryPoint,
app.processName, uid, uid, gids, runtimeFlags, mountExternal,
app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
app.info.dataDir, invokeWith,
new String[] {PROC_START_SEQ_IDENT + app.startSeq});
}
checkTime(startTime, "startProcess: returned from zygote!");
return startResult;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
}

调用 Process.start() 方法新建进程,继续追进去:

> Process.java

public static final ProcessStartResult start(
// android.app.ActivityThread,创建进程后会调用其 main() 方法
final String processClass,
final String niceName, // 进程名
int uid, int gid, int[] gids,
int runtimeFlags, int mountExternal,
int targetSdkVersion,
String seInfo,
String abi,
String instructionSet,
String appDataDir,
String invokeWith, // 一般新建应用进程时,此参数不为 null
String[] zygoteArgs) {
return zygoteProcess.start(processClass, niceName, uid, gid, gids,
runtimeFlags, mountExternal, targetSdkVersion, seInfo,
abi, instructionSet, appDataDir, invokeWith, zygoteArgs);
}

继续调用 zygoteProcess.start()

> ZygoteProess.java

public final Process.ProcessStartResult start(final String processClass,
final String niceName,
int uid, int gid, int[] gids,
int runtimeFlags, int mountExternal,
int targetSdkVersion,
String seInfo,
String abi,
String instructionSet,
String appDataDir,
String invokeWith,
String[] zygoteArgs) {
try {
return startViaZygote(processClass, niceName, uid, gid, gids,
runtimeFlags, mountExternal, targetSdkVersion, seInfo,
abi, instructionSet, appDataDir, invokeWith, false /* startChildZygote */,
zygoteArgs);
} catch (ZygoteStartFailedEx ex) {
Log.e(LOG_TAG,
"Starting VM process through Zygote failed");
throw new RuntimeException(
"Starting VM process through Zygote failed", ex);
}
}

调用 startViaZygote() 方法。终于看到 Zygote 的身影了。

startViaZygote()

> ZygoteProcess.java

private Process.ProcessStartResult startViaZygote(final String processClass,
final String niceName,
final int uid, final int gid,
final int[] gids,
int runtimeFlags, int mountExternal,
int targetSdkVersion,
String seInfo,
String abi,
String instructionSet,
String appDataDir,
String invokeWith,
boolean startChildZygote, // 是否克隆 zygote 进程的所有状态
String[] extraArgs)
throws ZygoteStartFailedEx {
ArrayList<String> argsForZygote = new ArrayList<String>(); // --runtime-args, --setuid=, --setgid=,
// and --setgroups= must go first
// 处理参数
argsForZygote.add("--runtime-args");
argsForZygote.add("--setuid=" + uid);
argsForZygote.add("--setgid=" + gid);
argsForZygote.add("--runtime-flags=" + runtimeFlags);
if (mountExternal == Zygote.MOUNT_EXTERNAL_DEFAULT) {
argsForZygote.add("--mount-external-default");
} else if (mountExternal == Zygote.MOUNT_EXTERNAL_READ) {
argsForZygote.add("--mount-external-read");
} else if (mountExternal == Zygote.MOUNT_EXTERNAL_WRITE) {
argsForZygote.add("--mount-external-write");
}
argsForZygote.add("--target-sdk-version=" + targetSdkVersion); // --setgroups is a comma-separated list
if (gids != null && gids.length > 0) {
StringBuilder sb = new StringBuilder();
sb.append("--setgroups="); int sz = gids.length;
for (int i = 0; i < sz; i++) {
if (i != 0) {
sb.append(',');
}
sb.append(gids[i]);
} argsForZygote.add(sb.toString());
} if (niceName != null) {
argsForZygote.add("--nice-name=" + niceName);
} if (seInfo != null) {
argsForZygote.add("--seinfo=" + seInfo);
} if (instructionSet != null) {
argsForZygote.add("--instruction-set=" + instructionSet);
} if (appDataDir != null) {
argsForZygote.add("--app-data-dir=" + appDataDir);
} if (invokeWith != null) {
argsForZygote.add("--invoke-with");
argsForZygote.add(invokeWith);
} if (startChildZygote) {
argsForZygote.add("--start-child-zygote");
} argsForZygote.add(processClass); if (extraArgs != null) {
for (String arg : extraArgs) {
argsForZygote.add(arg);
}
} synchronized(mLock) {
// 和 Zygote 进程进行 socket 通信
return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
}
}

前面一大串代码都是在处理参数,大致浏览即可。核心在于最后的 openZygoteSocketIfNeeded()zygoteSendArgsAndGetResult() 这两个方法。从方法命名就可以看出来,这里要和 Zygote 进行 socket 通信了。还记得 ZygoteInit.main() 方法中调用的 registerServerSocketFromEnv() 方法吗?它在 Zygote 进程中创建了服务端 socket。

openZygoteSocketIfNeeded()

先来看看 openZygoteSocketIfNeeded() 方法。

> ZygoteProcess.java

private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
Preconditions.checkState(Thread.holdsLock(mLock), "ZygoteProcess lock not held"); // 未连接或者连接已关闭
if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
try {
// 开启 socket 连接
primaryZygoteState = ZygoteState.connect(mSocket);
} catch (IOException ioe) {
throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
}
maybeSetApiBlacklistExemptions(primaryZygoteState, false);
maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState);
}
if (primaryZygoteState.matches(abi)) {
return primaryZygoteState;
} // 当主 zygote 没有匹配成功,尝试 connect 第二个 zygote
if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) {
try {
secondaryZygoteState = ZygoteState.connect(mSecondarySocket);
} catch (IOException ioe) {
throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
}
maybeSetApiBlacklistExemptions(secondaryZygoteState, false);
maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState);
} if (secondaryZygoteState.matches(abi)) {
return secondaryZygoteState;
} throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi);
}

如果与 Zygote 进程的 socket 连接未开启,则尝试开启,可能会产生阻塞和重试。连接调用的是 ZygoteState.connect() 方法,ZygoteStateZygoteProcess 的内部类。

> ZygoteProcess.java

public static class ZygoteState {
final LocalSocket socket;
final DataInputStream inputStream;
final BufferedWriter writer;
final List<String> abiList; boolean mClosed; private ZygoteState(LocalSocket socket, DataInputStream inputStream,
BufferedWriter writer, List<String> abiList) {
this.socket = socket;
this.inputStream = inputStream;
this.writer = writer;
this.abiList = abiList;
} public static ZygoteState connect(LocalSocketAddress address) throws IOException {
DataInputStream zygoteInputStream = null;
BufferedWriter zygoteWriter = null;
final LocalSocket zygoteSocket = new LocalSocket(); try {
zygoteSocket.connect(address); zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream()); zygoteWriter = new BufferedWriter(new OutputStreamWriter(
zygoteSocket.getOutputStream()), 256);
} catch (IOException ex) {
try {
zygoteSocket.close();
} catch (IOException ignore) {
} throw ex;
} String abiListString = getAbiList(zygoteWriter, zygoteInputStream);
Log.i("Zygote", "Process: zygote socket " + address.getNamespace() + "/"
+ address.getName() + " opened, supported ABIS: " + abiListString); return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter,
Arrays.asList(abiListString.split(",")));
}
...
}

通过 socket 连接 Zygote 远程服务端。

再回头看之前的 zygoteSendArgsAndGetResult() 方法。

zygoteSendArgsAndGetResult()

 > ZygoteProcess.java

private static Process.ProcessStartResult zygoteSendArgsAndGetResult(
ZygoteState zygoteState, ArrayList<String> args)
throws ZygoteStartFailedEx {
try {
...
final BufferedWriter writer = zygoteState.writer;
final DataInputStream inputStream = zygoteState.inputStream; writer.write(Integer.toString(args.size()));
writer.newLine(); // 向 zygote 进程发送参数
for (int i = 0; i < sz; i++) {
String arg = args.get(i);
writer.write(arg);
writer.newLine();
} writer.flush(); // 是不是应该有一个超时时间?
Process.ProcessStartResult result = new Process.ProcessStartResult(); // Always read the entire result from the input stream to avoid leaving
// bytes in the stream for future process starts to accidentally stumble
// upon.
// 读取 zygote 进程返回的子进程 pid
result.pid = inputStream.readInt();
result.usingWrapper = inputStream.readBoolean(); if (result.pid < 0) { // pid 小于 0 ,fork 失败
throw new ZygoteStartFailedEx("fork() failed");
}
return result;
} catch (IOException ex) {
zygoteState.close();
throw new ZygoteStartFailedEx(ex);
}
}

通过 socket 发送请求参数,然后等待 Zygote 进程返回子进程 pid 。客户端的工作到这里就暂时完成了,我们再追踪到服务端,看看服务端是如何处理客户端请求的。

Zygote 处理客户端请求

Zygote 处理客户端请求的代码在 ZygoteServer.runSelectLoop() 方法中。

> ZygoteServer.java

Runnable runSelectLoop(String abiList) {
... while (true) {
...
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 { // 处理客户端请求
try {
ZygoteConnection connection = peers.get(i);
// fork 子进程,并返回包含子进程 main() 函数的 Runnable 对象
final Runnable command = connection.processOneCommand(this); if (mIsForkChild) {
// 位于子进程
if (command == null) {
throw new IllegalStateException("command == null");
} return command;
} else {
// 位于父进程
if (command != null) {
throw new IllegalStateException("command != null");
} if (connection.isClosedByPeer()) {
connection.closeSocket();
peers.remove(i);
fds.remove(i);
}
}
} catch (Exception e) {
...
} finally {
mIsForkChild = false;
}
}
}
}
}

acceptCommandPeer() 方法用来响应新客户端的 socket 连接请求。processOneCommand() 方法用来处理客户端的一般请求。

processOneCommand()

> ZygoteConnection.java

Runnable processOneCommand(ZygoteServer zygoteServer) {
String args[];
Arguments parsedArgs = null;
FileDescriptor[] descriptors; try {
// 1. 读取 socket 客户端发送过来的参数列表
args = readArgumentList();
descriptors = mSocket.getAncillaryFileDescriptors();
} catch (IOException ex) {
throw new IllegalStateException("IOException on command socket", ex);
} ... // 2. fork 子进程
pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
parsedArgs.runtimeFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
parsedArgs.niceName, fdsToClose, fdsToIgnore, parsedArgs.startChildZygote,
parsedArgs.instructionSet, parsedArgs.appDataDir); try {
if (pid == 0) {
// 处于进子进程
zygoteServer.setForkChild();
// 关闭服务端 socket
zygoteServer.closeServerSocket();
IoUtils.closeQuietly(serverPipeFd);
serverPipeFd = null;
// 3. 处理子进程事务
return handleChildProc(parsedArgs, descriptors, childPipeFd,
parsedArgs.startChildZygote);
} else {
// 处于 Zygote 进程
IoUtils.closeQuietly(childPipeFd);
childPipeFd = null;
// 4. 处理父进程事务
handleParentProc(pid, descriptors, serverPipeFd);
return null;
}
} finally {
IoUtils.closeQuietly(childPipeFd);
IoUtils.closeQuietly(serverPipeFd);
}
}

processOneCommand() 方法大致可以分为五步,下面逐步分析。

readArgumentList()

> ZygoteConnection.java

private String[] readArgumentList()
throws IOException { int argc; try {
// 逐行读取参数
String s = mSocketReader.readLine(); if (s == null) {
// EOF reached.
return null;
}
argc = Integer.parseInt(s);
} catch (NumberFormatException ex) {
throw new IOException("invalid wire format");
} // See bug 1092107: large argc can be used for a DOS attack
if (argc > MAX_ZYGOTE_ARGC) {
throw new IOException("max arg count exceeded");
} String[] result = new String[argc];
for (int i = 0; i < argc; i++) {
result[i] = mSocketReader.readLine();
if (result[i] == null) {
// We got an unexpected EOF.
throw new IOException("truncated request");
}
} return result;
}

读取客户端发送过来的请求参数。

forkAndSpecialize()

> Zygote.java

public static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags,
int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose,
int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir) {
VM_HOOKS.preFork();
// Resets nice priority for zygote process.
resetNicePriority();
int pid = nativeForkAndSpecialize(
uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose,
fdsToIgnore, startChildZygote, instructionSet, appDataDir);
// Enable tracing as soon as possible for the child process.
if (pid == 0) {
Trace.setTracingEnabled(true, runtimeFlags); // Note that this event ends at the end of handleChildProc,
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork");
}
VM_HOOKS.postForkCommon();
return pid;
}

nativeForkAndSpecialize() 是一个 native 方法,在底层 fork 了一个新进程,并返回其 pid。不要忘记了这里的 一次fork,两次返回pid > 0 说明还是父进程。pid = 0 说明进入了子进程。子进程中会调用 handleChildProc,而父进程中会调用 handleParentProc()

handleChildProc()

> ZygoteConnection.java

private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,
FileDescriptor pipeFd, boolean isZygote) {
closeSocket(); // 关闭 socket 连接
... if (parsedArgs.niceName != null) {
// 设置进程名
Process.setArgV0(parsedArgs.niceName);
} if (parsedArgs.invokeWith != null) {
WrapperInit.execApplication(parsedArgs.invokeWith,
parsedArgs.niceName, parsedArgs.targetSdkVersion,
VMRuntime.getCurrentInstructionSet(),
pipeFd, parsedArgs.remainingArgs); // Should not get here.
throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
} else {
if (!isZygote) { // 新建应用进程时 isZygote 参数为 false
return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,
null /* classLoader */);
} else {
return ZygoteInit.childZygoteInit(parsedArgs.targetSdkVersion,
parsedArgs.remainingArgs, null /* classLoader */);
}
}
}

当看到 ZygoteInit.zygoteInit() 时你应该感觉很熟悉了,接下来的流程就是:

ZygoteInit.zygoteInit() -> RuntimeInit.applicationInit() -> findStaticMain()

SystemServer 进程的创建流程一致。这里要找的 main 方法就是 ActivityThrad.main()ActivityThread 虽然并不是一个线程,但你可以把它理解为应用的主线程。

handleParentProc()

> ZygoteConnection.java

private void handleParentProc(int pid, FileDescriptor[] descriptors, FileDescriptor pipeFd) {
if (pid > 0) {
setChildPgid(pid);
} if (descriptors != null) {
for (FileDescriptor fd: descriptors) {
IoUtils.closeQuietly(fd);
}
} boolean usingWrapper = false;
if (pipeFd != null && pid > 0) {
int innerPid = -1;
try {
// Do a busy loop here. We can't guarantee that a failure (and thus an exception
// bail) happens in a timely manner.
final int BYTES_REQUIRED = 4; // Bytes in an int. StructPollfd fds[] = new StructPollfd[] {
new StructPollfd()
}; byte data[] = new byte[BYTES_REQUIRED]; int remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS;
int dataIndex = 0;
long startTime = System.nanoTime(); while (dataIndex < data.length && remainingSleepTime > 0) {
fds[0].fd = pipeFd;
fds[0].events = (short) POLLIN;
fds[0].revents = 0;
fds[0].userData = null; int res = android.system.Os.poll(fds, remainingSleepTime);
long endTime = System.nanoTime();
int elapsedTimeMs = (int)((endTime - startTime) / 1000000l);
remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS - elapsedTimeMs; if (res > 0) {
if ((fds[0].revents & POLLIN) != 0) {
// Only read one byte, so as not to block.
int readBytes = android.system.Os.read(pipeFd, data, dataIndex, 1);
if (readBytes < 0) {
throw new RuntimeException("Some error");
}
dataIndex += readBytes;
} else {
// Error case. revents should contain one of the error bits.
break;
}
} else if (res == 0) {
Log.w(TAG, "Timed out waiting for child.");
}
} if (dataIndex == data.length) {
DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
innerPid = is.readInt();
} if (innerPid == -1) {
Log.w(TAG, "Error reading pid from wrapped process, child may have died");
}
} catch (Exception ex) {
Log.w(TAG, "Error reading pid from wrapped process, child may have died", ex);
} // Ensure that the pid reported by the wrapped process is either the
// child process that we forked, or a descendant of it.
if (innerPid > 0) {
int parentPid = innerPid;
while (parentPid > 0 && parentPid != pid) {
parentPid = Process.getParentPid(parentPid);
}
if (parentPid > 0) {
Log.i(TAG, "Wrapped process has pid " + innerPid);
pid = innerPid;
usingWrapper = true;
} else {
Log.w(TAG, "Wrapped process reported a pid that is not a child of "
+ "the process that we forked: childPid=" + pid
+ " innerPid=" + innerPid);
}
}
} try {
mSocketOutStream.writeInt(pid);
mSocketOutStream.writeBoolean(usingWrapper);
} catch (IOException ex) {
throw new IllegalStateException("Error writing to command socket", ex);
}
}

主要进行一些资源清理的工作。到这里,子进程就创建完成了。

总结

  1. 调用 Process.start() 创建应用进程
  2. ZygoteProcess 负责和 Zygote 进程建立 socket 连接,并将创建进程需要的参数发送给 Zygote 的 socket 服务端
  3. Zygote 服务端接收到参数之后调用 ZygoteConnection.processOneCommand() 处理参数,并 fork 进程
  4. 最后通过 findStaticMain() 找到 ActivityThread 类的 main() 方法并执行,子进程就启动了

预告

到现在为止已经解析了 Zygote 进程 ,SystemServer 进程,以及应用进程的创建。下一篇的内容是和应用最密切相关的系统服务 ActivityManagerService , 来看看它在 SystemServer 中是如何被创建和启动的,敬请期待!

文章首发微信公众号: 秉心说 , 专注 Java 、 Android 原创知识分享,LeetCode 题解。

更多最新原创文章,扫码关注我吧!

Android 世界中,谁喊醒了 Zygote ?的更多相关文章

  1. 浅析android系统设计中的回调思想

    一.为何写作本文  在慢慢深入接触android开发的过程中,我越来越发现android中(至少应用曾的开发)用到了很多回调的思想.比如activity的生命周期,fragment的生命周期,皆是回调 ...

  2. Android世界第一个activity启动过程

    Android世界第一个activity启动过程 第一次使用Markdown,感觉不错. Android系统从按下开机键一直到launcher的出现,是一个如何的过程,中间都做出了什么操作呢.带着这些 ...

  3. Android系统中的广播(Broadcast)机制简要介绍和学习计划

    在Android系统中,广播(Broadcast)是在组件之间传播数据(Intent)的一种机制:这些组件甚至是可以位于不同的进程中,这样它就像Binder机制一样,起到进程间通信的作用:本文通过一个 ...

  4. 【转】android ddms中查看线程释疑

    原文网址:http://www.mobiletrain.org/lecture/doc/android/2011-05/457.html 大家都用过ddm,如果你用ddms查看一个程序的所有线程,你会 ...

  5. Dagger2在Android开发中的应用

    世界是普遍联系的,任何事物和个体都直接或间接相互依赖,在时空长河中共同发展.在面向对象的世界中,更是如此,类与类之间的依赖,关联关系,模块(亦或是分层架构中的层)之间的耦合关系,都是我们在软件开发实践 ...

  6. Android NFC开发(二)——Android世界里的NFC所具备的条件以及使用方法

    Android NFC开发(二)--Android世界里的NFC所具备的条件以及使用方法 NFC的应用比较广泛,而且知识面也是比较广的,所以就多啰嗦了几句,我还还是得跟着官方文档:http://dev ...

  7. Android系统启动流程(二)解析Zygote进程启动过程

    1.Zygote简介 在Android系统中,DVM(Dalvik虚拟机).应用程序进程以及运行系统的关键服务的SystemServer进程都是由Zygote进程来创建的,我们也将它称为孵化器.它通过 ...

  8. Android Framework中的线程Thread及它的threadLoop方法

    当初跟踪Camera的代码中的时候一直追到了HAL层,而在Framework中的代码看见了许很多多的Thread.它们普遍的特点就是有一个threadLoop方法.依照字面的意思应该是这个线程能够循环 ...

  9. 【转】Android系统中的.apk文件和dex文件

    1. *.apk文件 APK是Android Package的缩写,即Android安装包.通过将APK文件直接传到Android模拟器或Android手机中执行即可安装. 使用Android打包工具 ...

随机推荐

  1. yzoj P2343 & 洛谷 P1437 [HNOI2004]敲砖块

    题意 在一个凹槽中放置了N层砖块,最上面的一层油N块砖,从上到下每层一次减少一块砖.每块砖都有一个分值,敲掉这块砖就能得到相应的分值,如图所示. 如果你想敲掉第i层的第j块砖的话,若i=1,你可以直接 ...

  2. KubeSphere CI/CD+GitLab+Harbor将Spring Boot项目部署至Kubernetes

    上一篇文章分享了如何在 KubeSphere 对公共的代码仓库 GitHub 和镜像仓库 DockerHub 创建流水线,本文将继续使用 KubeSphere,基于 Harbor 和 GitLab 创 ...

  3. Charles安装windows篇

    简介 Charles是一款非常好用的网络抓包工具,类似fiddle抓包工具,当然也可以理解为一款HTTP代理服务器.HTTP监视器.反向代理服务器等. 二.官网下载 地址:https://www.ch ...

  4. ~!#$%^&*这些符号怎么读? 当然是用英语(键盘特殊符号小结)

    ~!#$%^&*这些符号怎么读? 当然是用英语(键盘特殊符号小结)   感谢原文作者:http://www.360doc.com/content/14/0105/20/85007_342874 ...

  5. 剑指Offer(三十二):把数组排成最小的数

    剑指Offer(三十二):把数组排成最小的数 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/b ...

  6. 【LeetCode】105#从前序与中序遍历序列构造二叉树

    题目描述 根据一棵树的前序遍历与中序遍历构造二叉树. 注意: 你可以假设树中没有重复的元素. 例如,给出 前序遍历 preorder = [3,9,20,15,7] 中序遍历 inorder = [9 ...

  7. 059 Python计算生态概览

    目录 一.概要 二.导学 三.实践能力 一.概要 从数据处理到人工智能 实例15-霍兰德人格分析雷达图 从Web解析到网络空间 从人机交互到艺术设计 实例16-玫瑰花绘制 二.导学 纵览Python计 ...

  8. android 屏幕切换

    1.将Activity固定位竖屏可以在配置文件这么写 <activity android:screenOrientation="portrait"> 横屏显示: < ...

  9. flex布局笔记整理

    flex布局笔记整理 了解-webkit-box 利用postcss进行css代码的向后兼容时,display:flex兼容后的代码常会带有display:-webkit-box. 部分移动端内核较低 ...

  10. 04: OpenGL ES 基础教程03 纹理

    前言 1:常用类: 1:纹理的作用 正文 一:常用类 上下文 顶点数据缓存 着色器 baseEffect 一:纹理 1.1:   纹理可以控制渲染的每个像素的颜色. 1.2: 纹素:与像素一样,保存每 ...