Libev中的信号监视器,用于监控信号的发生,因信号是异步的,所以Libev的处理方式是尽量的将异步信号同步化。异步信号的同步化方法主要有:signalfd、eventfd、pipe、sigwaitinfo等。这里Libev采用的是前三种方法,最终都是将对异步信号的处理,转化成对文件描述符的处理,也就是将ev_signal转化为处理ev_io。

一:数据结构

1:ev_signal

typedef struct ev_signal
{int active;int pending;int priority;void *data;void (*cb)(EV_P_ struct ev_signal *w, int revents);struct ev_watcher_list *next;int signum;
} ev_signal;

ev_signal的结构跟ev_io的结构十分类似,前6个成员是完全一样的,最后一个signum记录信号值。前六个成员构成了一个ev_watcher_list结构,因此信号监视器也是按照链表组织的。

2:ANSIG

typedef struct
{sig_atomic_t volatile pending;
#if EV_MULTIPLICITYstruct ev_loop *loop;
#endifev_watcher_list *head;
} ANSIG;static ANSIG signals [EV_NSIG - 1];

ANSIG就是Libev内部用来组织ev_signal的结构体,它的成员包括:pending表明该信号是否处于未决状态(触发但尚未处理),head表明该信号对应的监视器链表的头指针,另外,Libev不允许同一个信号出现在多个ev_loop结构中,因此,如果支持多个ev_loop的话,还有一个loop成员记录该信号对应的ev_loop。

signals是ANSIG类型的数组,它的下标就是相应的信号值-1,因此,每个信号都有对应的ANSIG结构。

二:初始化ev_signal

#define ev_signal_set(ev,signum_)  do {\(ev)->signum = (signum_); \
} while (0)#define ev_signal_init(ev,cb,signum)  do {\ev_init ((ev), (cb)); \ev_signal_set ((ev), (signum)); \
} while (0)

三:使用signalfd处理信号

各个系统支持的信号同步机制各有不同,针对多种信号同步机制,Libev采用下面的优先级循序:signalfd、eventfd、pipe。

signalfd是最简单方便的信号同步机制,可以很容易的将异步的信号的监听转化成对文件描述符的监听。下面首先看一下使用signalfd时的信号处理流程。

1:ev_signal_start

