1 zygote分析

1.1 简介

Zygote本身是一个NATIVE层的应用程序,与驱动、内核无关。前面已经介绍过了,zygote由init进程根据init.rc配置文件创建。其实本质上来说,zygote就是app_process,这个名字在android.mk中指定,但是在运行的时候,app_process通过LINUX下的pctrl系统调用将自己的名字换成了“zygote”,所以通过ps命令就可以看到进程的名字为zygote。Pctrl函数相关信息见:

http://blog.csdn.net/zuokong/article/details/7318154

zygote的原型app_process在App_main.cpp中:

int main(int argc, char* const argv[])

{

// These are global variables in ProcessState.cpp

mArgC = argc;

mArgV = argv;

mArgLen = 0;

for (int i=0; i<argc; i++) {

mArgLen += strlen(argv[i]) + 1;

}

mArgLen--;

AppRuntime runtime;

const char* argv0 = argv[0];

……

/*将参数加入到JAVAVM中作为其参数选项*/

int i = runtime.addVmArguments(argc, argv);

// Parse runtime arguments.  Stop at first unrecognized option.

bool zygote = false;

bool startSystemServer = false;

bool application = false;

const char* parentDir = NULL;

const char* niceName = NULL;

const char* className = NULL;

while (i < argc) {

const char* arg = argv[i++];

if (!parentDir) {

parentDir = arg;

} else if (strcmp(arg, "--zygote") == 0) {

zygote = true;

niceName = "zygote";

} 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 = arg + 12;

} else {

className = arg;

break;

}

}

//这就是前面说的换名把戏的实现函数

if (niceName && *niceName) {

setArgv0(argv0, niceName);

set_process_name(niceName);

}

//设置runtime的mParentDir为/sysem/bin

runtime.mParentDir = parentDir;

if (zygote) {

//★这是关键函数,怎个zygote的主要功能都由此函数启动!

runtime.start("com.android.internal.os.ZygoteInit",

startSystemServer ? "start-system-server" : "");

} else if (className) {

// Remainder of args get passed to startup class main()

runtime.mClassName = className;

runtime.mArgC = argc - i;

runtime.mArgV = argv + i;

runtime.start("com.android.internal.os.RuntimeInit",

application ? "application" : "tool");

} else {

fprintf(stderr, "Error: no class name or --zygote supplied.\n");

app_usage();

LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");

return 10;

}

}

1.2 AppRuntime分析

AppRuntime是一个类,该类的声明和实现都在APP_main.cpp中,该类派生至AndroidRuntime类。

AppRuntime重载了onStarted, onZygoteInit, onExit函数。

1.1节中说道zygote的main函数中调用了AppRuntime.start函数,下面来详细分析下这个函数的功能:

1) 获取环境变量ANDROID_ROOT的值,赋给rootDir,如果该值为null,就新增该变量,并将其值设置为”/system”。

2) ★创建虚拟机:

if (startVm(&mJavaVM, &env) != 0) {

return;

}

3) ★注册JNI函数:

startReg(env);

4) 构建一个String数组,这个数组含有两个元素:classname, option。这个数组用于后面第五步的main函数的参数!

stringClass = env->FindClass("java/lang/String");

assert(stringClass != NULL);

//string strArray[] = new String[2];

strArray = env->NewObjectArray(2, stringClass, NULL);

assert(strArray != NULL);

classNameStr = env->NewStringUTF(className);

assert(classNameStr != NULL);

//设置第一个元素值为“com.android.internal.os.zygoteInit”

env->SetObjectArrayElement(strArray, 0, classNameStr);

optionsStr = env->NewStringUTF(options);

//设置第二个元素值为“start-system-server”!

env->SetObjectArrayElement(strArray, 1, optionsStr);

5) ★开始虚拟机,此线程就变为虚拟机的主线程,除非虚拟机退出,否则这个线程是不会返回的:

①将前面的com.android.internal.os.zygoteInit转换为斜线的形式,这样就符合jni函数的规范了,以后我们均简称其为zygoteInit类:

char* slashClassName = toSlashClassName(className);

②调用该类的main(strings)函数,此函数的参数就是前面说的string数组。

