深入理解init

1.1 概述

init 是一个进程,确切地说,它是Linux 系统中用户空间地第一个进程。由于Android 是基于Linux 内核的,所以init 也是Android 系统中用户空间的第一个进程,它的进程号是1。它的主要指责为:

  • init 进程负责创建系统中的几个关键进程,包括zygote。
  • Android 系统有很多属性,于是init 就提供了一个property service(属性服务)来管理它们。

1.2 init 分析

init 进程的入口函数是main,它的代码如下所示:

/** 1.C++中主函数有两个参数,第一个参数argc表示参数个数,第二个参数是参数列表,也就是具体的参数* 2.init的main函数有两个其它入口,一是参数中有ueventd,进入ueventd_main,二是参数中有watchdogd,进入watchdogd_main*/
int main(int argc, char** argv) {/** 1.strcmp是String的一个函数,比较字符串,相等返回0* 2.C++中0也可以表示false* 3.basename是C库中的一个函数,得到特定的路径中的最后一个'/'后面的内容,* 比如/sdcard/miui_recovery/backup,得到的结果是backup*/if (!strcmp(basename(argv[0]), "ueventd")) {//当argv[0]的内容为ueventd时,strcmp的值为0,!strcmp为1//1表示true,也就执行ueventd_main,ueventd主要是负责设备节点的创建、权限设定等一些列工作return ueventd_main(argc, argv);}if (!strcmp(basename(argv[0]), "watchdogd")) {//watchdogd俗称看门狗,用于系统出问题时重启系统return watchdogd_main(argc, argv);}if (argc > 1 && !strcmp(argv[1], "subcontext")) {InitKernelLogging(argv);const BuiltinFunctionMap function_map;return SubcontextMain(argc, argv, &function_map);}if (REBOOT_BOOTLOADER_ON_PANIC) {//初始化重启系统的处理信号,内部通过sigaction 注册信号,当监听到该信号时重启系统InstallRebootSignalHandlers();}bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr); //查看是否有环境变量INIT_SECOND_STAGE/** init的main方法会执行两次,由is_first_stage控制,first_stage就是第一阶段要做的事*/if (is_first_stage) { //只执行一次,因为在方法体中有设置INIT_SECOND_STAGEboot_clock::time_point start_time = boot_clock::now();// Clear the umask.umask(0); //清空文件权限clearenv();setenv("PATH", _PATH_DEFPATH, 1); //注册环境变量PATH// Get the basic filesystem setup we need put together in the initramdisk// on / and then we'll let the rc file figure out the rest.// 创建一些文件,并挂载设备,这些是与Linux 相关的mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");mkdir("/dev/pts", 0755);mkdir("/dev/socket", 0755);mount("devpts", "/dev/pts", "devpts", 0, NULL);#define MAKE_STR(x) __STRING(x)mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));// Don't expose the raw commandline to unprivileged processes.chmod("/proc/cmdline", 0440);gid_t groups[] = { AID_READPROC };setgroups(arraysize(groups), groups);mount("sysfs", "/sys", "sysfs", 0, NULL);mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));if constexpr (WORLD_WRITABLE_KMSG) {mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11));}mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));// Mount staging areas for devices managed by vold// See storage config details at http://source.android.com/devices/storage/mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,"mode=0755,uid=0,gid=1000");// /mnt/vendor is used to mount vendor-specific partitions that can not be// part of the vendor partition, e.g. because they are mounted read-write.mkdir("/mnt/vendor", 0755);// Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually// talk to the outside world...// InitKernelLogging 首先是将标准输入输出重定向到"/sys/fs/selinux/null",然后调用InitLogging初始化log日志系统InitKernelLogging(argv);LOG(INFO) << "init first stage started!";if (!DoFirstStageMount()) { // 主要作用是初始化特定设备并挂载LOG(FATAL) << "Failed to mount required partitions early ...";}//Avb即Android Verfied boot,功能包括Secure Boot, verfying boot 和 dm-verity, //原理都是对二进制文件进行签名,在系统启动时进行认证,确保系统运行的是合法的二进制镜像文件。//其中认证的范围涵盖:bootloader,boot.img,system.imgSetInitAvbVersionInRecovery(); //在刷机模式下初始化avb的版本,不是刷机模式// Enable seccomp if global boot option was passed (otherwise it is enabled in zygote).global_seccomp();// Set up SELinux, loading the SELinux policy.SelinuxSetupKernelLogging();SelinuxInitialize(); //加载SELinux policy,也就是安全策略,// We're in the kernel domain, so re-exec init to transition to the init domain now// that the SELinux policy has been loaded.if (selinux_android_restorecon("/init", 0) == -1) { //restorecon命令用来恢复SELinux文件属性即恢复文件的安全上下文PLOG(FATAL) << "restorecon failed of /init failed";}setenv("INIT_SECOND_STAGE", "true", 1);static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1); //记录第二阶段开始时间戳char* path = argv[0];char* args[] = { path, nullptr };execv(path, args); //重新执行main方法,进入第二阶段// execv() only returns if an error happened, in which case we// panic and never fall through this conditional.PLOG(FATAL) << "execv(\"" << path << "\") failed";}...
}

在init初始化过程中,Android分别挂载了tmpfs,devpts,proc,sysfs,selinuxfs这5类文件系统。

  • tmpfs

tmpfs是一种虚拟内存文件系统,它会将所有的文件存储在虚拟内存中,如果你将tmpfs文件系统卸载后,那么其下的所有的内容将不复存在。
tmpfs既可以使用RAM,也可以使用交换分区,会根据你的实际需要而改变大小。
tmpfs的速度非常惊人,毕竟它是驻留在RAM中的,即使用了交换分区,性能仍然非常卓越。
由于tmpfs是驻留在RAM的,因此它的内容是不持久的。断电后,tmpfs的内容就消失了,这也是被称作tmpfs的根本原因。

  • devpts

devpts文件系统为伪终端提供了一个标准接口,它的标准挂接点是/dev/ pts。
只要pty的主复合设备/dev/ptmx被打开,就会在/dev/pts下动态的创建一个新的pty设备文件。

  • proc

proc文件系统是一个非常重要的虚拟文件系统,它可以看作是内核内部数据结构的接口,通过它我们可以获得系统的信息,同时也能够在运行时修改特定的内核参数。

  • sysfs

与proc文件系统类似,sysfs文件系统也是一个不占有任何磁盘空间的虚拟文件系统。
它通常被挂接在/sys目录下。sysfs文件系统是Linux2.6内核引入的,
它把连接在系统上的设备和总线组织成为一个分级的文件,使得它们可以在用户空间存取。

  • selinuxfs

selinuxfs也是虚拟文件系统,通常挂载在/sys/fs/selinux目录下,用来存放SELinux安全策略文件

init进程第一阶段做的主要工作是挂载分区,创建设备节点和一些关键目录,初始化日志输出系统,启用SELinux安全策略

第二阶段

int main(int argc, char** argv) {...// At this point we're in the second stage of init.InitKernelLogging(argv);LOG(INFO) << "init second stage started!";// Set up a session keyring that all processes will have access to. It// will hold things like FBE encryption keys. No process should override// its session keyring.keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);//初始化进程会话密钥// Indicate that booting is in progress to background fw loaders, etc.close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));//创建 /dev/.booting 文件,就是个标记,表示booting进行中property_init();//初始化属性系统,并从指定文件读取属性//接下来的一系列操作都是从各个文件读取一些属性,然后通过property_set设置系统属性// If arguments are passed both on the command line and in DT,// properties set in DT always have priority over the command-line ones./** 1.这句英文的大概意思是,如果参数同时从命令行和DT传过来,DT的优先级总是大于命令行的* 2.DT即device-tree,中文意思是设备树,这里面记录自己的硬件配置和系统运行参数*/process_kernel_dt();//处理DT属性process_kernel_cmdline();//处理命令行属性// Propagate the kernel variables to internal variables// used by init as well as the current required properties.export_kernel_boot_props();//处理其他的一些属性// Make the time that init started available for bootstat to log.property_set("ro.boottime.init", getenv("INIT_STARTED_AT"));property_set("ro.boottime.init.selinux", getenv("INIT_SELINUX_TOOK"));// Set libavb version for Framework-only OTA match in Treble build.const char* avb_version = getenv("INIT_AVB_VERSION");if (avb_version) property_set("ro.boot.avb_version", avb_version);// Set memcg property based on kernel cmdline argumentbool memcg_enabled = android::base::GetBoolProperty("ro.boot.memcg",false);if (memcg_enabled) {// root memory control cgroupmkdir("/dev/memcg", 0700);chown("/dev/memcg",AID_ROOT,AID_SYSTEM);mount("none", "/dev/memcg", "cgroup", 0, "memory");// app mem cgroups, used by activity manager, lmkd and zygotemkdir("/dev/memcg/apps/",0755);chown("/dev/memcg/apps/",AID_SYSTEM,AID_SYSTEM);mkdir("/dev/memcg/system",0550);chown("/dev/memcg/system",AID_SYSTEM,AID_SYSTEM);}// Clean up our environment.unsetenv("INIT_SECOND_STAGE");unsetenv("INIT_STARTED_AT");unsetenv("INIT_SELINUX_TOOK");unsetenv("INIT_AVB_VERSION");// Now set up SELinux for second stage.SelinuxSetupKernelLogging(); //第二阶段初始化SELinux policySelabelInitialize();SelinuxRestoreContext(); //恢复安全上下文epoll_fd = epoll_create1(EPOLL_CLOEXEC); //创建epoll实例,并返回epoll的文件描述符if (epoll_fd == -1) {PLOG(FATAL) << "epoll_create1 failed";}sigchld_handler_init(); //主要是创建handler处理子进程终止信号,创建一个匿名socket并注册到epoll进行监听if (!IsRebootCapable()) {// If init does not have the CAP_SYS_BOOT capability, it is running in a container.// In that case, receiving SIGTERM will cause the system to shut down.InstallSigtermHandler();}property_load_boot_defaults(); //从文件中加载一些属性,读取usb配置export_oem_lock_status(); //设置ro.boot.flash.locked 属性start_property_service(); //开启一个socket监听系统属性的设置set_usb_controller(); //设置sys.usb.controller 属性...
}

init进程第二阶段主要工作是初始化属性系统,解析SELinux的匹配规则,处理子进程终止信号,启动系统属性服务,可以说每一项都很关键,如果说第一阶段是为属性系统,SELinux做准备,那么第二阶段就是真正去把这些落实的。

init.rc

在该函数里面,还会解析一个叫init.rc 的配置文件。

init.rc是init进程启动的配置脚本,这个脚本是用一种叫Android Init Language(Android初始化语言)的语言写的,在7.0以前,init进程只解析根目录下的init.rc文件,但是随着版本的迭代,init.rc越来越臃肿,所以在7.0以后,init.rc一些业务被分拆到/system/etc/init,/vendor/etc/init,/odm/etc/init三个目录下,

Android Init Language语法定义在platform/system/core/init/README.md

.rc 文件主要配置了两个东西,一个是action,一个是service,trigger 和command 是对action 的补充,options 是对service 的补充。action 加上trigger 以及一些command,组成一个Section,service加上一些option,也组成一个Section,.rc文件就是由一个个Section组成。.rc文件头部有一个import的语法,表示这些.rc也一并包含并解析。

Init .rc Files

/init.rc 是最主要的一个.rc文件,它由init进程在初始化时加载,主要负责系统初始化,它会导入 /init.${ro.hardware}.rc ,这个是系统级核心厂商提供的主要.rc文件。当执行 mount_all 语句时,init进程将加载所有在 /{system,vendor,odm}/etc/init/ 目录下的文件,挂载好文件系统后,这些目录将会为Actions和Services服务。
有一个特殊的目录可能被用来替换上面的三个默认目录,这主要是为了支持工厂模式和其他非标准的启动模式,上面三个目录用于正常的启动过程,这三个用于扩展的目录是:

  • /system/etc/init/ 用于系统本身,比如SurfaceFlinger, MediaService, and logcatd.
  • /vendor/etc/init/ 用于SoC(系统级核心厂商,如高通),为他们提供一些核心功能和服务
  • /odm/etc/init/ 用于设备制造商(odm定制厂商,如华为、小米),为他们的传感器或外围设备提供一些核心功能和服务

1.3 解析.rc 文件

int main(int argc, char** argv) {...LoadBootScripts(am, sm);...
}
// 加载和解析init.rc 文件
static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {// 创建解析器Parser parser = CreateParser(action_manager, service_list);std::string bootscript = GetProperty("ro.boot.init_rc", "");if (bootscript.empty()) {// 解析init.rc 文件parser.ParseConfig("/init.rc");if (!parser.ParseConfig("/system/etc/init")) {late_import_paths.emplace_back("/system/etc/init");}if (!parser.ParseConfig("/product/etc/init")) {late_import_paths.emplace_back("/product/etc/init");}if (!parser.ParseConfig("/odm/etc/init")) {late_import_paths.emplace_back("/odm/etc/init");}if (!parser.ParseConfig("/vendor/etc/init")) {late_import_paths.emplace_back("/vendor/etc/init");}} else {parser.ParseConfig(bootscript);}
}

主要解析工作是在/system/core/init/parser.cpp

void Parser::ParseData(const std::string& filename, const std::string& data, size_t* parse_errors) {...//lambda表达式,函数的引用,每次解析新的一行之前//都会调用这个将上一行解析的结果放到对应的数组中auto end_section = [&] {if (section_parser == nullptr) return;if (auto result = section_parser->EndSection(); !result) {(*parse_errors)++;LOG(ERROR) << filename << ": " << section_start_line << ": " << result.error();}//重置section_parser = nullptr;section_start_line = -1;};...
}

具体解析过程在后面的一个无限for 循环里面:

void Parser::ParseData(const std::string& filename, const std::string& data, size_t* parse_errors) {...for (;;) {switch (next_token(&state)) {case T_EOF:end_section(); // 这里就是调用上面的end_sectionreturn;case T_NEWLINE:state.line++;if (args.empty()) break;// If we have a line matching a prefix we recognize, call its callback and unset any// current section parsers.  This is meant for /sys/ and /dev/ line entries for// uevent.for (const auto& [prefix, callback] : line_callbacks_) {if (android::base::StartsWith(args[0], prefix)) {end_section();if (auto result = callback(std::move(args)); !result) {(*parse_errors)++;LOG(ERROR) << filename << ": " << state.line << ": " << result.error();}break;}}if (section_parsers_.count(args[0])) { //判断是否包含 on service importend_section();section_parser = section_parsers_[args[0]].get(); //取出对应的parsersection_start_line = state.line;if (auto result =section_parser->ParseSection(std::move(args), filename, state.line);!result) {(*parse_errors)++;LOG(ERROR) << filename << ": " << state.line << ": " << result.error();section_parser = nullptr;}} else if (section_parser) { //不包含 on service import则是command或optionif (auto result = section_parser->ParseLineSection(std::move(args), state.line);!result) {(*parse_errors)++;LOG(ERROR) << filename << ": " << state.line << ": " << result.error();}}args.clear();break;case T_TEXT:args.emplace_back(state.text);break;}}
}

next_token扫描init.rc中的token

找到其中的文件结束(EOF)、文本(TEXT)、新行(NEWLINE),其中的空格\t\r会被忽略掉;对于TEXT,空格\t\r\n都是TEXT的结束标志。

section_parsers_.count(args[0])

会判断args[]第一个字符是不是on、service或import
如果是就拿到对应的解析器,例如是service开头的新行,section_parser就是对应之前添加的ServcieParser解析器,并且执行auto result = section_parser-> ParseSection(std::move(args), filename, state.line);,否则就执行auto result = section_parser-> ParseLineSection(std::move(args), state.line);

ParseLineSection是解析service下面附带的option(例如oneshot,class等等),并将这些属性添加到Service对象中)。

解析完一个Service对象后
end_section()中的section_parser->EndSection()将Service对象添加到Service List(services_)中,并且重置section_parser = null,为解析下一个Service做准备。

on开头的使用ActionParser解析器解析,该解析器的ParseSection方法主要是生成action对象,并将action对象添加到actions_数组中,ParseLineSection方法主要是解析command对应的函数,然后添加到action对象的commands_数组中。ServiceParser的ParseLineSection是直接执行option对应函数,而ActionParser是将对应函数保存到commands_数组中,当Action触发时,才会依次执行command函数。

总共有三个解析器,分别是:

  • ActionParser

定义在platform/system/core/init/action.cpp

  • ServiceParser

定义在platform/system/core/init/service.cpp

  • ImportParser

定义在platform/system/core/init/import_parser.cpp

init.rc解析大致如此。

1.4 触发条件

经过上一步的解析,系统从各种.rc文件中读取了需要执行的Action和Service,但是还是需要一些额外的配置,也需要加入触发条件准备去触发。

int main(int argc, char** argv) {...// Turning this on and letting the INFO logging be discarded adds 0.2s to// Nexus 9 boot time, so it's disabled by default.if (false) parser.DumpState(); //打印一些当前Parser的信息,默认是不执行的ActionManager& am = ActionManager::GetInstance();am.QueueEventTrigger("early-init");//QueueEventTrigger用于触发Action,这里触发 early-init事件// Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");//QueueBuiltinAction用于添加Action,第一个参数是Action要执行的Command,第二个是Trigger// ... so that we can start queuing up actions that require stuff from /dev.am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");am.QueueBuiltinAction(set_mmap_rnd_bits_action, "set_mmap_rnd_bits");am.QueueBuiltinAction(set_kptr_restrict_action, "set_kptr_restrict");am.QueueBuiltinAction(keychord_init_action, "keychord_init");am.QueueBuiltinAction(console_init_action, "console_init");// Trigger all the boot actions to get us started.am.QueueEventTrigger("init");// Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random// wasn't ready immediately after wait_for_coldboot_doneam.QueueBuiltinAction(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");// Don't mount filesystems or start core system services in charger mode.std::string bootmode = GetProperty("ro.bootmode", "");if (bootmode == "charger") {am.QueueEventTrigger("charger");} else {am.QueueEventTrigger("late-init");}// Run all property triggers based on current state of the properties.am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");    ...
}
QueueEventTrigger

定义在platform/system/core/init/action.cpp
它并没有去触发trigger,而是构造了一个EventTrigger对象,放到队列中存起来

QueueBuiltinAction

定义在platform/system/core/init/action.cpp
这个函数有两个参数,第一个参数是一个函数指针,第二参数是字符串。首先是创建一个Action对象,将第二参数作为Action触发条件,将第一个参数作为Action触发后的执行命令,并且又把第二个参数作为命令的参数,最后是将Action加入触发队列并加入Action列表。

1.5 触发及监听

之前的所有工作都是往各种数组、队列里面存入信息,并没有真正去触发,而接下来的工作就是真正去触发这些事件,以及用epoll不断监听新的事件。

int main(int argc, char** argv) {...while (true) {// By default, sleep until something happens.int epoll_timeout_ms = -1; //epoll超时时间,相当于阻塞时间if (do_shutdown && !shutting_down) {do_shutdown = false;if (HandlePowerctlMessage(shutdown_command)) {shutting_down = true;}}/** 1.waiting_for_prop和IsWaitingForExec都是判断一个Timer为不为空,相当于一个标志位* 2.waiting_for_prop负责属性设置,IsWaitingForExe负责service运行* 3.当有属性设置或Service开始运行时,这两个值就不为空,直到执行完毕才置为空* 4.其实这两个判断条件主要作用就是保证属性设置和service启动的完整性,也可以说是为了同步*/if (!(waiting_for_prop || Service::is_exec_service_running())) {am.ExecuteOneCommand(); //执行一个command}if (!(waiting_for_prop || Service::is_exec_service_running())) {if (!shutting_down) {auto next_process_restart_time = RestartProcesses(); //重启服务// If there's a process that needs restarting, wake up in time for that.if (next_process_restart_time) {epoll_timeout_ms = std::chrono::ceil<std::chrono::milliseconds>(*next_process_restart_time - boot_clock::now()).count();if (epoll_timeout_ms < 0) epoll_timeout_ms = 0;}}// If there's more work to do, wake up again immediately.if (am.HasMoreCommands()) epoll_timeout_ms = 0;  //当还有命令要执行时,将epoll_timeout_ms设置为0}epoll_event ev;/** 1.epoll_wait与上一篇中讲的epoll_create1、epoll_ctl是一起使用的* 2.epoll_create1用于创建epoll的文件描述符,epoll_ctl、epoll_wait都把它创建的fd作为第一个参数传入* 3.epoll_ctl用于操作epoll,EPOLL_CTL_ADD:注册新的fd到epfd中,EPOLL_CTL_MOD:修改已经注册的fd的监听事件,EPOLL_CTL_DEL:从epfd中删除一个fd;* 4.epoll_wait用于等待事件的产生,epoll_ctl调用EPOLL_CTL_ADD时会传入需要监听什么类型的事件,* 比如EPOLLIN表示监听fd可读,当该fd有可读的数据时,调用epoll_wait经过epoll_timeout_ms时间就会把该事件的信息返回给&ev*/int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, epoll_timeout_ms));if (nr == -1) {PLOG(ERROR) << "epoll_wait failed";} else if (nr == 1) {((void (*)()) ev.data.ptr)();}}     ...
}
ExecuteOneCommand

定义在platform/system/core/init/action.cpp
从名字可以看出,它只执行一个command,只执行一个。在函数一开始就从trigger_queue_队列中取出一个trigger,然后遍历所有action,找出满足trigger条件的action加入待执行列表current_executing_actions_中,接着从这个列表中取出一个action,执行它的第一个命令,并将命令所在下标自加1。由于ExecuteOneCommand外部是一个无限循环,因此按照上面的逻辑一遍遍执行,将按照trigger表的顺序,依次执行满足trigger条件的action,然后依次执行action中的命令.

RestartProcesses

定义在platform/system/core/init/init.cpp
restart_processes调用的其实是ForEachServiceWithFlags函数,这个函数主要是遍历services_数组,比较它们的flags是否是SVC_RESTARTING,也就是当前service是否是等待重启的,如果是就执行它的RestartIfNeeded函数。

RestartIfNeeded

定义在platform/system/core/init/service.cpp
这个函数将主要工作交给了Start,也就是具体的启动service,但是交给它之前做了一些判断,也就是5秒内只能启动一个服务,如果有多个服务,那么后续的服务将进入等待。

总结

Init进程根据语法一步步去解析.rc,将这些配置转换成一个个数组、队列,然后开启无限循环去处理这些数组、队列中的command和service,并且通过epoll监听子进程结束和属性设置。


参考文献
[1] 邓平凡.深入理解Android-卷Ⅰ.北京:机械工业出版社.2011-9
[2] Android 8.0 : 系统启动流程之init进程
[3] Android P (9.0) 之Init进程源码分析

《深入理解Android 卷Ⅰ》深入理解init相关推荐

  1. 《深入理解Android 卷1》读书笔记 (一)—— Android Init之属性服务 (property_service)

    本文的大体流程还是按照书本上来,分三段. (一)从Main开始到service start (二)zygote restart (三)属性服务 (property_service) 由于本文内容较长, ...

  2. 深入理解android卷II 即将发布

    为什么80%的码农都做不了架构师?>>>    深入理解android卷II即将发布.本书关注Android Framework Java层.由于内容庞大,本书只能涵盖其中一部分内容 ...

  3. 《深入理解Android 卷III》第四章 深入理解WindowManagerService

    <深入理解Android 卷III>即将公布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白.即Android Framework中和UI相关的部分. ...

  4. 《深入理解Android 卷III》第八章深入理解Android壁纸

    <深入理解Android 卷III>即将发布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. ...

  5. 《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

    <深入理解Android 卷III>即将发布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. ...

  6. 《深入理解Android 卷III》第七章 深入理解SystemUI

    <深入理解Android 卷III>即将发布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. ...

  7. 《深入理解Android 卷III》第八章深入理解Android壁纸(完整版)

    第8章 深入理解Android壁纸 本章主要内容: ·  讨论动态壁纸的实现. ·  在动态壁纸的基础上讨论静态壁纸的实现. ·  讨论WMS对壁纸窗口所做的特殊处理. 本章涉及的源代码文件名及位置: ...

  8. 深入理解 Android 卷I - 第8章 深入理解Surface系统

    原文地址:http://wiki.jikexueyuan.com/project/deep-android-v1/ 第8章  深入理解Surface系统 本章主要内容 ·  详细分析一个Activit ...

  9. 《深入理解Android 卷III》第七章 深入理解SystemUI(完整版)

    第7章 深入理解SystemUI 本章主要内容: ·  探讨状态栏与导航栏的启动过程 ·  介绍状态栏中的通知信息.系统状态图标等信息的管理与显示原理 ·  介绍导航栏中的虚拟按键.SearchPan ...

最新文章

  1. ubuntu14.04 LTS Python IDE专用编辑器PyCharm开发环境搭建
  2. 笔记-项目管理过程组与知识领域(基础)
  3. 【嵌入式】Libmodbus下载和编译详解
  4. 跳频通信(梅文华)pdf
  5. .Protobuf,GRpc,Maven项目出现UnsatisfiedDependencyException、ClassNotFoundException、BuilderException等异常
  6. bzoj 2179: FFT快速傅立叶 -- FFT
  7. FISCO BCOS 确定性多合约并行(DMC)
  8. 代挂管家易开源7.4+web版
  9. S5PV210体系结构与接口01:ARM体系结构概述
  10. 同城门户同城分类信息网站源码discuz插件+pc端+小程序端+49款插件
  11. 基于springboot点餐系统java web订餐管理平台源码
  12. Opencv系列教程(一):Opencv读取指定文件夹图片、视频,调用摄像头
  13. 印刷ERP解决方案推荐
  14. 微信分享按钮隐藏、显示问题和注意事项
  15. 用html2canvas长按保存h5页面,html2canvas - 微信中长按存图 - 将h5活动结果保存到本地...
  16. 【魔方攻略】SQ1魔方教程(原创)
  17. ValueError.DataFrame columns must be unique for orient=“column“
  18. 借助Microsoft Teams进行在线学习小组的组织与管理
  19. http协议抓包工具
  20. Week 8 Deep learning for object detection

热门文章

  1. C# WPF动画,时钟程序源码分享,闹钟程序源码分享
  2. Android实现打电话,发短信
  3. 微信公众号成绩查询开发-Leo老师
  4. 博途组态阀岛_SMC EX260总线阀岛
  5. STM32 GPIO_INIT()库函数的程序含义
  6. exFAT和NTFS哪个好
  7. 微信小程序的开发(前端)
  8. 问题解决:pycharm pip无法更新,显示更新成功,但版本依然是旧版本
  9. Oracle 按照每小时分组,求每组数量
  10. 微软物联网解决方案-Windows IoT Core