Redis源代码解析:13Redis中的事件驱动机制
Redis中。处理网络IO时,採用的是事件驱动机制。但它没有使用libevent或者libev这种库,而是自己实现了一个很easy明了的事件驱动库ae_event,主要代码只400行左右。
没有选择libevent或libev的原因大概在于。这些库为了迎合通用性造成代码庞大,并且当中的非常多功能,比方监控子进程,复杂的定时器等。这些都不是Redis所须要的。
Redis中的事件驱动库仅仅关注网络IO,以及定时器。该事件库处理以下两类事件:
a:文件事件(file event):用于处理Redisserver和client之间的网络IO。
b:时间事件(time eveat):Redis服务器中的一些操作(比方serverCron函数)须要在给定的时间点运行,而时间事件就是处理这类定时操作的。
事件驱动库的代码主要是在src/ae.c中实现的。
一:文件事件
Redis基于Reactor模式开发了自己的网络事件处理器,也就是文件事件处理器。
文件事件处理器使用IO多路复用技术。同一时候监听多个套接字,并为套接字关联不同的事件处理函数。
当套接字的可读或者可写事件触发时,就会调用对应的事件处理函数。
Redis使用的IO多路复用技术主要有:select、epoll、evport和kqueue等。每一个IO多路复用函数库在Redis源代码中都相应一个单独的文件,比方ae_select.c,ae_epoll.c, ae_kqueue.c等。
这些多路复用技术,依据不同的操作系统,Redis依照一定的优先级。选择当中的一种使用。在ae.c中。是这样实现的:
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
注意这里是include的.c文件,因此。使用哪种多路复用技术。是在编译阶段就决定了的。
文件事件由结构体aeFileEvent表示,它的定义例如以下:
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
当中mask表示描写叙述符注冊的事件。能够是AE_READABLE。AE_WRITABLE或者是AE_READABLE|AE_WRITABLE。
rfileProc和wfileProc分别表示可读和可写事件的回调函数。
clientData是用户提供的数据,在调用回调函数时被当做參数。注意。该数据是可读和可写事件共用的。
二:时间事件
Redis的时间事件主要有一次性事件和周期性事件两种。一次性时间事件仅触发一次。而周期性事件每隔一段时间就触发一次。
时间事件由aeTimeEvent结构体表示,它的定义例如以下:
/* Time event structure */
typedef struct aeTimeEvent {
long long id; /* time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *next;
} aeTimeEvent;
id用于标识时间事件。id号依照从小到大的顺序递增,新时间事件的id号比旧时间事件的id号要大。
when_sec和when_ms表示时间事件的下次触发时间,实际上就是一个Unix时间戳,when_sec记录它的秒数,when_ms记录它的毫秒数。因此触发时间是一个绝对值,而非相对值。
timeProc是时间事件处理器,也就是时间事件触发时的回调函数;
finalizerProc是删除该时间事件时要调用的函数;
clientData是用户提供的数据。在调用timeProc和finalizerProc时。作为參数;
全部的时间事件aeTimeEvent结构被组织成一个链表。next指针就运行链表中,当前aeTimeEvent结构的后继结点。
aeTimeEvent结构链表是一个无序链表。也就是说它并不依照事件的触发时间而排序。
每当创建一个新的时间事件aeTimeEvent结构时,该结构就插入链表的头部。因此。当监控时间事件时,须要遍历整个链表。查找全部已到达的时间事件,并调用对应的事件处理器。
在眼下版本号中,正常模式下的Redisserver仅仅使用serverCron一个时间事件。而在benchmark模式下,server也仅仅使用两个时间事件。
因此。时间事件链表的这样的设计尽管简单粗暴,可是也能满足性能需求。
三:事件循环结构
在事件驱动的实现中,须要有一个事件循环结构来监控调度全部的事件,比方Libevent库中的event_base,libev中的ev_loop等。
在Redis中的事件驱动库中,事件循环结构是由aeEventLoop结构体实现的。aeEventLoop结构是Redis中事件驱动机制的主要数据结构。它的定义例如以下:
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
time_t lastTime; /* Used to detect system clock skew */
aeFileEvent *events; /* Registered events */
aeFiredEvent *fired; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
} aeEventLoop;
events是aeFileEvent结构的数组,每一个aeFileEvent结构表示一个注冊的文件事件。events数组以描写叙述符的值为下标。
fired是aeFiredEvent结构的数组,aeFiredEvent结构表示一个触发的文件事件。
结构中包括了描写叙述符,以及其上已经触发的事件。该数组不是以描写叙述符的值为下标。而是依次保存全部触发的文件事件。当处理事件时,轮训fired数组中的每一个元素。然后依次处理。
setsize表示eventLoop->events和eventLoop->fired数组的大小。因此。setsize- 1就表示所能处理的最大的描写叙述符的值。
lastTime:为了处理时间事件而记录的Unix时间戳。主要为了在系统时间被调整时可以尽快的处理时间事件;
timeEventHead:时间事件aeTimeEvent结构组成的链表的头指针。
timeEventNextId:下个时间事件的ID,该ID依次递增,因此当前时间事件的最大ID为timeEventNextId-1;
stop:是否停止事件监控;
maxfd:当前处理的最大的描写叙述符的值,主要是在select中使用。
beforesleep:每次监控事件触发之前。须要调用的函数。
apidata表示详细的底层多路复用所使用的数据结构,比方对于select来说。该结构中保存了读写描写叙述符数组;对于epoll来说。该结构中保存了epoll描写叙述符,以及epoll_event结构数组;
四:监控调度时间事件
监控调度时间事件是由函数processTimeEvents实现的,它的代码例如以下:
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = 0;
aeTimeEvent *te;
long long maxId;
time_t now = time(NULL); /* If the system clock is moved to the future, and then set back to the
* right value, time events may be delayed in a random way. Often this
* means that scheduled operations will not be performed soon enough.
*
* Here we try to detect system clock skews, and force all the time
* events to be processed ASAP when this happens: the idea is that
* processing events earlier is less dangerous than delaying them
* indefinitely, and practice suggests it is. */
if (now < eventLoop->lastTime) {
te = eventLoop->timeEventHead;
while(te) {
te->when_sec = 0;
te = te->next;
}
}
eventLoop->lastTime = now; te = eventLoop->timeEventHead;
maxId = eventLoop->timeEventNextId-1;
while(te) {
long now_sec, now_ms;
long long id; if (te->id > maxId) {
te = te->next;
continue;
}
aeGetTime(&now_sec, &now_ms);
if (now_sec > te->when_sec ||
(now_sec == te->when_sec && now_ms >= te->when_ms))
{
int retval; id = te->id;
retval = te->timeProc(eventLoop, id, te->clientData);
processed++;
/* After an event is processed our time event list may
* no longer be the same, so we restart from head.
* Still we make sure to don't process events registered
* by event handlers itself in order to don't loop forever.
* To do so we saved the max ID we want to handle.
*
* FUTURE OPTIMIZATIONS:
* Note that this is NOT great algorithmically. Redis uses
* a single time event so it's not a problem but the right
* way to do this is to add the new elements on head, and
* to flag deleted elements in a special way for later
* deletion (putting references to the nodes to delete into
* another linked list). */
if (retval != AE_NOMORE) {
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
aeDeleteTimeEvent(eventLoop, id);
}
te = eventLoop->timeEventHead;
} else {
te = te->next;
}
}
return processed;
}
首先推断系统时间是否被调整了。将当前时间now,与上次记录的时间戳eventLoop->lastTime相比較。假设now小于eventLoop->lastTime。说明系统时间被调整到过去了,比方由201603312030调整到了201603312000了。这样的情况下,直接将全部事件的触发时间的秒数清0,这意味着全部的时间事件都会马上触发。
之所以这么做。是由于提前处理比延后处理的危急性要小;
然后更新eventLoop->lastTime为now;
接下来,先记录当前的maxId。之所以这么做,是由于有时间事件触发后。要又一次回到链表头结点開始处理。而在时间事件的触发回调函数中。有可能注冊了新的时间事件,成为新的链表头结点,这就可能导致会无限处理下去。为了防止这样的情况发生。记录当前的maxId,仅仅处理当前的时间事件;
轮训链表eventLoop->timeEventHead,针对当中的每个事件节点te,假设te的id大于maxId。说明该事件,是在之前已经触发的时间事件的回调函数中注冊的。不处理这种事件,直接处理下一个;
然后得到当前时间,推断当前时间是否已经超过了te的触发时间,若是。说明该事件须要触发,调用触发回调函数te->timeProc,该函数的返回值为retval;
假设retval是AE_NOMORE,说明触发的时间事件是一次性事件,直接从链表中删除;否则。说明该事件是周期性事件。将其触发时间更改为当前时间加上retval;
事件触发后,链表已经被改动了,要又一次回到链表头结点開始处理。由于Redis中仅仅有一个时间事件,因此採用了这样的简单粗暴的算法。更好的处理方式是处理完当前事件后。标记该节点须要删除(比方在还有一个链表中保存该节点的指针),然后接着处理下一个节点,全部节点处理完之后,将标记为删除的节点统一删除就可以。
最后返回触发的事件总数。
五:监控调度全部事件
监控调度全部事件是由函数aeProcessEvents实现的,它的代码例如以下:
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents; /* Nothing to do? return ASAP */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0; /* Note that we want call select() even if there are no
* file events to process as long as we want to process time
* events, in order to sleep until the next time event is ready
* to fire. */
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp; if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);
if (shortest) {
long now_sec, now_ms; /* Calculate the time missing for the nearest
* timer to fire. */
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
tvp->tv_sec = shortest->when_sec - now_sec;
if (shortest->when_ms < now_ms) {
tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
tvp->tv_sec --;
} else {
tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
}
if (tvp->tv_sec < 0) tvp->tv_sec = 0;
if (tvp->tv_usec < 0) tvp->tv_usec = 0;
} else {
/* If we have to check for events but need to return
* ASAP because of AE_DONT_WAIT we need to set the timeout
* to zero */
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* Otherwise we can block */
tvp = NULL; /* wait forever */
}
} numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0; /* note the fe->mask & mask & ... code: maybe an already processed
* event removed an element that fired and we still didn't
* processed, so we check if the event is still valid. */
if (fe->mask & mask & AE_READABLE) {
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
}
/* Check time events */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop); return processed; /* return the number of processed file/time events */
}
依据flags处理不同的事件:
假设flags为0。则该函数直接返回。
假设flags中设置了AE_ALL_EVENTS,则处理全部的文件事件和时间事件;
假设flags中设置了AE_FILE_EVENTS。则处理全部的文件事件。
假设flags中设置了AE_TIME_EVENTS,则处理全部的时间事件;
假设flags中设置了AE_DONT_WAIT,则调用多路复用函数时。不会堵塞等
待事件的触发。将全部已触发的事件处理完后马上返回。
眼下在Redis中,调用aeProcessEvents时设置的flags仅仅有AE_ALL_EVENTS和
AE_FILE_EVENTS|AE_DONT_WAIT两种。
函数中,首先假设flags中既没有设置AE_TIME_EVENTS。也没有设置AE_FILE_EVENTS。则该函数直接返回0.
接下来,假设已经注冊过文件事件,或者须要处理时间事件且不是AE_DONT_WAIT。则须要调用底层多路复用函数aeApiPoll。因此须要计算调用aeApiPoll函数时,最长堵塞时间tvp。该值是由最早要触发的时间事件(假设有的话)决定的。
假设须要处理时间事件且不是AE_DONT_WAIT。这样的情况下,无论有没有文件事件,都要堵塞一段时间。堵塞的时间依据shortest得到,shortest是通过调用aeSearchNearestTimer得到的最早要触发的时间事件。得到shortest后,计算得出其触发时间距离当前时间的差值,该差值就是堵塞时间tvp。
否则。假设注冊过文件事件,而且flags中设置了AE_DONT_WAIT。则将tvp中的值设置为0。表示全然不堵塞;
假设注冊过文件事件,可是flags中没有设置AE_DONT_WAIT,则将tvp置为NULL,表示一直堵塞,直到有文件事件触发;
得到最长堵塞时间tvp之后。以tvp为參数调用aeApiPoll等待文件事件的触发。该函数由不同的底层多路复用函数实现。终于都返回触发的文件事件总数numevents,并将触发的事件和描写叙述符,依次记录到eventLoop->fired中。
接下来。依次轮训eventLoop->fired中的前numevents个元素。调用对应的事件回调函数。注意。假设一个套接字又可读又可写的话,那么server将先处理可读事件,然后在处理可写事件。
触发的文件事件是依次处理的,假设某个文件事件的处理时间过长,就会影响到下一个事件的处理。在事件驱动的实现中,要由用户保证事件回调函数可以高速返回,而不堵塞。
注意。有这样一种情况。比方描写叙述符3和4都有事件触发了,在3的事件回调函数中,调用aeDeleteFileEvent将4的注冊事件删除了。这样在处理描写叙述符4时,就不应该再次调用4的回调函数了。
所以,每次调用事件回调函数之前,都推断该描写叙述符上的注冊事件是否还有效。并且假设可读和可写事件的回调函数同样的话,仅仅能调用一次该函数。
处理完文件事件之后(或者没有文件事件。而只堵塞了tvp的时间),假设flags中设置了AE_TIME_EVENTS。则调用processTimeEvents处理时间事件,因已经堵塞了tvp的时间,因此此时肯定有触发的时间事件。最后。返回全部触发的事件总数。
由于时间事件在文件事件之后处理,而且事件之间不会出现抢占,所以时间事件的实际处理时间。一般会比时间事件设定的到达时间稍晚一些。
再次强调一点:对文件事件和时间事件的处理都是同步、有序、原子地运行的,server不会中途中断事件处理,也不会对事件进行抢占。
因此。无论是文件事件的回调函数,还是时间事件的回调函数。都须要尽可地降低程序的堵塞时间,从而降低造成事件饥饿的可能性。比方,在命令回复回调函数中,将一个命令回复写入到client套接字时。假设写人字节数超过了一个预设常量的话。命令回复函数就会主动用break跳出写人循环。将余下的数据留到下次再写。
另外,时间事件也会将很耗时的持久化操作放到子线程或者子进程运行。
六:事件循环监控
事件循环监控是由函数aeMain实现的,它的代码例如以下:
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
仅仅要eventLoop->stop不为1。则持续调用aeProcessEvents监控调度全部事件的触发。正常情况下,在Redisserver中,eventLoop->stop永远不可能为1。
在Redisserver的主函数中,全部初始化工作完毕之后,就会调用该函数。监控全部事件的触发。
七:样例:ECHOserver
以下是使用Redis的事件驱动库,实现的一个简单echoserver:
#define SERVER_PORT 9998 typedef struct
{
char clientaddr[INET_ADDRSTRLEN];
int port;
char buf[1024];
}Userbuf; void setunblock(int fd)
{
int flags;
if ((flags = fcntl(fd, F_GETFL)) == -1)
{
perror("fcntl(F_GETFL) error");
return;
} flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) == -1)
{
perror("fcntl(F_SETFL) error");
return;
}
return;
} void acceptfun(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask)
{
int acceptfd = -1;
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr); acceptfd = accept(fd, (struct sockaddr *)&cliaddr, &addrlen);
if (acceptfd < 0)
{
perror("accept error\n");
return;
} Userbuf *usrbuf = calloc(1, sizeof(Userbuf));
printf("calloc %p\n", usrbuf);
inet_ntop(AF_INET, &cliaddr.sin_addr, usrbuf->clientaddr, INET_ADDRSTRLEN),
usrbuf->port = ntohs(cliaddr.sin_port);
printf("\naccept from <%s:%d>\n", usrbuf->clientaddr, usrbuf->port); setunblock(acceptfd); if (aeCreateFileEvent(eventLoop, acceptfd, AE_READABLE, readfun, usrbuf) != AE_OK)
{
perror("aeCreateFileEvent error");
close(acceptfd);
printf("free %p\n", usrbuf);
free(usrbuf);
return;
}
return;
} void readfun(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask)
{
char readbuf[1024] = {};
int len = -1;
Userbuf *usrbuf = (Userbuf *)clientData; if ((len = read(fd, readbuf, 1024)) > 0)
{
printf("read from <%s:%d>: %s\n", usrbuf->clientaddr, usrbuf->port, readbuf); memcpy(usrbuf->buf, readbuf, 1024);
if (aeCreateFileEvent(eventLoop, fd, AE_WRITABLE, writefun, clientData) != AE_OK)
{
printf("aeCreateFileEvent error\n");
goto END; }
else
return;
}
else if (len == 0)
{
printf("close link from %s\n", usrbuf->buf);
goto END;
}
else
{
printf("read error from %s\n", usrbuf->buf);
goto END;
} END:
close(fd);
aeDeleteFileEvent(eventLoop, fd, AE_READABLE);
aeDeleteFileEvent(eventLoop, fd, AE_WRITABLE);
printf("free %p\n", clientData);
free(clientData);
return;
} void writefun(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask)
{
int len = 0;
char *buf = ((Userbuf *)clientData)->buf;
len = strlen(buf); printf("write to client: %s\n", buf);
if(write(fd, buf, len) != len)
{
perror("write error"); close(fd);
aeDeleteFileEvent(eventLoop, fd, AE_READABLE);
aeDeleteFileEvent(eventLoop, fd, AE_WRITABLE); printf("free %p\n", clientData);
free(clientData);
}
aeDeleteFileEvent(eventLoop, fd, AE_WRITABLE);
} int main()
{
int listenfd;
aeEventLoop *eventloop = NULL;
struct sockaddr_in seraddr; listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0)
{
perror("socket error");
return -1;
} seraddr.sin_family = AF_INET;
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
seraddr.sin_port = htons(SERVER_PORT); if (bind(listenfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) < 0)
{
perror("bind error");
close(listenfd);
return -1;
} if (listen(listenfd, 5) < 0)
{
perror("listen error");
close(listenfd);
return -1;
} eventloop = aeCreateEventLoop(1024);
if (eventloop == NULL)
{
printf("aeCreateEventLoop error\n");
close(listenfd);
return -1;
} if (aeCreateFileEvent(eventloop, listenfd, AE_READABLE, acceptfun, NULL) != AE_OK)
{
perror("aeCreateFileEvent error");
close(listenfd);
aeDeleteEventLoop(eventloop);
return -1;
} aeMain(eventloop);
return 0;
}
这里要注意的是,对于同一个acceptfd,调用aeCreateFileEvent函数。分别注冊可读事件和可写事件时。其clientData是共享的。
假设在注冊可写事件时,改动了clientData,则可读事件的clientData也对应改变,这是由于一个描写叙述符仅仅有一个aeFileEvent结构。
client的代码依据Webbench改写。详细代码见:
https://github.com/gqtc/redis-3.0.5/blob/master/redis-3.0.5/tests/hhunittest/test_ae_client.c
其它有关事件驱动的代码实现。能够參考:
https://github.com/gqtc/redis-3.0.5/blob/master/redis-3.0.5/src/ae.c
https://github.com/gqtc/redis-3.0.5/blob/master/redis-3.0.5/src/ae_epoll.c
https://github.com/gqtc/redis-3.0.5/blob/master/redis-3.0.5/src/ae_select.c
Redis源代码解析:13Redis中的事件驱动机制的更多相关文章
- Redis源码解析:13Redis中的事件驱动机制
Redis中,处理网络IO时,采用的是事件驱动机制.但它没有使用libevent或者libev这样的库,而是自己实现了一个非常简单明了的事件驱动库ae_event,主要代码仅仅400行左右. 没有选择 ...
- 使用具体解释及源代码解析Android中的Adapter、BaseAdapter、ArrayAdapter、SimpleAdapter和SimpleCursorAdapter
Adapter相当于一个数据源,能够给AdapterView提供数据.并依据数据创建相应的UI.能够通过调用AdapterView的setAdapter方法使得AdapterView将Adapter作 ...
- 源代码解析Android中View的layout布局过程
Android中的Veiw从内存中到呈如今UI界面上须要依次经历三个阶段:量算 -> 布局 -> 画图,关于View的量算.布局.画图的整体机制可參见博文 < Android中Vie ...
- 深度解析VC中的消息传递机制
摘要:Windows编程和Dos编程,一个很大的区别就是,Windows编程是事件驱动,消息传递的.所以,要学好Windows编程,必须 对消息机制有一个清楚的认识,本文希望能够对消息的传递做一个全面 ...
- 深入源代码解析Android中的Handler,Message,MessageQueue,Looper
本文主要是对Handler和消息循环的实现原理进行源代码分析.假设不熟悉Handler能够參见博文< Android中Handler的使用>,里面对Android为何以引入Handler机 ...
- [原理][源代码解析]spring中@Transactional,Propagation.SUPPORTS,以及 Hibernate Session,以及jdbc Connection关系---转载
问题: 一. 1. Spring 如何处理propagation=Propagation.SUPPORTS? 2. Spring 何时生成HibernateSession ? 3. propagati ...
- 深入解析kubernetes中的选举机制
Overview 在 Kubernetes的 kube-controller-manager , kube-scheduler, 以及使用 Operator 的底层实现 controller-rumt ...
- Android中HandlerThread的使用及源代码解析
关于Hanlder的基本使用能够參见博文<Android中Handler的使用>,假设想了解Handler.Looper.Thread等的相互关系以及内部实现原理能够參见博文<深入源 ...
- Android View体系(八)从源代码解析View的layout和draw流程
相关文章 Android View体系(一)视图坐标系 Android View体系(二)实现View滑动的六种方法 Android View体系(三)属性动画 Android View体系(四)从源 ...
随机推荐
- getResourceAsStream 地址
getResourceAsStream ()返回的是inputstream getResource()返回:URL Class.getResource("") 返回的是当前C ...
- [P2698][USACO12MAR]花盆Flowerpot
Link: P2698 传送门 Solution: 对于可行区间$[L,R]$,随着$L$的递增$R$不会递减 因此可以使用尺取法来解决此题:不断向右移动左右指针,复杂度保持线性 同时为了维护区间内的 ...
- 【DFS】Paintball(6-22)
[UVA11853]Paintball 算法入门经典第6章6-22(P175) 题目大意:有一个1000*1000的正方形战场,西南角坐标(0,0),西北角坐标(0,1000),有n个敌人,每个敌人处 ...
- 20162312 2016-2017-2《Java程序设计》课程总结
一.每周作业链接汇总 预备作业01 写的是有关老师和学生的关系: 预备作业02 如何做中学: 预备作业03 实验楼学习linux环境: 第一周作业 java入门,虚拟机等课前准备: 第二周作业 掌握J ...
- vue-cli创建vue项目
原文出处:https://segmentfault.com/a/1190000008922234 第一步 node环境安装 1.1 如果本机没有安装node运行环境,请下载node 安装包进行安装1. ...
- ncnn阅读 - CMakeLists.txt
CMAKE_TOOLCHAIN_FILE This variable is specified on the command line when cross-compiling with CMake. ...
- Problem B: 年龄问题
#include<stdio.h> int xx(int n,int m,int k) { int a; ) { a=k; } else { a=xx(n-,m,k)+m; } retur ...
- codevs 1959 拔河比赛--判断背包内刚好装满n/2个物品
1959 拔河比赛 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 黄金 Gold 题解 查看运行结果 题目描述 Description 一个学校举行拔河比赛,所有的 ...
- codevs 1297 硬币
1297 硬币 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 黄金 Gold 题解 查看运行结果 题目描述 Description 我们知道即使是同一种面值的硬币, ...
- HDU 5288 OO’s Sequence 水题
OO's Sequence 题目连接: http://acm.hdu.edu.cn/showproblem.php?pid=5288 Description OO has got a array A ...