/*在调用zygoteInit的main函数后,zygote便进入了java世界!所以说zygote是android系统中JAVA世界的鼻祖!*/

char* slashClassName = toSlashClassName(className);

jclass startClass = env->FindClass(slashClassName);

if (startClass == NULL) {

ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);

/* keep going */

} else {

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 {

env->CallStaticVoidMethod(startClass, startMeth, strArray);

…….

通过上面对app_runtime的start函数的分析,我们发现了三个关键点:2、3、5。它们共同创建了整个Android的JAVA世界,下面分别对它们三个进行详细分析。

1.3 创建虚拟机——startVM

该函数就是调用JNI的虚拟机创建函数,但是创建虚拟机时的一些参数却是在此函数中确定的,详细代码在base/core/jni/Android_Runtime.cpp中:

/*此函数的绝大部分代码都是设置虚拟机的参数,这里只分析其中的两个。*/

/*下面的代码是用来设置JNI check选项的,JNIcheck就是NATIVE层调用JNI函数时,系统所做的一些检查工作。如,调用NEWUTFString函数时,系统会检测传入的字符串是否满足UTF-8编码格式。它还能检测资源是否被正确释放,但这个选项比较耗时,只有在eng版的系统里面才有,正式发布的user版是关闭了此功能的。下面几句代码就是有系统属性来控制是否使用JNI check。*/

property_get("dalvik.vm.checkjni", propBuf, "");

if (strcmp(propBuf, "true") == 0) {

checkJni = true;

} else if (strcmp(propBuf, "false") != 0) {

/* property is neither true nor false; fall back on kernel parameter */

property_get("ro.kernel.android.checkjni", propBuf, "");

if (propBuf[0] == '1') {

checkJni = true;

}

}

……

/*开始设置虚拟机的堆栈起始大小和最大大小,默认起始大小是4MB最大大小是16MB,不过绝大多数厂商都会更改这个值*/

strcpy(heapstartsizeOptsBuf, "-Xms");

property_get("dalvik.vm.heapstartsize", heapstartsizeOptsBuf+4, "4m");

opt.optionString = heapstartsizeOptsBuf;

mOptions.add(opt);

strcpy(heapsizeOptsBuf, "-Xmx");

property_get("dalvik.vm.heapsize", heapsizeOptsBuf+4, "16m");

opt.optionString = heapsizeOptsBuf;

mOptions.add(opt);

……

/*最后调用JNI_CreateJavaVM 函数创建虚拟机,pEnv返回当前线程的JNIEnv,从这里就可以了解到VM是对进程而言的,JNIEnv是对线程而言的*/

if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {

ALOGE("JNI_CreateJavaVM failed\n");

goto bail;

}

1.4 注册JNI函数——startReg

上一节介绍了如何创建虚拟机,本节就介绍如何给这个虚拟机注册JNI函数。因为后续的JAVA世界用到的一些函数是采用NATIVE方式实现的,所以才必须提前注册这些JNI函数。详细代码同样在AndroidRuntime.cpp中:

/*static*/ int AndroidRuntime::startReg(JNIEnv* env)

{

/*

* This hook causes all future threads created in this process to be

* attached to the JavaVM.  (This needs to go away in favor of JNI

* Attach calls.)

*/

/* 设置Thread类的线程创建函数为javaCreateThreadEtc(这其实是一个hook函数),此函数的具体作用将在后面进行详细分析 */

androidSetCreateThreadFunc((android_create_thread_fn)javaCreateThreadEtc);

ALOGV("--- registering native functions ---\n");

/*

* Every "register" function calls one or more things that return

* a local reference (e.g. FindClass).  Because we haven't really

* started the VM yet, they're all getting stored in the base frame

* and never released.  Use Push/Pop to manage the storage.

*/

env->PushLocalFrame(200);

//注册JNI函数,gRegJNI是一个全局大数组,每个元素对应了一个JNI注册方法。

if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {

env->PopLocalFrame(NULL);

return -1;

}

env->PopLocalFrame(NULL);

//这句话听说是码农休闲时的小把戏,可以称为IT界的文物~

//createJavaThread("fubar", quickTest, (void*) "hello");

return 0;

}

下面再来看看register_jni_procs函数的详细代码:

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)

{

for (size_t i = 0; i < count; i++) {

if (array[i].mProc(env) < 0) {//此函数仅仅是一个封装,调用数组元素的mproc函数,完成对应的JNI注册

#ifndef NDEBUG

ALOGD("----------!!! %s failed to load\n", array[i].mName);

#endif

return -1;

}

}

return 0;

}

上面提到了gRegJNI全局数组,该数组元素调用mProc函数,那么它们到底是什么呢?

static const RegJNIRec gRegJNI[] = {

REG_JNI(register_com_android_internal_os_RuntimeInit),

REG_JNI(register_android_os_SystemClock),

REG_JNI(register_android_util_EventLog),

REG_JNI(register_android_util_Log),

REG_JNI(register_android_util_FloatMath),

REG_JNI(register_android_text_format_Time),

….

约有100项,每一项的括号里面的参数name对应一个函数name(env)

}

REG_JNI是一个宏,宏里面包含的就是那个mProc函数。

#ifdef NDEBUG

#define REG_JNI(name)      { name }

struct RegJNIRec {

int (*mProc)(JNIEnv*);

};

#else

#define REG_JNI(name)      { name, #name }

struct RegJNIRec {

int (*mProc)(JNIEnv*);

const char* mName;

};

#endif

这个宏的意思就是,当我们调用gRegJNI[i].mProc(env)时,真正执行的函数就是name_i (env),这里为了方便就这样定义了,这个小技巧值得学习啊。

至此,我们已经分析了startVM, StartReg,了解了虚拟机的创建,学习了JNI函数的注册,下面就调用zygoteInit的main函数,进入java世界了!

1.5 Welcom to Java World

Java世界的入口函数就是zygoteInit的main函数,下面看看这个main函数的具体功能(ps:后面的代码都是java代码了~)。

public static void main(String argv[]) {

try {

// Start profiling the zygote initialization.

SamplingProfilerIntegration.start();

//★key1注册zygote用的socket

registerZygoteSocket();

EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,

SystemClock.uptimeMillis());

preload(); //★key2预加载类资源等

/*

static void preload() {

preloadClasses();

preloadResources();

preloadOpenGL();

}

*/

EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,

SystemClock.uptimeMillis());

// Finish profiling the zygote initialization.

SamplingProfilerIntegration.writeZygoteSnapshot();

//强制执行一次垃圾回收

gc();

// If requested, start system server directly from Zygote

if (argv.length != 2) {

throw new RuntimeException(argv[0] + USAGE_STRING);

}

if (argv[1].equals("start-system-server")) {

startSystemServer(); //★key3我们传入的参数是满足if的,所以调用此函数启动system_server进程

} else if (!argv[1].equals("")) {

throw new RuntimeException(argv[0] + USAGE_STRING);

}

Log.i(TAG, "Accepting command socket connections");

runSelectLoop();//★key4 zygote调用这个函数

closeServerSocket();

} catch (MethodAndArgsCaller caller) {

caller.run(); //★key5 很重要的caller run函数,以后分析

} catch (RuntimeException ex) {

Log.e(TAG, "Zygote died with exception", ex);

closeServerSocket();

throw ex;

}

}