void ev_signal_start (struct ev_loop *loop, ev_signal *w)
{if (expect_false (ev_is_active (w)))return;assert (("libev: ev_signal_start called with illegal signal number", w->signum > 0 && w->signum < EV_NSIG));#if EV_MULTIPLICITYassert (("libev: a signal must not be attached to two different loops",!signals [w->signum - 1].loop || signals [w->signum - 1].loop == loop));signals [w->signum - 1].loop = loop;
#endif#if EV_USE_SIGNALFDif (sigfd == -2){sigfd = signalfd (-1, &sigfd_set, SFD_NONBLOCK | SFD_CLOEXEC);if (sigfd < 0 && errno == EINVAL)sigfd = signalfd (-1, &sigfd_set, 0); /* retry without flags */if (sigfd >= 0){fd_intern (sigfd); /* doing it twice will not hurt */sigemptyset (&sigfd_set);ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ);ev_set_priority (&sigfd_w, EV_MAXPRI);ev_io_start (EV_A_ &sigfd_w);ev_unref (EV_A); /* signalfd watcher should not keep loop alive */}}if (sigfd >= 0){sigaddset (&sigfd_set, w->signum);sigprocmask (SIG_BLOCK, &sigfd_set, 0);signalfd (sigfd, &sigfd_set, 0);}
#endifev_start (EV_A_ (W)w, 1);wlist_add (&signals [w->signum - 1].head, (WL)w);...}

在ev_signal_start中,首先对信号监视器w进行验证:

如果它已经被激活,也就是w->active不为0,直接返回;

信号监视器中的信号值应该处于合法范围(0, EV_NSIG)内,否则进程退出;

该信号对应的ANSIG结构中记录的ev_loop,应该就是参数loop,否则进程退出;

验证完成后,首先记录一下该信号所在的ev_loop:

signals [w->signum - 1].loop = loop;

然后,根据宏EV_USE_SIGNALFD判断系统是否支持signalfd函数,根据loop->sigfd的值判断用户是否使用signalfd函数,在初始化ev_loop的函数loop_init (struct ev_loop *loop, unsigned intflags)中有:

sigfd = flags & EVFLAG_SIGNALFD ? -2 : -1;

因此,用户如果想使用signalfd函数,flags参数中必须有EVFLAG_SIGNALFD,也就是在使用ev_default_loop或者ev_loop_new初始化ev_loop时,必须指明EVFLAG_SIGNALFD标志。

如果系统支持signalfd,并且loop->sigfd为-2的话,则开始调用signalfd函数(这是第一次调用signalfd),创建signalfd文件描述符。如果signalfd支持SFD_NONBLOCK和SFD_CLOEXEC标志的话,则直接在signalfd中设置,否则调用fd_intern,使用fcntl设置(即使signalfd支持这俩标志,也会调用该函数重新设置一遍,无伤大雅)。注意,第一次调用signalfd时,信号集(sigset_t)sigfd_set尚未初始化,在下面初始化。

第一次调用signalfd成功之后,首先清空信号集sigfd_set,然后将signalfd文件描述符加入到Libev内部的IO监视器(ev_io)sigfd_w中,并且启动sigfd_w,注意这里设置sigfd_w的优先级为最高优先级,回调函数为sigfdcb:

sigemptyset (&sigfd_set);ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ);
ev_set_priority (&sigfd_w, EV_MAXPRI);
ev_io_start (EV_A_ &sigfd_w);
ev_unref (EV_A); /* signalfd watcher should not keep loop alive */

接下来,将信号w->signum加入到信号集sigfd_set中,阻塞该信号,重新关联signalfd和sigfd_set:

sigaddset (&sigfd_set, w->signum);
sigprocmask (SIG_BLOCK, &sigfd_set, 0);
signalfd (sigfd, &sigfd_set, 0);

之所以不在调用signalfd的时候阻塞该信号并关联signalfd描述符,是因为所有信号仅使用一个signalfd描述符sigfd,一个IO监视器sigfd_w。当有多个信号监视器时,需要多次调用ev_signal_start,在第一次调用ev_signal_start成功之后,sigfd的值便已是大于等于0的整数了,这样只需要将信号阻塞,然后重新关联sigfd即可,而无需重新创建一个signalfd描述符并加入到IO监视器sigfd_w中。

最后,激活该信号监视器w,并且将其加入到相应的ANSIG结构中:

ev_start (EV_A_ (W)w, 1);
wlist_add (&signals [w->signum - 1].head, (WL)w);

这样,如果使用signalfd监控信号,ev_signal_start函数的流程就结束了。接下来,就是监控IO监视器sigfd_w了。当sigfd_set中的一个或多个信号发生时,sigfd变成可读状态,IO监视器sigfd_w触发,在ev_run中,调用ev_invoke_pending时,就会调用它的回调函数sigfdcb。ev_invoke_pending的代码如下:

void ev_invoke_pending (struct ev_loop *loop)
{pendingpri = NUMPRI;while (pendingpri) /* pendingpri possibly gets modified in the inner loop */{--pendingpri;while (pendingcnt [pendingpri]){ANPENDING *p = pendings [pendingpri] + --pendingcnt [pendingpri];p->w->pending = 0;EV_CB_INVOKE (p->w, p->events);EV_FREQUENT_CHECK;}}
}

该函数中,首先从最高优先级的pendings开始轮训,依次调用其中监视器的回调函数。因IO监视器sigfd_w具有最高优先级,因此如果信号触发了,则sigfd_w的回调函数sigfdcb会首先被调用到。

2:sigfdcb

