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中的事件驱动机制的更多相关文章

  1. Redis源码解析:13Redis中的事件驱动机制

    Redis中,处理网络IO时,采用的是事件驱动机制.但它没有使用libevent或者libev这样的库,而是自己实现了一个非常简单明了的事件驱动库ae_event,主要代码仅仅400行左右. 没有选择 ...

  2. 使用具体解释及源代码解析Android中的Adapter、BaseAdapter、ArrayAdapter、SimpleAdapter和SimpleCursorAdapter

    Adapter相当于一个数据源,能够给AdapterView提供数据.并依据数据创建相应的UI.能够通过调用AdapterView的setAdapter方法使得AdapterView将Adapter作 ...

  3. 源代码解析Android中View的layout布局过程

    Android中的Veiw从内存中到呈如今UI界面上须要依次经历三个阶段:量算 -> 布局 -> 画图,关于View的量算.布局.画图的整体机制可參见博文 < Android中Vie ...

  4. 深度解析VC中的消息传递机制

    摘要:Windows编程和Dos编程,一个很大的区别就是,Windows编程是事件驱动,消息传递的.所以,要学好Windows编程,必须 对消息机制有一个清楚的认识,本文希望能够对消息的传递做一个全面 ...

  5. 深入源代码解析Android中的Handler,Message,MessageQueue,Looper

    本文主要是对Handler和消息循环的实现原理进行源代码分析.假设不熟悉Handler能够參见博文< Android中Handler的使用>,里面对Android为何以引入Handler机 ...

  6. [原理][源代码解析]spring中@Transactional,Propagation.SUPPORTS,以及 Hibernate Session,以及jdbc Connection关系---转载

    问题: 一. 1. Spring 如何处理propagation=Propagation.SUPPORTS? 2. Spring 何时生成HibernateSession ? 3. propagati ...

  7. 深入解析kubernetes中的选举机制

    Overview 在 Kubernetes的 kube-controller-manager , kube-scheduler, 以及使用 Operator 的底层实现 controller-rumt ...

  8. Android中HandlerThread的使用及源代码解析

    关于Hanlder的基本使用能够參见博文<Android中Handler的使用>,假设想了解Handler.Looper.Thread等的相互关系以及内部实现原理能够參见博文<深入源 ...

  9. Android View体系(八)从源代码解析View的layout和draw流程

    相关文章 Android View体系(一)视图坐标系 Android View体系(二)实现View滑动的六种方法 Android View体系(三)属性动画 Android View体系(四)从源 ...

随机推荐

  1. Codeforces Round #307 (Div. 2) E. GukiZ and GukiZiana(分块)

    E. GukiZ and GukiZiana time limit per test 10 seconds memory limit per test 256 megabytes input stan ...

  2. python itertools的使用(转)

    1. chain的使用 import itertools listone = ['a','b','c'] listtwo = ['11','22','abc'] for item in itertoo ...

  3. PIE.htc的使用

    文件下载:http://css3pie.com/download/ 使用: .pie_radius{ width:200px; height:200px; background-color:red; ...

  4. 关于lower_bound的优先级重载

    今天才知道$lower\_bound$最后有一个优先级参数…… 首先$lower\_bound$中的优先级和序列优先级必须相同才有效 $lower\_bound$中优先级默认的是小于号,也就是说仅当序 ...

  5. 【动态规划】【滚动数组】Educational Codeforces Round 26 D. Round Subset

    给你n个数,让你任选K个,使得它们乘起来以后结尾的0最多. 将每个数的因子2和因子5的数量求出来,记作a[i]和b[i]. 答案就是max{ min{Σa[i],Σb[i]} }(a[i],b[i]是 ...

  6. spring quartz 配置

    quartz简介 各种企业应用几乎都会碰到任务调度的需求,就拿论坛来说:每隔半个小时生成精华文章的RSS文件,每天凌晨统计论坛用户的积分排名,每隔30分钟执行锁定用户解锁任务.任务调度本身涉及到多线程 ...

  7. (原创)Stanford Machine Learning (by Andrew NG) --- (week 5) Neural Networks Learning

    本栏目内容来自Andrew NG老师的公开课:https://class.coursera.org/ml/class/index 一般而言, 人工神经网络与经典计算方法相比并非优越, 只有当常规方法解 ...

  8. Problem D: 指针:调用自定义排序函数sort,对输入的n个数进行从小到大输出。

    #include<stdio.h> int sort(int *p,int n) { int i,j,temp; ;i<n-;i++) for(j=i;j<n;j++) if( ...

  9. Problem F: 零起点学算法101——统计字母数字等个数

    #include<stdio.h> #include<string.h> int main(){ ]; while(gets(str)!=NULL){ ,b=,c=,d=; ; ...

  10. Codeforces Round #127 (Div. 1) D. Brand New Problem 暴力dp

    D. Brand New Problem 题目连接: http://www.codeforces.com/contest/201/problem/D Description A widely know ...