在这个main函数中,列举了5大关键点,下面一一进行分析。

1、建立IPC通信服务端:registerZygoteSocket

Zygote及系统中其他程序之间的通讯并没有使用Binder,而是采用了基于AF_UNIX类型的Socket。registerZygoteSocket的工作就是建立这个socket。

private static void registerZygoteSocket() {

if (sServerSocket == null) {

int fileDesc;

try {

/*从环境变量中获取socket的fd,回想一下zygote是如何启动的:在init进程的main函数中有一个execve函数,该函数的第三个参数就是环境变量*/

String env = System.getenv(ANDROID_SOCKET_ENV);

fileDesc = Integer.parseInt(env);

} catch (RuntimeException ex) {

throw new RuntimeException(

ANDROID_SOCKET_ENV + " unset or invalid", ex);

}

try {

//创建服务端socket,这个socket将listen并accept Client

sServerSocket = new LocalServerSocket(

createFileDescriptor(fileDesc));

} catch (IOException ex) {

throw new RuntimeException(

"Error binding to local socket '" + fileDesc + "'", ex);

}

}

}

registerZygoteSocket很简单,就是创建一个socket服务器。不过我们应当思考一下:

①谁是客户端?

②服务端如何处理来自客户端的请求?

2、预加载类和资源

这里主要分析preloadClasses和preloadResources函数。