static void sigfdcb (struct ev_loop *loop, ev_io *iow, int revents)
{struct signalfd_siginfo si[2], *sip; /* these structs are big */for (;;){ssize_t res = read (sigfd, si, sizeof (si));/* not ISO-C, as res might be -1, but works with SuS */for (sip = si; (char *)sip < (char *)si + res; ++sip)ev_feed_signal_event (EV_A_ sip->ssi_signo);if (res < (ssize_t)sizeof (si))break;}
}

该回调函数中,主要是读取sigfd中的信号信息。因struct  signalfd_siginfo结构比较大(128字节),这里采用的技巧是每次read时最多只读取2个。

针对读取到的信号值,调用ev_feed_signal_event函数。

3:ev_feed_signal_event

void ev_feed_signal_event (struct ev_loop *loop, int signum)
{WL w;if (expect_false (signum <= 0 || signum >= EV_NSIG))return;--signum;#if EV_MULTIPLICITY/* it is permissible to try to feed a signal to the wrong loop *//* or, likely more useful, feeding a signal nobody is waiting for*/if (expect_false (signals [signum].loop != EV_A))return;
#endifsignals [signum].pending = 0;for (w = signals [signum].head; w; w = w->next)ev_feed_event (EV_A_ (W)w, EV_SIGNAL);
}

在ev_feed_signal_event中,首先检查信号值是否处于合法范围(0, EV_NSIG)内,然后检查该信号对应的ev_loop是否就是当前的loop,如果不是则直接返回。

然后置signals [signum].pending为0,在signals中找到该信号的监视器列表,针对该列表中的所有监视器,调用ev_feed_event,将监视器加入到loop->pendings中。

注意,此时添加信号监视器到loop->pendings的流程,还是处于ev_invoke_pending函数的流程中的,因此,在ev_invoke_pending中,处理完sigfd_w监视器后,接着就会处理到刚刚加到loop->pendings的信号监视器。从而信号自己的回调函数就会被调用到。

这样使用signalfd监控信号的完整流程就结束了。

四:使用eventfd、pipe处理信号

使用eventfd和pipe处理信号的基本思路是一样的,首先创建eventfd描述符或者管道pipe,使用IO监视器监听eventfd描述符或者pipe[0],当信号发生时时,在信号处理程序中,写入eventfd描述符或者pipe[1],从而触发IO监视器,调用回调函数pipecb处理信号。

1:ev_signal_start

首先看下,当不使用signalfd,或者调用signalfd失败时,ev_signal_start的流程:

void ev_signal_start (struct ev_loop *loop, ev_signal *w) EV_THROW
{if (expect_false (ev_is_active (w)))return;assert (("libev: ev_signal_start called with illegal signal number", w->signum > 0 && w->signum < EV_NSIG));#if EV_MULTIPLICITYassert (("libev: a signal must not be attached to two different loops",!signals [w->signum - 1].loop || signals [w->signum - 1].loop == loop));signals [w->signum - 1].loop = EV_A;
#endif...ev_start (EV_A_ (W)w, 1);wlist_add (&signals [w->signum - 1].head, (WL)w);if (!((WL)w)->next)
# if EV_USE_SIGNALFDif (sigfd < 0) /*TODO*/
# endif{struct sigaction sa;evpipe_init (EV_A);sa.sa_handler = ev_sighandler;sigfillset (&sa.sa_mask);sa.sa_flags = SA_RESTART; /* if restarting works we save one iteration*/sigaction (w->signum, &sa, 0);if (origflags & EVFLAG_NOSIGMASK){sigemptyset (&sa.sa_mask);sigaddset (&sa.sa_mask, w->signum);sigprocmask (SIG_UNBLOCK, &sa.sa_mask, 0);}}
}

首先对信号监视器w进行验证,然后激活该信号监视器w,并且将其加入到相应的ANSIG结构中,该过程与使用signalfd的流程一样,不再赘述。

接下来,如果该监视器是当前信号的第一个监视器(((WL)w)->next == NULL),说明这是第一次监听该信号,需要创建该信号的信号处理函数,并且创建eventfd或pipe结构。

首先调用evpipe_init初始化eventfd或pipe结构,暂且不表,下面详述。

