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机制分析的更多相关文章

  1. Android2.2源码属性服务分析

    属性服务property service 大家都知道,在windows中有个注册表,里面存储的是一些键值对.注册表的作用就是:系统或者应用程序将自己的一些属性存储在注册表中,即使系统或应用程序重启,它 ...

  2. 从源码的角度分析ViewGruop的事件分发

    从源码的角度分析ViewGruop的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View ...

  3. java基础解析系列(十)---ArrayList和LinkedList源码及使用分析

    java基础解析系列(十)---ArrayList和LinkedList源码及使用分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder jav ...

  4. 75篇关于Tomcat源码和机制的文章

    75篇关于Tomcat源码和机制的文章 标签: tomcat源码机制 2016-12-30 16:00 10083人阅读 评论(1) 收藏 举报  分类: tomcat内核(82)  版权声明:本文为 ...

  5. 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这样的错误最 ...

  6. 安卓图表引擎AChartEngine(二) - 示例源码概述和分析

    首先看一下示例中类之间的关系: 1. ChartDemo这个类是整个应用程序的入口,运行之后的效果显示一个list. 2. IDemoChart接口,这个接口定义了三个方法, getName()返回值 ...

  7. 第九节:从源码的角度分析MVC中的一些特性及其用法

    一. 前世今生 乍眼一看,该标题写的有点煽情,最近也是在不断反思,怎么能把博客写好,让人能读下去,通俗易懂,深入浅出. 接下来几个章节都是围绕框架本身提供特性展开,有MVC程序集提供的,也有其它程序集 ...

  8. 通过官方API结合源码,如何分析程序流程

    通过官方API结合源码,如何分析程序流程通过官方API找到我们关注的API的某个方法,然后把整个流程执行起来,然后在idea中,把我们关注的方法打上断点,然后通过Step Out,从内向外一层一层分析 ...

  9. HTTP请求库——axios源码阅读与分析

    概述 在前端开发过程中,我们经常会遇到需要发送异步请求的情况.而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率. axios是一个在近些年来非常火的 ...

随机推荐

  1. SpringMVC-常用的注解

    1. RequestParam注解 把请求中的指定名称的参数传递给控制器中的形参赋值 value:请求参数中的名称 require:请求参数中是否必须提供此参数,默认值是true,必须提供 2. Re ...

  2. 为管理复杂组件状态困扰?试试 vue 简单状态管理 Store 模式【转】

    https://juejin.im/post/5cd50849f265da03a54c3877 在 vue 中,通信有几种形式: 父子组件 emit/on vuex 中共享 state 跨组件 Eve ...

  3. 安装 Win7 的系统的时候如何分区

    解决方案 在安装Win7的系统的时候,可以使用下面方法进行分区: 1. 在出现同意许可条款,勾选“我接受许可条款(A)”后,点击下一步,然后继续下面操作: 2. 进入分区界面,点击“驱动器选项(高级) ...

  4. Java动画 重力弹球 如鹏游戏引擎 精灵 设计一个小球加速落地又减速弹起并反复直到停止的Java程序

    package com.swift; import com.rupeng.game.GameCore; public class BouncingBall implements Runnable { ...

  5. jq 下拉框

    <div class="alls"> <div class="item"> <div class="all"& ...

  6. 洛谷 P1516 青蛙的约会

    https://www.luogu.org/problemnew/show/P1516#sub 题意还是非常好理解的..... 假如这不是一道环形的跑道而是一条直线,你会怎样做呢? 如果是我就会列一个 ...

  7. NOIP模拟赛 经营与开发 小奇挖矿

    [题目描述] 4X概念体系,是指在PC战略游戏中一种相当普及和成熟的系统概念,得名自4个同样以“EX”为开头的英语单词. eXplore(探索) eXpand(拓张与发展) eXploit(经营与开发 ...

  8. 【laravel】laravel class 里面定义以head开头的方法会报错

    BadMethodCallException in Macroable.php line 81:Method head does not exist.

  9. 4.layhm框架初始化准备Init

    hm\core\Boot 里 Boot 里run() 自动开起session 设置时区 <?php /** * Created by Haima. * Author:Haima * QQ:228 ...

  10. GoF23种设计模式之行为型模式之备忘录模式

    一.概述         在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象的外部保存这个状态.以便以后可以将该对象恢复到原先保存的状态. 二.适用性 1.当需要保存一个对象在某个时刻的状态( ...