首先来看preloadClasses函数:

①预加载类的信息存储在PRELOADED_CLASSES变量中,它的值为”preloaded_classes”。这是一个文件的名字。意思就是预加载类的信息存储在这个文件中!

②读取该文件的每一行,再通过JAVA反射来加载类,每一行中存储的就是预加载类的类名。

Class.forName(line);

③一些扫尾工作。

Preloadclasses看起来很简单,但其实它要加载1268个类!!!这是相当耗时的,这也是Android系统启动慢的原因之一。Preload-class文件由frame/base/tools/preload工具生成,它会判断每个类的加载时间是否大于1250微秒,超过这个时间的话就会被写到这个文件中,再由zygote预加载。详细信息可以参考preload工具的说明。

至于preloadResources,它与preloadClasses类似,它主要加载frameword-res.apk中的资源——在UI编程中常用到的com.android.R.xxx是系统默认资源,它们就是由zygote在这里加载的。

3、启动system_server

现在开始分析第三个关键函数startSystemServer。这个函数会创建JAVA世界中系统server所驻留的进程system_server,该进程是framework的核心

private static boolean startSystemServer()

throws MethodAndArgsCaller, RuntimeException {

/** Hardcoded command line to start the system server */

String args[] = {

"--setuid=1000",Class.forName(line);

"--setgid=1000",       "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,3001,3002,3003",

"--capabilities=130104352,130104352",

"--runtime-init",

"--nice-name=system_server",

"com.android.server.SystemServer",

};

ZygoteConnection.Arguments parsedArgs = null;

int pid;

try {

//将args转换成Arguments类对象

parsedArgs = new ZygoteConnection.Arguments(args);

int debugFlags = parsedArgs.debugFlags;

if ("1".equals(SystemProperties.get("ro.debuggable")))

debugFlags |= Zygote.DEBUG_ENABLE_DEBUGGER;

/*fork一个子进程,这个子进程就是system_server进程 */

pid = Zygote.forkSystemServer(

parsedArgs.uid, parsedArgs.gid,

parsedArgs.gids, debugFlags, null);

} catch (IllegalArgumentException ex) {

throw new RuntimeException(ex);

}

/** For child process */

if (pid == 0) {

handleSystemServerProcess(parsedArgs); //system_server进程的工作。

}

return true;

}

handleSystemServerProcess的详细代码如下:

private static void handleSystemServerProcess(

ZygoteConnection.Arguments parsedArgs)

throws ZygoteInit.MethodAndArgsCaller {

/*首先设置兼容性*/

if (parsedArgs.uid != 0) {

try {

setCapabilities(parsedArgs.permittedCapabilities,

parsedArgs.effectiveCapabilities);

} catch (IOException ex) {

Log.e(TAG, "Error setting capabilities", ex);

}

}

//然后关闭该服务的socket

closeServerSocket();

/**

* 将主要的参数传递给SystemServer.

* "--nice-name=system_server com.android.server.SystemServer"

*/

RuntimeInit.zygoteInit(parsedArgs.remainingArgs);

}

从上面的代码可以看出,zygote进行了一次fork,创建了一个子进程system_server进程(代码中的zygote.forkSystemServer),该进程的具体作用后面再讲,下面继续分析zygote进程。

4、runSelectLoop分析

当zygote从startSystemServer返回后,就进入第四个关键函数:runSelectLoop。

回想在前面的第一个关键点registerZygoteSocket中注册了一个用于IPC的socket,并提出了两个问题,第二个问题就是:服务端如何处理请求。答案就在这个函数中:

