Android2.2源码init机制分析
1 源码分析必备知识
1.1 linux内核链表
Linux内核链表的核心思想是:在用户自定义的结构A中声明list_head类型的成员p,这样每个结构类型为A的变量a中,都拥有同样的成员p,如下: struct A{ int property; struct list_head p; } 其中,list_head结构类型定义如下: struct list_head { struct list_head *next,*prev; }; list_head拥有两个指针成员,其类型都为list_head,分别为前驱指针prev和后驱指针next。 假设: (1)多个结构类型为A的变量a1...an,其list_head结构类型的成员为p1...pn (2)一个list_head结构类型的变量head,代表头节点 使: (1)head.next= p1 ; head.prev = pn (2) p1.prev = head,p1.next = p2; (3)p2.prev= p1 , p2.next = p3; … (n)pn.prev= pn-1 , pn.next = head 以上,则构成了一个循环链表。 因p是嵌入到a中的,p与a的地址偏移量可知,又因为head的地址可知,所以每个结构类型为A的链表节点a1...an的地址也是可以计算出的,从而可实现链表的遍历,在此基础上,则可以实现链表的各种操作。 注:android源码中就是使用的 struct listnode { struct listnode *next,*prev; }; |
1.2 内核链表的遍历
#define list_for_each(pos, head) \ for (pos = (head)->next; prefetch(pos->next), pos != (head); pos = pos->next) 从上可以看出list_for_each其实就是一个for循环, for()实现的就是一个链表的遍历。 同时,为了取得链表中的节点值,还是用了node_to_item函数来取得节点数据。 综合使用如下: list_for_each(node, &service_list) { svc = node_to_item(node, struct service, slist); /*处理函数*/ Fun()…. } |
1.3 linux umask机制
当我们登录系统之后创建一个文件总是有一个默认权限的,那么这个权限是怎么来的呢?这就是umask干的事情。umask设置了用户创建文件的默认 权限,它与chmod的效果刚好相反,umask设置的是权限“补码”,而chmod设置的是文件权限码。umask是从权限中“拿走”相应的位,且文件创建时不能赋予执行权限。
举例:
指定umask(022)。那么就意味这我们在创建目录时,目录的默认权限为777 – 022 = 755——rwxr_xr_x。
值得注意的是:如果我们创建一个文件那么该文件的默认权限是777 – 022 – 111(默认文件不能赋予执行权限) = 644!
2 init.rc 资源配置文件的解析
Init.rc 指示系统在那个阶段,按照什么方式,执行哪些行为。
在认识init.rc之前,我们需要了解keywords.h里面的定义。在那个文件中主要工作是:定义多种keyword(每个keyword分属不同的类型,如:section,option,command),并将每个keyword与其对应的操作函数联系起来。
这个文件分为多个section,每个section由section标识符(on, service,import)的关键字开始,到下一个section的开始的地方结束。
2.1 解析service
这里以zygote为例。
首先在parse_config函数里面调用kw_is(kw, SECTION)找到init.rc的一个section,然后调用parse_new_section(&state, kw, nargs, args)针对不同的section使用不同的解析函数来解析。
由于zygote是一个K_service,所以调用parse_service和parse_line_service来解析service。
在查看这两个函数之前,我们需要理解什么是service。
2.2 service结构体
Init使用了这个结构体来保存与service section相关的信息。详见:init.h::service中。
在这个结构体中比较重要的是:
1、struct listnode slist; 这是一个特殊的结构体,在内核代码中使用得相当广泛,主要用来将结构体(可以是不同类型的)链接成一个双向链表。Init中有一个全局的service_list,专门用来保存解析rc文件后得到的各个service。
2、struct action onrestart; 这里需要注意:虽然关键字onrestart是OPTION,但是通常此关键字后面都会跟着一些COMMAND。此结构体就是用来存储onrestart后面的COMMAND信息的!
struct action { /* node in list of all actions */ struct listnode alist; //所有的action /* node in the queue of pending actions */ struct listnode qlist; //等待执行的action /* node in list of actions for a trigger */ struct listnode tlist; //等待某些条件满足后触发的action unsigned hash; const char *name; /*★ 前面已经说了listnode用于连接结构体。这里会根据OPTION后面的command数量来创建对应的数量的command 结构体,然后组成双向链表 */ struct listnode commands; struct command *current; //指向当前的command结构体 }; |
Command结构体的定义如下:
struct command { /* list of commands in an action */ struct listnode clist; /*在后面分析的parse_line_service函数中的switch语句中的case K_onrestart中会给此函数指针赋值,指向具体的command执行函数*/ int (*func)(int nargs, char **args); int nargs; char *args[1]; }; |
3、unsigned flags,service的属性标记,共有9种:
SVC_DISABLED:不随class自启动(后面会分析class的作用); SVC_ONESHOT:退出后不需要重启,也就是说这个service只启动一次; SVC_RUNNING:正在运行; SVC_RESTARTING:等待重启; SVC_CONSOLE:该service需要使用控制台; SVC_CRITICAL:如果在规定的时间内该service不断重启,则系统会重启并进入恢复模式 SVC_RESET:当系统主动停止一个service的时候使用,这不会让该service变成disable状态,所以,该service可以随着其所属的class的启动而启动; SVC_RC_DISABLED:记住service的disable标记是否由init.rc脚本显示指定的; SVC_RESTART:用于安全地重启一个service; Zygote没有使用任何属性,这表明他它会随着class的处理而自动启动,退出后由init重启;不使用控制台;即使不断重启也不会进入恢复模式。 |
2.3 分析parse_service函数
①声明一个指向service结构体的指针:svc;
②然后进行参数校验;
③去全局链表中查看是否有同名的services存在;
它是通过调用函数service_find_by_name来实现的。在这个函数中使用list_for_each函数来遍历整个链表,进行service名字匹配。
④如果存在了,就直接返回0;否则就为svc分配内存,并给各个字段赋值;
⑤初始化svc->onrestart.commands链表;
list_init(&svc->onrestart.commands); |
⑥把zygote这个service加到全局链表service_list中
list_add_tail(&service_list, &svc->slist); |
总结:parse_service函数只是搭建了一个service的框架,并没有什么实质的解析操作,具体的内容是有parse_line_service函数来填充的。
2.4 分析parse_line_service函数
此函数主要结构为:
kw = lookup_keyword(args[0]); //将rc中的字符型kw转换成keywords.h中定义的枚举值。 /*根据kw的值,进行相应的操作*/ switch(kw){ case: ………. }; |
需要注意的是case K_onrestart //根据onrestart的内容来填充action结构体的内容。
3 init控制service
在解析完init.rc文件后,系统就已经将相关信息写入了相应的队列之中,下一步就是执行这些队列里面的COMMAND了。
同样的以zygote为例。
3.1 启动zygote
在解析init.rc的时候,发现zygote的class名字为main。那么就相当于把zygote服务加入到了全局service_list链表中,并且将它所对应的classname 赋值为main。
那么这个classname的作用是什么呢?其实就是一个标识符,用于区分不同服务的类别。纵观整个init.rc文件,class name 共有两种“main”,“core”。
继续往下分析。到目前为止我们还没发现系统是如何启动服务的,直到init.c的main函数执行的下面的语句:
action_for_each_trigger("boot", action_add_queue_tail); //将init.rc中boot section 的command加入到执行队列中。 |
再转而看init.rc中的boot section:
On boot …… class_start core class_start main |
Class_start 在keywords中表示为一个COMMAND,其对应的处理函数是do_class_start。
所以当init进程执行到:
/* run all property triggers based on current state of the properties */ queue_builtin_action(queue_property_triggers_action, "queue_property_triggers"); |
就会执行do_class_start函数(这里为了表示方便才这样说,其实这里仅仅是将此函数加入到待执行action队列尾,再由后面的execute_one_command函数执行此函数)。
开始分析do_class_start的函数流程。
int do_class_start(int nargs, char **args) { /* Starting a class does not start services * which are explicitly disabled. They must * be started individually. */ /* Args为init.rc文件中class后面的那个参数值(core或main)。下面这个函数将遍历service_list,找到对应名字的service,然后调用service_start_if_not_disable函数——此函数实质上就是调用service_start函数。 */ service_for_each_class(args[1], service_start_if_not_disabled); return 0; } |
Service_start函数的代码较多,就不列出来了,可以在init.c中去找。
下面分析该函数的逻辑:
①设置服务的状态标识符;
②如果这个service已经在运行了,那么就不用处理;
③由于service一般运行在另外的进程中(这个进程也是init的子进程),所以在启动service之前,需要判断对应的可执行文件是否存在,zygote的可执行文件为/system/bin/app_process
if (stat(svc->args[0], &s) != 0) { ERROR("cannot find '%s', disabling '%s'\n", svc->args[0], svc->name); svc->flags |= SVC_DISABLED; return; } |
④判断是否在selinux环境中,并进行相应的操作(这个不是很懂,也不是核心代码,就略过了);
⑤★然后就是真正的核心部分了——使用fork函数创建子进程!
pid = fork(); if (pid == 0) { //表示现在运行在子进程中 struct socketinfo *si; struct svcenvinfo *ei; char tmp[32]; int fd, sz; umask(077); //默认目录权限为700,文件权限为600 if (properties_inited()) { //判断属性是否已经完成初始化了 //得到属性存储空间的信息并加入到环境变量中 get_property_workspace(&fd, &sz); sprintf(tmp, "%d,%d", dup(fd), sz); add_environment("ANDROID_PROPERTY_WORKSPACE", tmp); } //添加环境变量信息 for (ei = svc->envvars; ei; ei = ei->next) add_environment(ei->name, ei->value); //根据socketinfo创建socket,SOCK_STREAM 用于面向流的套接字, SOCK_DGRAM 用于面向数据报的套接字,其可以保存消息界限. Unix 套接字总是可靠的,而且不会重组数据报. for (si = svc->sockets; si; si = si->next) { int socket_type = ( !strcmp(si->type, "stream") ? SOCK_STREAM : (!strcmp(si->type, "dgram") ? SOCK_DGRAM : SOCK_SEQPACKET)); //创建socket,这里创建的socket的域名是PF_UNIX:用于本地进程间的通信 int s = create_socket(si->name, socket_type, si->perm, si->uid, si->gid, si->socketcon ?: scon); if (s >= 0) { //在环境变量中添加socket信息 publish_socket(si->name, s); } } freecon(scon); //不懂,什么意思?网上说是:free memory associated with SELinux security contexts. 暂且当作free看待吧~ scon = NULL; … //判断service是否需要控制终端 if (needs_console) { //调用setsid(),使得当前进程成为会话组长。详细信息涉及到pid,gid,sid等,可自行百度。 setsid(); open_console(); } else { zap_stdio(); } //然后就是设置gid,uid等 …… if (!dynamic_args) { // 执行/system/bin/app_process,这样就进入到app_process的MAIN函数中了。 if (execve(svc->args[0], (char**) svc->args, (char**) ENV) < 0) { ERROR("cannot execve('%s'): %s\n", svc->args[0], strerror(errno)); } } else { ……… } …… //父进程init中,设置service的信息:启动时间,进程号,以及状态等。 svc->time_started = gettime(); svc->pid = pid; svc->flags |= SVC_RUNNING; if (properties_inited()){ //每一个service都有一个属性,zygote的属性为init.svc.zygote, 现在设置它的值为”running”。 notify_service_state(svc->name, "running"); } }?end service start? |
至此service_start函数分析完毕。总结一句话就是:每一个service都是由init进程通过fork和execv函数共同创建的!
3.2 重启zygote
分析完了service的启动过程,我们发现,service中的onrestart并没有使用,why?
从名字可以看出,这应该是用于service重新启动的时候使用的。下面开始分析当zygote死后,其父进程init会进行哪些操作。常识告诉我们,子进程死后,通常会向父进程发送信号,父进程接收到此信号后进行相应的处理。那么这就需要我们找到子进程如何向父进程发送信号,以及父进程是如何接收并处理来自子进程的信号的。
首先,我们回到init.c的main函数中。下面的语句就是init的信号量处理机制:
//执行signal_init_action函数。此函数初始化父子进程信号量处理机制。 queue_builtin_action(signal_init_action, "signal_init"); //signal_init_action函数调用signal_init(). static int signal_init_action(int nargs, char **args) { signal_init(); return 0; } //重点就是这个函数 void signal_init(void) { int s[2]; //声明一个信号量处理结构体 struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = sigchld_handler; /* 定义信号量处理函数,当子进程退出时,调用此函数。 static void sigchld_handler(int s) { write(signal_fd, &s, 1); //向父进程(init)发送信号 } */ act.sa_flags = SA_NOCLDSTOP; sigaction(SIGCHLD, &act, 0); /* create a signalling mechanism for the sigchld handler 使用socketpair创建一对socket,只要一个socket发送数据,另一个socket就一定能收到此数据。 */ if (socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) { signal_fd = s[0]; //发送方socket,一般是子进程使用 signal_recv_fd = s[1]; //接收方socket,一般是父进程(init)使用 fcntl(s[0], F_SETFD, FD_CLOEXEC); fcntl(s[0], F_SETFL, O_NONBLOCK); fcntl(s[1], F_SETFD, FD_CLOEXEC); fcntl(s[1], F_SETFL, O_NONBLOCK); } handle_signal(); //处理信号 } |
从上面的信息我们可以得出:当子进程退出的时候,它会调用sigchld_handler函数向父进程(init)发送信号量。那么init进程又是怎样接收并处理这个信号量的呢?回到init.c中main的for循环中:
nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue; for (i = 0; i < fd_count; i++) { if (ufds[i].revents & POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); else if (ufds[i].fd == get_keychord_fd()) handle_keychord(); /*★get_signal_fd函数返回signal_recv_fd,这里判断是否有来自signal_recv_fd的信息,如果有,那么就调用信号量处理函数handle_signal() */ else if (ufds[i].fd == get_signal_fd()) handle_signal(); } } void handle_signal(void) { char tmp[32]; /* we got a SIGCHLD - reap and restart as needed */ read(signal_recv_fd, tmp, sizeof(tmp)); //读取信号量 while (!wait_for_one_process(0)) //调用该函数进行处理 ; } |
wait_for_one_process函数的代码较多,这里就不列出了,可以自己去Signal_handler.c中查看。
下面简要介绍下该函数的逻辑:
①使用waitpid函数获取死掉进程的pid,status;这里是zygote的PID。
②使用service_find_by_pid函数,获取死掉的那个进程的service;这里是zygote service。
③kill该service创建的所有子进程——这就是zygote死后,整个JAVA世界崩溃的原因。
if (!(svc->flags & SVC_ONESHOT) || (svc->flags & SVC_RESTART)) { kill(-pid, SIGKILL); NOTICE("process '%s' killing any children in process group\n", svc->name); } |
④清理该service创建的所有socket;
⑤如果设置了SVC_CRITIVAL标志,则四分钟内该service重启次数操作4次的话,系统将会进入recovery模式。根据init.rc来看。只有:ueventd、healthd、healthd-charger、servicemanager这四个服务享有此待遇。
if ((svc->flags & SVC_CRITICAL) && !(svc->flags & SVC_RESTART)) { if (svc->time_crashed + CRITICAL_CRASH_WINDOW >= now) { if (++svc->nr_crashed > CRITICAL_CRASH_THRESHOLD) { ERROR("critical process '%s' exited %d times in %d minutes; " "rebooting into recovery mode\n", svc->name, CRITICAL_CRASH_THRESHOLD, CRITICAL_CRASH_WINDOW / 60); android_reboot(ANDROID_RB_RESTART2, 0, "recovery"); return 0; } } else { svc->time_crashed = now; svc->nr_crashed = 1; } } |
⑥设置标识为SVC_RESTARTING,然后执行该service onrestart中的COMMAND。
svc->flags |= SVC_RESTARTING; /* Execute all onrestart commands for this service. */ /*★这里onrestart终于派上用场了!*/ list_for_each(node, &svc->onrestart.commands) { cmd = node_to_item(node, struct command, clist); cmd->func(cmd->nargs, cmd->args); //调用相应函数处理command } |
⑦设置service的状态为restarting,并退出。
通过上面的分析,就可以知道service结构体中的onrestart变量的作用了。但是service(zygote)本身又在哪重启呢?
在init.c的main函数的for循环中有如下语句:
execute_one_command();//此函数的逻辑见下面分析。 restart_processes(); //★在这里重启所有标识为restarting的services! |
上面有一个很重要的函数execute_one_command();此函数详细代码如下:
void execute_one_command(void) { int ret; /*如果当前action为空,或者当前command为空,或者当前command是当前action的最后一个命令,那么就在队列头取出一个action。如果取出的action为空(表示队列中已经没有需要执行的action了),那么就直接返回,否则取得此action的第一条command*/ if (!cur_action || !cur_command || is_last_command(cur_action, cur_command)) { cur_action = action_remove_queue_head(); cur_command = NULL; if (!cur_action) return; INFO("processing action %p (%s)\n", cur_action, cur_action->name); cur_command = get_first_command(cur_action); } else { //如果当前action不为空,且command不为空,且不是最后一条command,那么就执行取得当前action的后一条command. cur_command = get_next_command(cur_action, cur_command); } //如果command为空,就直接返回,否者执行此command。 if (!cur_command) return; ret = cur_command->func(cur_command->nargs, cur_command->args); INFO("command '%s' r=%d\n", cur_command->args[0], ret); } |
到这里我们就分析完了整个service的重启过程。
4 完整的init进程分析
前面我们是站在service的角度来看init进程如何运行的。现在我们来站在init自己的角度来分析它的整个逻辑。在init.c的main函数中:
①挂载必要的文件系统——如创建一些根目录下的目录等;
②重定向标注输入/输出/错误输出到/dev/_null_;
open_devnull_stdio(); |
③设置init的日志输出设备(klog_fd)为/dev/__kmsg__,设置完后马上unlink,其他进程就无法打开这个文件读取日志信息了;
klog_init(); |
④一些初始化任务;
//属性服务的初始化操作,主要是分配内存什么的 property_init(); //得到硬件名字和版本号 get_hardware_name(hardware, &revision); //处理内核命令行参数 process_kernel_cmdline(); |
⑤分析init.rc和init.hardware.rc;
⑥将init.rc中early-init section中的action加入到全局队列action_queue中;
action_for_each_trigger("early-init", action_add_queue_tail); //此函数的作用就是将init.rc中early-init section中的action加入到全局队列action-queue中,后面类似。 |
⑦通过调用queue_builtin_action()把wait_for_coldboot_done_action, mix_hwrng_into_linux_rng, keychord_init_action, console_init_action,加到action_queue 里;
queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done"); //此函数的作用是将第二个参数name = “wait_for_coldboot_done”的action加入到action_queue中,并指定该action的command的执行函数为第一个参数wait_for_coldboot_done_action。后面类似。 |
⑧将init.rc中init section中的action加入到全局队列action_queue中;
⑨如果不处于充电模式(注:这里的充电模式,是关闭手机后,进行充电时的系统状态,而不是开机充电的状态,因为在开机完成后,init进程也早已完成了初始化任务了),则依次将init.rc中的early-fs, fs, post-fs, post-fs-data section中的action加入到action_queue中;
⑩通过调用queue_builtin_action()把mix_hwrng_into_linux_rng(第二次加入),property_service_init,signal_init,check_startup加到action_queue 里;这里简要说明一下:
mix_hwrng_into_linux_rng:主要用于随机数发生器,android的随机数发生器有两种/dev/hw_random or /dev/random,这里为了加强随机性,将hw_random生成的随机数中的512bytes写入到Linux RNG's via /dev/urandom中。 property_service_init:属性服务的初始化 signal_init:信号量机制的初始化,创建socket对用于init进程同其子进程通信 |
⑪如果不处于充电模式,那么就将eearly-boot, boot section中的action加入到action_queue中;否者将charge section中的action加入到队列中;
⑫通过调用queue_builtin_action()把queue_property_triggers加到action_queue 里;就是执行基于当前所有属性状态的所有属性触发器(trigger)
⑬如果已经定义了bootchart,那么就将init.rc中的bootchart_init section加入到action_queue中;
⑭开始for循环;
execute_one_command();//执行action_queue队列中当前action的一条command; restart_processes();//执行list_service中所有flags为restarting的services; //然后根据需要来设置ufds[], 分别监听来自属性服务器,由soketpair创建的另一个socket,keychord设备这三个事件 //然后调用poll等待监听事情的发生,如果有来自上面监听的事件,则处理事件,否则,返回for循环,做下一个action nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue; for (i = 0; i < fd_count; i++) { if (ufds[i].revents & POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); else if (ufds[i].fd == get_keychord_fd()) handle_keychord(); else if (ufds[i].fd == get_signal_fd()) handle_signal(); } } |
Android2.2源码init机制分析的更多相关文章
- Android2.2源码属性服务分析
属性服务property service 大家都知道,在windows中有个注册表,里面存储的是一些键值对.注册表的作用就是:系统或者应用程序将自己的一些属性存储在注册表中,即使系统或应用程序重启,它 ...
- 从源码的角度分析ViewGruop的事件分发
从源码的角度分析ViewGruop的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View ...
- java基础解析系列(十)---ArrayList和LinkedList源码及使用分析
java基础解析系列(十)---ArrayList和LinkedList源码及使用分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder jav ...
- 75篇关于Tomcat源码和机制的文章
75篇关于Tomcat源码和机制的文章 标签: tomcat源码机制 2016-12-30 16:00 10083人阅读 评论(1) 收藏 举报 分类: tomcat内核(82) 版权声明:本文为 ...
- Ubuntu 12.04 Android2.2源码make** /classes-full-debug.jar Error 41错误解决
出现make: *** [out/target/common/obj/APPS/CMParts_intermediates/classes-full-debug.jar] Error 41这样的错误最 ...
- 安卓图表引擎AChartEngine(二) - 示例源码概述和分析
首先看一下示例中类之间的关系: 1. ChartDemo这个类是整个应用程序的入口,运行之后的效果显示一个list. 2. IDemoChart接口,这个接口定义了三个方法, getName()返回值 ...
- 第九节:从源码的角度分析MVC中的一些特性及其用法
一. 前世今生 乍眼一看,该标题写的有点煽情,最近也是在不断反思,怎么能把博客写好,让人能读下去,通俗易懂,深入浅出. 接下来几个章节都是围绕框架本身提供特性展开,有MVC程序集提供的,也有其它程序集 ...
- 通过官方API结合源码,如何分析程序流程
通过官方API结合源码,如何分析程序流程通过官方API找到我们关注的API的某个方法,然后把整个流程执行起来,然后在idea中,把我们关注的方法打上断点,然后通过Step Out,从内向外一层一层分析 ...
- HTTP请求库——axios源码阅读与分析
概述 在前端开发过程中,我们经常会遇到需要发送异步请求的情况.而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率. axios是一个在近些年来非常火的 ...
随机推荐
- 用fmt标签对EL表达式取整
本篇文章转载自:https://blog.csdn.net/u013400939/article/details/47948541 一般来说我们是无法实现EL表达式取整的.对于EL表达式的除法而言,他 ...
- 01_1_准备ibatis环境
01_1_准备ibatis环境 1. 搭建环境:导入相关的jar包 mysql-connector-java-5.1.5-bin.jar(mysql)或者ojdbc6.jar(oracle).ibat ...
- ssh整合思想 Spring与Hibernate的整合ssh整合相关JAR包下载 .MySQLDialect方言解决无法服务器启动自动update创建表问题
除之前的Spring相关包,还有structs2包外,还需要Hibernate的相关包 首先,Spring整合其他持久化层框架的JAR包 spring-orm-4.2.4.RELEASE.jar ( ...
- eclipse 中main()函数中的String[] args如何使用?通过String[] args验证账号密码的登录类?静态的主方法怎样才能调用非static的方法——通过生成对象?在类中制作一个方法——能够修改对象的属性值?
eclipse 中main()函数中的String[] args如何使用? 右击你的项目,选择run as中选择 run configuration,选择arguments总的program argu ...
- 【动态规划】bzoj1705: [Usaco2007 Nov]Telephone Wire 架设电话线
可能是一类dp的通用优化 Description 最近,Farmer John的奶牛们越来越不满于牛棚里一塌糊涂的电话服务 于是,她们要求FJ把那些老旧的电话线换成性能更好的新电话线. 新的电话线架设 ...
- java中的jdbc操作
package demo; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedSta ...
- day12-迭代器
迭代器的概念 内部含有_next_和_iter_方法的就是迭代器. 可以被for循环的都是可迭代的,只有是可迭代对象,才能用for循环. 可迭代的内部都有_iter_方法——可迭代协议. 只要是迭代器 ...
- python数据类型之字符串(str)和其常用方法
字符串是有序的,不可变的. 下面的例子说明了字符串是不可变的 name = 'alex' name = 'Jack' """ 并没有变,只是给name开启了一块新内存,储 ...
- python之序列化
什么叫序列化? 序列化是指把内存里的数据类型转变成字符串,以使其能存储到硬盘或通过网络传输到远程,因为硬盘或网络传输时只能接受bytes. 把字符转换成内存数据类型,叫反序列化. 为什么要序列化? 你 ...
- LeetCode(168) Excel Sheet Column Title
题目 Given a positive integer, return its corresponding column title as appear in an Excel sheet. For ...