然后调用sigaction建立该信号的处理函数为ev_sighandler,并且在调用信号处理函数时,阻塞所有信号,且被信号中断的低速系统调用会被重启

sa.sa_handler = ev_sighandler;
sigfillset (&sa.sa_mask);
sa.sa_flags = SA_RESTART; /* if restarting works we save one iteration */
sigaction (w->signum, &sa, 0);

如果在初始化ev_loop时指定了EVFLAG_NOSIGMASK标志的话,还需要明确将监听的信号解除阻塞。

if (origflags & EVFLAG_NOSIGMASK)
{
sigemptyset (&sa.sa_mask);
sigaddset (&sa.sa_mask, w->signum);
sigprocmask (SIG_UNBLOCK, &sa.sa_mask, 0);
}

2:evpipe_init

该函数用来创建eventfd描述符或者pipe,并将信号监视器转换为IO监视器pipe_w。代码如下:

static void evpipe_init (struct ev_loop *loop)
{if (!ev_is_active (&pipe_w)){int fds [2];# if EV_USE_EVENTFDfds [0] = -1;fds [1] = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC);if (fds [1] < 0 && errno == EINVAL)fds [1] = eventfd (0, 0);if (fds [1] < 0)
# endif{while (pipe (fds))ev_syserr ("(libev) error creating signal/async pipe");fd_intern (fds [0]);}evpipe [0] = fds [0];if (evpipe [1] < 0)evpipe [1] = fds [1]; /* first call, set write fd */else{/* on subsequent calls, do not change evpipe [1] *//* so that evpipe_write can always rely on its value. *//* this branch does not do anything sensible on windows, *//* so must not be executed on windows */dup2 (fds [1], evpipe [1]);close (fds [1]);}fd_intern (evpipe [1]);ev_io_set (&pipe_w, evpipe [0] < 0 ? evpipe [1] : evpipe [0], EV_READ);ev_io_start (EV_A_ &pipe_w);ev_unref (EV_A); /* watcher should not keep loop alive */}
}

所有信号使用一个IO监视器pipe_w,如果pipe_w已经处于激活状态,则说明相应的结构已经创建好了,直接返回即可。

然后根据宏EV_USE_EVENTFD判断系统是否支持eventfd,如果支持,则调用eventfd创建eventfd描述符,如果不支持,或者调用eventfd失败,则调用pipe创建管道,并设置管道读端描述符的FD_CLOEXEC和O_NONBLOCK标志。

使用evpipe[0]记录读描述符,evpipe[1]记录写描述符,如果使用eventfd,则evpipe[0]为-1,evpipe[1]为eventfd描述符,读写描述符都是eventfd。

调用fd_intern,使用fcntl设置写描述符evpipe[1]的FD_CLOEXEC和O_NONBLOCK标志。

最后启动内部IO监视器pipe_w,监听读描述符:

ev_io_set (&pipe_w, evpipe [0] < 0 ? evpipe [1] : evpipe [0], EV_READ);
ev_io_start (EV_A_ &pipe_w);
ev_unref (EV_A); /* watcher should not keep loop alive */

注意,pipe_w监视器的初始化,在loop_init中就已经做了:

#if EV_SIGNAL_ENABLE || EV_ASYNC_ENABLEev_init (&pipe_w, pipecb);ev_set_priority (&pipe_w, EV_MAXPRI);
#endif

这里设置pipe_w的回调函数为pipecb,优先级为最高优先级EV_MAXPRI。

当信号产生时,就会调用到信号处理函数ev_sighandler,改处理函数仅仅就是调用函数ev_feed_signal而已。

3:ev_feed_signal

void ev_feed_signal (int signum)
{
#if EV_MULTIPLICITYstruct ev_loop *loop;loop = signals [signum - 1].loop;if (!loop)return;
#endifsignals [signum - 1].pending = 1;evpipe_write (loop, &sig_pending);
}

该函数首先根据信号值得到该信号所在的ev_loop,然后置该信号对应的ANSIG的pending为1,最后调用evpipe_write函数。

4:evpipe_write

void evpipe_write (struct ev_loop *loop, sig_atomic_t volatile *flag)
{if (expect_true (*flag))return;*flag = 1;pipe_write_skipped = 1;if (pipe_write_wanted){int old_errno;pipe_write_skipped = 0;old_errno = errno; /* save errno because write will clobber it*/#if EV_USE_EVENTFDif (evpipe [0] < 0){uint64_t counter = 1;write (evpipe [1], &counter, sizeof (uint64_t));}else
#endif{write (evpipe [1], &(evpipe [1]), 1);}errno = old_errno;}
}

注意,当在一个loop中有多个信号发生时,也仅需要产生一个事件而已。本函数主要作用就是当信号发生时向写描述符写入一个事件,即可触发pipe_w中的读事件,表明有一个或者多个信号触发了。但是因为信号处理函数的调用时机是完全随机的,因此,需要有一定的手段保证代码的安全性。

a:sig_pending

该值表示是否有信号处于未决状态(触发但尚未处理)。该值在初始化ev_loop时置为0,调用evpipe_write时,会首先判断该值是否为1。如果该值已经为1,表示已经有监听的信号处于未决状态了,无需再向写描述符写入事件了。该值直到调用pipe_w的回调函数pipecb时,消费掉该事件之后才重置为0,表明从此刻起,若有监听信号触发,才能继续向写描述符写入事件。

b:pipe_write_wanted和pipe_write_skipped

pipe_write_wanted表明是否允许向写描述符写入事件,pipe_write_skipped表明是否信号发生了,却因pipe_write_wanted的关系被暂时忽略了。这两个值在初始化ev_loop时置为0。

在evpipe_write中,首先置pipe_write_skipped为1,如果pipe_write_wanted此时为0,evpipe_write直接返回,这就表明触发信号暂时被忽略掉了(仅仅是暂时的)。否则,重置pipe_write_skipped为0,向写描述符写入事件。

在ev_run中,调用backend_poll之前,会将pipe_write_wanted置为1,表明此刻起写描述符才能接受写入事件,如果此刻之前有监听信号发生的话,则会在信号处理函数调用的evpipe_write中,置pipe_write_skipped为1表示信号暂时忽略掉。

在ev_run中调用backend_poll后立即置pipe_write_wanted为0。如果pipe_write_skipped为1,表明有信号被忽略了,调用ev_feed_event,直接将pipe_w标记为pending状态,将pipe_w加入到loop->pendings中:

do{ev_tstamp waittime  = 0.;pipe_write_wanted = 1;if (expect_true (!(flags & EVRUN_NOWAIT || idleall || !activecnt || pipe_write_skipped))){waittime = MAX_BLOCKTIME;...      }backend_poll (EV_A_ waittime);pipe_write_wanted = 0; /* just an optimisation, no fence needed */if (pipe_write_skipped){assert (("libev: pipe_w not active, but pipe not written", ev_is_active (&pipe_w)));ev_feed_event (EV_A_ &pipe_w, EV_CUSTOM);}EV_INVOKE_PENDING;
}while(expect_true (activecnt&& !loop_done&& !(flags & (EVRUN_ONCE | EVRUN_NOWAIT))
))

上面就是ev_run中的相关逻辑,注意:调用backend_poll之后,如果在检测pipe_write_skipped之后才有信号发生的话,此时在evpipe_write中仅设置pipe_write_skipped为1后就返回。然后进入下次循环,因pipe_write_skipped为1,所以waittime为0,backend_poll会立即返回,处理pipe_w的激活事件。

PS:现在还没有想明白,为什么需要pipe_write_wanted和pipe_write_skipped这两个标志,感觉sig_pending已经足够了。

在向写描述符写入事件之后,pipe_w监视器触发,调用回调函数pipecb。

5:pipecb

static void pipecb (struct ev_loop *loop, ev_io *iow, int revents)
{int i;if (revents & EV_READ){
#if EV_USE_EVENTFDif (evpipe [0] < 0){uint64_t counter;read (evpipe [1], &counter, sizeof (uint64_t));}else
#endif{char dummy[4];read (evpipe [0], &dummy, sizeof (dummy));}}pipe_write_skipped = 0;#if EV_SIGNAL_ENABLEif (sig_pending){sig_pending = 0;for (i = EV_NSIG - 1; i--; )if (expect_false (signals [i].pending))ev_feed_signal_event (EV_A_ i + 1);}
#endif
...
}

在pipecb中,首先从eventfd描述符或者pipe[0]中消费掉事件。然后轮训signals数组中每个ANSIG结构的pending字段,只要是信号触发了,则该字段一定为1,从而可以调用ev_feed_signal_event处理该信号。剩下的流程就与使用signalfd时一样了,不再赘述。

四:ev_signal_stop

void ev_signal_stop (struct ev_loop *loop, ev_signal *w)
{clear_pending (EV_A_ (W)w);if (expect_false (!ev_is_active (w)))return;wlist_del (&signals [w->signum - 1].head, (WL)w);ev_stop (EV_A_ (W)w);if (!signals [w->signum - 1].head){
#if EV_MULTIPLICITYsignals [w->signum - 1].loop = 0; /* unattach from signal */
#endif
#if EV_USE_SIGNALFDif (sigfd >= 0){sigset_t ss;sigemptyset (&ss);sigaddset (&ss, w->signum);sigdelset (&sigfd_set, w->signum);signalfd (sigfd, &sigfd_set, 0);sigprocmask (SIG_UNBLOCK, &ss, 0);}else
#endifsignal (w->signum, SIG_DFL);}
}

         在ev_signal_stop中,首先调用clear_pending清除监视器w在loop->pendings中的状态,置w->pending = 0。这里有个技巧是将loop->pendings中,原w所在位置直接赋值为内部伪监视器pending_w,pending_w的回调函数为空函数,会直接返回。

调用wlist_del,将w从该信号的监视器列表中删除,调用ev_stop注销改监视器;

如果相应信号的监视器列表空了,则首先signals [w->signum - 1].loop =0,然后恢复该信号的处理方式:若使用signalfd,则取消阻塞该信号,将该信号从sigfd描述符关联的信号集中删除;若不使用signalfd,则直接恢复该信号的处理方式为默认方式。

五:总结


六:例子

ev_signal signal_w;void signal_action(struct ev_loop *main_loop,ev_signal *signal_w,int e)
{puts("\nin signal cb \n");
}int main()
{struct ev_loop *main_loop = ev_default_loop(EVFLAG_SIGNALFD);ev_init(&signal_w,signal_action);ev_signal_set(&signal_w,SIGINT); ev_signal_start(main_loop,&signal_w);ev_run(main_loop,0);return 0;
}

结果:

#./a.out
^C
in signal cb ^C
in signal cb ^\Quit (core dumped)

Libev源码分析08:Libev中的信号监视器相关推荐

  1. libev源码分析(一)libev数据结构整理

    这里选取的版本为最新版:libev-4.04.libev的代码很简练,除了对高效I/O模型等的封装文件,核心文件就两个:ev.h和ev.c,其中ev.c大概4000行左右.代码大量用到了宏,并且宏还嵌 ...

  2. libev源码分析---整体设计

    libev是Marc Lehmann用C写的高性能事件循环库.通过libev,可以灵活地把各种事件组织管理起来,如:时钟.io.信号等.libev在业界内也是广受好评,不少项目都采用它来做底层的事件循 ...

  3. Libev源码分析09:select突破处理描述符个数的限制

    众所周知,Linux下的多路复用函数select采用描述符集表示处理的描述符.描述符集的大小就是它所能处理的最大描述符限制.通常情况下该值为1024,等同于每个进程所能打开的描述符个数. 增大描述符集 ...

  4. lodash源码分析之compact中的遍历

    小时候, 乡愁是一枚小小的邮票, 我在这头, 母亲在那头. 长大后,乡愁是一张窄窄的船票, 我在这头, 新娘在那头. 后来啊, 乡愁是一方矮矮的坟墓, 我在外头, 母亲在里头. 而现在, 乡愁是一湾浅 ...

  5. apollo源码分析 感知_Kitty中的动态线程池支持Nacos,Apollo多配置中心了

    目录 回顾昨日 nacos 集成 Spring Cloud Alibaba 方式 Nacos Spring Boot 方式 Apollo 集成 自研配置中心对接 无配置中心对接 实现源码分析 兼容 A ...

  6. 「源码分析」CopyOnWriteArrayList 中的隐藏知识,你Get了吗?

    前言 本觉 CopyOnWriteArrayList 过于简单,寻思看名字就能知道内部的实现逻辑,所以没有写这篇文章的想法,最近又仔细看了下 CopyOnWriteArrayList 的源码实现,大体 ...

  7. lodash源码分析之baseFindIndex中的运算符优先级

    我悟出权力本来就是不讲理的--蟑螂就是海米:也悟出要造反,内心必须强大到足以承受任何后果才行. --北岛<城门开> 本文为读 lodash 源码的第十篇,后续文章会更新到这个仓库中,欢迎 ...

  8. [pig4cloud框架源码分析] 03 - MyBatis中的sql语句日志打印

    文章目录 导读 pig4cloud框架配置 Mybatis Log Plugin 插件开启方式 插件说明 [TODO]源码分析 拦截器方案实现sql日志查看 参考资料 导读 使用MyBatis开发过程 ...

  9. UGUI源码分析:LayoutGroup中的纵横布局组件(HorizontalOrVerticalLayoutGroup)

    系列 UGUI源码分析系列总览 相关前置: UGUI CanvasUpdateSystem源码分析 UGUI源码分析:LayoutSystem布局系统 文章目录 系列 UML图一览 LayoutGro ...

最新文章

  1. ubuntu下部署eclipse集成hadoop\android\web\GCC开发环境小记
  2. lucene源码分析(7)Analyzer分析
  3. pymol怎么做底物口袋表面_怎么从文献中发掘一篇新文章?
  4. windows server 2012服务器IIS基本配置
  5. 开发必备知识点--django项目启动时,url加载之前,执行某个.py文件
  6. MySQL数据库规范及解读
  7. 亲密关系-【有效表达】-如何完善自己的表达思路?
  8. 【bzoj4530】[Bjoi2014]大融合 LCT维护子树信息
  9. Apache环境利用.htaccess文件设置域名301跳转(不带www跳转到带www)
  10. 时序数据库 VS 工业实时数据库
  11. postman安装和安装后双击没反应
  12. lucene-使用htmlparser解析未设定编码页面
  13. 如何在word中的框中打钩、打叉
  14. u8系统更改了服务器,用友u8服务器地址修改
  15. windows 8 音乐(Xbox Music)详解
  16. BAT面试经验分享(机器学习算法岗)
  17. java图片处理以及pdf转图片
  18. 原生js小游戏——俄罗斯方块
  19. IS-IS协议所使用的NET地址由哪几部分构成?
  20. 安卓apk版本检测下载升级全过程

热门文章

  1. 整理常用的中英文预训练词向量(Pretrained Word Vectors)
  2. 基于javaweb+jsp的生病慢病报销管理信息系统(java+MySQL+Jdbc+Servlet+Jsp)
  3. 搭建直播平台的基础,实现直播平台源码的架构
  4. 用 Django 开发微信小程序后端实现用户登录
  5. 用 Python 全自动下载抖音小姐姐视频
  6. 细粒度情感三元组抽取任务及其最新进展
  7. 油猴脚本*********js简单替换页面文字
  8. 富士施乐3065扫描教程_精简高效灵活 富士施乐3065使用测试
  9. 施乐252服务器修复,富士施乐uCentre-IVC2263故障错误代码.pdf
  10. POI读写excel简单教程