private static void runSelectLoop() throws MethodAndArgsCaller {

ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();

ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();

FileDescriptor[] fdArray = new FileDescriptor[4];

/* sServerSocket 是我们在前面的registerZygoteSocket中建立的socket*/

fds.add(sServerSocket.getFileDescriptor());

peers.add(null);

int loopCount = GC_LOOP_COUNT;

while (true) {

int index;

if (loopCount <= 0) {

gc();

loopCount = GC_LOOP_COUNT;

} else {

loopCount--;

}

try {

fdArray = fds.toArray(fdArray);

/* selectReadable 是一个NATIVE函数,使用多路复用I/O模型。

当有客户端连接或有数据时,该函数就返回。新的客户端连接上返回0,已连接上的客户端发送数据则返回>0,错误返回-1*/

index = selectReadable(fdArray);

} catch (IOException ex) {

throw new RuntimeException("Error in select()", ex);

}

if (index < 0) {

throw new RuntimeException("Error in select()");

} else if (index == 0) {

/*有一个客户端连接上。注意,客户端在zygote的代表是zygoteConnection*/

ZygoteConnection newPeer = acceptCommandPeer();

peers.add(newPeer);

fds.add(newPeer.getFileDesciptor());

} else {

boolean done;

/*客户端发送了请求,peers.get返回zygoteConnection类对象,然后调用该类的runOnce函数来处理客户端请求*/

done = peers.get(index).runOnce();

if (done) {

/*表示客户端的请求已经得到解决,服务器就删除该连接*/

peers.remove(index);

fds.remove(index);

}

}

}

}

总结runSelectLoop:

①处理客户端的连接和请求。客户端在zygote中使用zygoteConnection对象表示。

②客户端的请求有zygoteconnection的runOnce函数处理。该函数实现代码较为复杂,主要原理是:从socket命令行里面读取一个开始命令。如果读取成功,就fork一个子进程并在这个子进程中抛出zygoteInit.MethodAndArgsCaller异常,而父进程中就正常返回;如果读取失败,那么就不会fork子进程,而是log出错误消息。至于父进程的返回值也有两种:如果在socket命令中读取到了一个EOF标志,就返回true值,如果还有命令可以读取,就返回false值——这个返回值就是标记客户端请求是否完成的~

1.6 zygote的总结

Zygote是Android系统中创建JAVA世界的盘古——它创建了第一个JAVA虚拟机;它也是女娲——它成功的fork了framework的核心system_server进程。现在回顾一下Zygote创建JAVA世界的步骤:

①创建AppRuntime对象,并调用它的start方法。此后的活动均由AppRuntime控制。

②调用startVM创建虚拟机,然后调用startReg来注册JNI函数。

③通过JNI调用com.android.internal.os.ZygoteInit类的main函数,从此进入了JAVA世界。只不过这个世界刚创造,里面什么东西都没有。

④调用registerZygoteSocket函数,这个函数相应子孙后代的请求,同时zygote调用preloadClass,preloadResource,preloadOpenGL为JAVA世界添砖加瓦。

⑤zygote觉得自己的工作量有点大,就fork了一个进程system_server来为java世界服务。

⑥zygote完成了Java世界的初创工作,它有点累了。以后的工作就由runSelectLoop函数来处理,zygote睡觉去了。

⑦以后的日子:zygote随时守护在我们的周围,当接收到子孙后代的请求时,它随时醒来,为它们工作。

Zygote原理学习的更多相关文章

  1. IIS原理学习

    IIS 原理学习 首先声明以下内容是我在网上搜索后整理的,在此只是进行记录,以备往后查阅只用. IIS 5.x介绍 IIS 5.x一个显著的特征就是Web Server和真正的ASP.NET Appl ...

  2. zookkeper原理学习

    zookkeper原理学习  https://segmentfault.com/a/1190000014479433   https://www.cnblogs.com/felixzh/p/58692 ...

  3. GIS原理学习目录

    GIS原理学习目录 内容提要 本网络教程是教育部“新世纪网络课程建设工程”的实施课程.系统扼要地阐述地理信息系统的技术体系,重点突出地理信息系统的基本技术及方法. 本网络教程共分八章:第一章绪论,重点 ...

  4. 转:SVM与SVR支持向量机原理学习与思考(一)

    SVM与SVR支持向量机原理学习与思考(一) 转:http://tonysh-thu.blogspot.com/2009/07/svmsvr.html 弱弱的看了看老掉牙的支持向量机(Support ...

  5. Android自复制传播APP原理学习(翻译)

     Android自复制传播APP原理学习(翻译) 1 背景介绍 论文链接:http://arxiv.org/abs/1511.00444 项目地址:https://github.com/Tribler ...

  6. 计算机原理学习(1)-- 冯诺依曼体系和CPU工作原理

    前言 对于我们80后来说,最早接触计算机应该是在95年左右,那个时候最流行的一个词语是多媒体. 依旧记得当时在同学家看同学输入几个DOS命令就成功的打开了一个游戏,当时实在是佩服的五体投地.因为对我来 ...

  7. Dubbo原理学习

    Dubbo源码及原理学习 阿里中间件团队博客 Dubbo官网 Dubbo源码解析 Dubbo源码解析-掘金 Dubbo源码解析-赵计刚 Dubbo系列 源码总结+最近感悟

  8. XGBoost原理学习总结

    XGBoost原理学习总结 前言 ​ XGBoost是一个上限提别高的机器学习算法,和Adaboost.GBDT等都属于Boosting类集成算法.虽然现在深度学习算法大行其道,但很多数据量往往没有太 ...

  9. Git原理学习记录

    Git原理学习记录 1.git init git-test ​ git init 实际上就是在特定的目录下创建对应的目录和文件 2.object $ echo "V1" > ...

随机推荐

  1. checkbox绑定v-for的数据

    简述自己遇到的问题,觉得合适就拿去用 我在使用v-for遍历checked复选框数据的时候,数据分为两部分,一个主活动,主活动下面有多个子活动 我实体类的设计是里面加个list放子活动, 页面循环需要 ...

  2. Ubuntu编译Android源码过程中的空间不足解决方法

    Android源码一般几十G,就拿Android5.0来说,下载下来大概也有44G左右,和编译产生的文件以及Ubuntu系统占用的空间加起来,源码双倍的空间都不够有.编译源码前能分配足够的空间再好不过 ...

  3. iOS 多线程编程

    参考文章: iOS多线程编程之NSThread的使用http://blog.csdn.net/totogo2010/article/details/8010231 iOS多线程编程之NSOperati ...

  4. 【转】vxworks的default boot line说明

    boot程序的主要功能是引导vxworks 内核,所以boot程序需要知道vxworks的内核存放在何处,通过什么手段去获取.在vxworks缺省的boot程序里有一条内建的default boot ...

  5. 转 Solr vs. Elasticsearch谁是开源搜索引擎王者

    转 https://www.cnblogs.com/xiaoqi/p/6545314.html Solr vs. Elasticsearch谁是开源搜索引擎王者 当前是云计算和数据快速增长的时代,今天 ...

  6. destoon去除编辑器替换图片删除原图功能,删除信息删除相关图片功能

    去除这些功能会造成大量垃圾图片,但是客户存在大量复制内容,其中图片一样,为了防止客户替换其中一个图片或者删除信息 造成其他复制信息图片丢失 去除文章模型级联图片功能. 对应模块class.php se ...

  7. MySQL查询时,查询结果如何按照where in数组排序

    MySQL查询时,查询结果如何按照where in数组排序 在查询中,MySQL默认是order by id asc排序的,但有时候需要按照where in 的数组顺序排序,比如where in的id ...

  8. IOC容器和Bean的配置实例

    实验1: <!--实验1:通过IOC容器创建对象,并为属性赋值 --> <!-- 需要由IOC容器创建对象的全类名 --> <!-- 为了便于从IOC容器中获取book对 ...

  9. UVA - 1220 Party at Hali-Bula (树形DP)

    有 n 个员工,n-1个从属关系. 不能同时选择某个员工和他的直接上司,问最多可以选多少人,以及选法是否唯一. 树上的最大独立集问题.只不过多了一个判断唯一性. dp[u][0]表示不选这个点的状态, ...

  10. Nordic Collegiate Programming Contest 2015​ D. Disastrous Downtime

    You're investigating what happened when one of your computer systems recently broke down. So far you ...