Libevent(3)之使用教程(2)创建事件
Author: Once Day Date: 2025年6月29日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦...
漫漫长路,有人对你微笑过嘛...
本文档翻译于:Fast portable non-blocking network programming with Libevent
全系列文章可参考专栏: 十年代码训练_Once-Day的博客-CSDN博客
参考文章:
文章目录
- Libevent(3)之使用教程(2)创建事件
-
-
-
- [4. 创建event_base](#4. 创建event_base)
-
- [4.1 设置默认的event_base](#4.1 设置默认的event_base)
- [4.2 构建复杂的event_base](#4.2 构建复杂的event_base)
- [4.3 查看 event_base 的后端方法](#4.3 查看 event_base 的后端方法)
- [4.4 释放 event_base](#4.4 释放 event_base)
- [4.5 在 event_base 上设置优先级](#4.5 在 event_base 上设置优先级)
- [4.6 Libevent 旧版本中的 "当前"event_base](#4.6 Libevent 旧版本中的 "当前"event_base)
- [5. 使用event loop](#5. 使用event loop)
-
- [5.1 运行 event_base 事件循环](#5.1 运行 event_base 事件循环)
- [5.2 停止循环](#5.2 停止循环)
- [5.3 重新检查事件](#5.3 重新检查事件)
- [5.4 检查内部时间缓存](#5.4 检查内部时间缓存)
- [5.5 输出 event_base 的状态](#5.5 输出 event_base 的状态)
- [5.6 遍历 event_base 中的所有事件](#5.6 遍历 event_base 中的所有事件)
-
-
4. 创建event_base
在使用任何有趣的 Libevent 函数之前,需要分配一个或多个 event_base 结构。每个 event_base 结构都包含一组事件,并且能够通过轮询来确定哪些事件处于活动状态。
如果一个 event_base 配置了锁定机制,那么在多个线程之间对其进行访问是安全的。不过,它的循环只能在单个线程中运行。如果你希望有多个线程对 IO 进行轮询,那么每个线程都需要有一个 event_base。
每个 event_base 都有一个 "方法",也就是它用于确定哪些事件就绪的后端。已识别的方法包括:
- select
- poll
- epoll
- kqueue
- devpoll
- evport
- win32
用户可以通过环境变量禁用特定的后端。如果你想关闭 kqueue 后端,设置 EVENT_NOKQUEUE 环境变量即可,其他后端的关闭方式以此类推。如果想在程序内部关闭后端,请参考下面关于 event_config_avoid_method () 的说明。
4.1 设置默认的event_base
event_base_new () 函数会分配并返回一个具有默认设置的新事件基础。它会检查环境变量,并返回一个指向新 event_base 的指针。如果出现错误,则返回 NULL。
在选择方法时,它会挑选操作系统支持的最快方法。
c
struct event_base *event_base_new(void);
对于大多数程序来说,这就是你所需要的全部。
event_base_new () 函数在 < event2/event.h>
中声明。它最早出现在 Libevent 1.4.3 版本中。
4.2 构建复杂的event_base
如果想更精确地控制所获取的 event_base 类型,就需要使用 event_config。event_config 是一种不透明的结构,用于存储对 event_base 的偏好设置。当需要一个 event_base 时,可将 event_config 传递给 event_base_new_with_config () 函数。
c
struct event_config *event_config_new(void);
struct event_base *event_base_new_with_config(const struct event_config *cfg);
void event_config_free(struct event_config *cfg);
要通过这些函数分配一个 event_base,需先调用 event_config_new () 来分配一个新的 event_config,然后调用其他相关函数向其告知你的需求,最后调用 event_base_new_with_config () 以获取新的 event_base。使用完毕后,可通过 event_config_free () 释放 event_config。
c
int event_config_avoid_method(struct event_config *cfg, const char *method);
enum event_method_feature {
EV_FEATURE_ET = 0x01,
EV_FEATURE_O1 = 0x02,
EV_FEATURE_FDS = 0x04,
};
int event_config_require_features(struct event_config *cfg,
enum event_method_feature feature);
enum event_base_config_flag {
EVENT_BASE_FLAG_NOLOCK = 0x01,
EVENT_BASE_FLAG_IGNORE_ENV = 0x02,
EVENT_BASE_FLAG_STARTUP_IOCP = 0x04,
EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08,
EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10,
EVENT_BASE_FLAG_PRECISE_TIMER = 0x20
};
int event_config_set_flag(struct event_config *cfg,
enum event_base_config_flag flag);
调用 event_config_avoid_method 函数可以告知 Libevent 避免使用特定名称的可用后端。调用 event_config_require_feature () 函数可以告知 Libevent 不要使用无法提供所有指定功能的后端。调用 event_config_set_flag () 函数可以告知 Libevent 在构建事件基础时设置以下一个或多个运行时标志。
event_config_require_features 所识别的功能值包括:
- EV_FEATURE_ET:要求后端方法支持边缘触发的 IO。
- EV_FEATURE_O1:要求后端方法支持添加、删除单个事件或单个事件变为活动状态时均为 O (1) 操作。
- EV_FEATURE_FDS:要求后端方法能够支持任意文件描述符类型,而不仅仅是套接字。
event_config_set_flag () 所识别的选项值包括:
- EVENT_BASE_FLAG_NOLOCK:不为 event_base 分配锁。设置此选项可能会节省一点锁定和释放 event_base 的时间,但会导致从多个线程访问它时既不安全也无法正常工作。
- EVENT_BASE_FLAG_IGNORE_ENV:在选择要使用的后端方法时,不检查 EVENT_* 环境变量。使用此标志前请慎重考虑:这会增加用户调试程序与 Libevent 之间交互的难度。
- EVENT_BASE_FLAG_STARTUP_IOCP:仅在 Windows 系统上,此标志会使 Libevent 在启动时启用所有必要的 IOCP 调度逻辑,而不是按需启用。
- EVENT_BASE_FLAG_NO_CACHE_TIME:不在事件循环准备运行超时回调时检查当前时间,而是在每个超时回调之后检查。这可能会消耗比预期更多的 CPU 资源,所以要格外注意!
- EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST:告知 Libevent,如果决定使用 epoll 后端,那么使用更快的基于 "变更列表(changelist)" 的后端是安全的。当同一个文件描述符在调用后端的调度函数之间多次修改状态时,epoll-changelist 后端可以避免不必要的系统调用,但如果向 Libevent 提供了通过 dup () 或其变体克隆的文件描述符,它也可能触发内核漏洞,导致错误结果。如果使用 epoll 以外的后端,此标志无效。你也可以通过设置 EVENT_EPOLL_USE_CHANGELIST 环境变量来开启 epoll-changelist 选项。
- EVENT_BASE_FLAG_PRECISE_TIMER:默认情况下,Libevent 会尝试使用操作系统提供的最快可用计时机制。如果存在一种速度较慢但时间精度更高的计时机制,此标志会告知 Libevent 改用该计时机制。如果操作系统没有提供这种速度较慢但精度更高的机制,此标志则无效。
上述用于操作 event_config 的函数在成功时均返回 0,失败时返回 - 1。
注意,很容易出现这种情况:配置的 event_config 要求操作系统不支持的后端。例如,在 Libevent 2.0.1-alpha 版本中,Windows 系统没有 O (1) 后端,Linux 系统也没有同时提供 EV_FEATURE_FDS 和 EV_FEATURE_O1 功能的后端。如果你的配置无法被 Libevent 满足,event_base_new_with_config () 将返回 NULL。
c
int event_config_set_num_cpus_hint(struct event_config *cfg, int cpus)
该函数目前仅在 Windows 系统使用 IOCP 时有用,但未来可能会在其他平台上发挥作用。调用此函数会告知 event_config,其生成的 event_base 在多线程环境下应尽量充分利用指定数量的 CPU。请注意,这只是一个提示:最终事件基础实际使用的 CPU 数量可能会多于或少于你所选择的数量。
c
int event_config_set_max_dispatch_interval(struct event_config *cfg,
const struct timeval *max_interval, int max_callbacks,
int min_priority);
此函数通过限制在检查更多高优先级事件之前可以调用的低优先级事件回调的数量,来防止优先级反转。如果 max_interval 不为空,事件循环会在每个回调之后检查时间,若已超过 max_interval,则重新扫描高优先级事件。如果 max_callbacks 为非负值,那么在调用了 max_callbacks 个回调之后,事件循环也会检查更多事件。这些规则适用于任何优先级不低于 min_priority 的事件。
c
struct event_config *cfg;
struct event_base *base;
int i;
/* My program wants to use edge-triggered events if at all possible. So
I'll try to get a base twice: Once insisting on edge-triggered IO, and
once not. */
for (i=0; i<2; ++i) {
cfg = event_config_new();
/* I don't like select. */
event_config_avoid_method(cfg, "select");
if (i == 0)
event_config_require_features(cfg, EV_FEATURE_ET);
base = event_base_new_with_config(cfg);
event_config_free(cfg);
if (base)
break;
/* If we get here, event_base_new_with_config() returned NULL. If
this is the first time around the loop, we'll try again without
setting EV_FEATURE_ET. If this is the second time around the
loop, we'll give up. */
}
示例:优先选择边缘触发的后端:
c
struct event_config *cfg;
struct event_base *base;
int i;
/* My program wants to use edge-triggered events if at all possible. So
I'll try to get a base twice: Once insisting on edge-triggered IO, and
once not. */
for (i=0; i<2; ++i) {
cfg = event_config_new();
/* I don't like select. */
event_config_avoid_method(cfg, "select");
if (i == 0)
event_config_require_features(cfg, EV_FEATURE_ET);
base = event_base_new_with_config(cfg);
event_config_free(cfg);
if (base)
break;
/* If we get here, event_base_new_with_config() returned NULL. If
this is the first time around the loop, we'll try again without
setting EV_FEATURE_ET. If this is the second time around the
loop, we'll give up. */
}
示例:避免优先级反转:
c
struct event_config *cfg;
struct event_base *base;
cfg = event_config_new();
if (!cfg)
/* Handle error */;
/* I'm going to have events running at two priorities. I expect that
some of my priority-1 events are going to have pretty slow callbacks,
so I don't want more than 100 msec to elapse (or 5 callbacks) before
checking for priority-0 events. */
struct timeval msec_100 = { 0, 100*1000 };
event_config_set_max_dispatch_interval(cfg, &msec_100, 5, 1);
base = event_base_new_with_config(cfg);
if (!base)
/* Handle error */;
event_base_priority_init(base, 2);
这些函数和类型在 <event2/event.h>
中声明。
EVENT_BASE_FLAG_IGNORE_ENV 标志最早出现在 Libevent 2.0.2-alpha 版本中。EVENT_BASE_FLAG_PRECISE_TIMER 标志最早出现在 Libevent 2.1.2-alpha 版本中。event_config_set_num_cpus_hint () 函数是在 Libevent 2.0.7-rc 版本中新增的,而 event_config_set_max_dispatch_interval () 函数则是在 2.1.1-alpha 版本中新增的。本节中的其他所有内容均最早出现在 Libevent 2.0.1-alpha 版本中。
4.3 查看 event_base 的后端方法
有时你可能想了解某个 event_base 实际支持哪些特性,或者它正在使用哪种方法。
c
const char **event_get_supported_methods(void);
event_get_supported_methods()
函数会返回一个指针,指向当前 Libevent 版本所支持的方法名称数组。该数组的最后一个元素为 NULL。
c
int i;
const char **methods = event_get_supported_methods();
printf("Starting Libevent %s. Available methods are:\n",
event_get_version());
for (i=0; methods[i] != NULL; ++i) {
printf(" %s\n", methods[i]);
}
此函数返回的是 Libevent 编译时支持的方法列表。但在 Libevent 实际运行时,操作系统可能并不支持其中的全部方法。例如,在某些版本的 OSX 系统中,kqueue 可能存在严重漏洞而无法使用。
c
const char *event_base_get_method(const struct event_base *base);
enum event_method_feature event_base_get_features(const struct event_base *base);
event_base_get_method()
调用会返回某个 event_base 实际使用的方法名称。event_base_get_features()
调用则会返回一个位掩码,代表该 event_base 支持的特性。
c
struct event_base *base;
enum event_method_feature f;
base = event_base_new();
if (!base) {
puts("Couldn't get an event_base!");
} else {
printf("Using Libevent with backend method %s.",
event_base_get_method(base));
f = event_base_get_features(base);
if ((f & EV_FEATURE_ET))
printf(" Edge-triggered events are supported.");
if ((f & EV_FEATURE_O1))
printf(" O(1) event notification is supported.");
if ((f & EV_FEATURE_FDS))
printf(" All FD types are supported.");
puts("");
}
这些函数均定义在<event2/event.h>
中。event_base_get_method()
最早在 Libevent 1.4.3 版本中可用,其他函数则最早出现在 Libevent 2.0.1-alpha 版本中。
4.4 释放 event_base
当你不再需要某个 event_base 时,可以使用event_base_free()
函数释放它。
需要注意的是,该函数不会释放任何当前与该 event_base 关联的事件,也不会关闭这些事件对应的套接字,更不会释放它们的指针。
event_base_free()
函数定义在<event2/event.h>
中,最早在 Libevent 1.2 版本中实现。
4.5 在 event_base 上设置优先级
Libevent 支持为事件设置多个优先级。不过,默认情况下,一个 event_base 仅支持单个优先级级别。你可以通过调用event_base_priority_init()
函数来设置 event_base 上的优先级数量。
c
int event_base_priority_init(struct event_base *base, int n_priorities);
该函数成功时返回 0,失败时返回 - 1。参数base
是要修改的 event_base,n_priorities
是要支持的优先级数量,其值必须至少为 1。新事件可用的优先级编号范围是从 0(最重要)到n_priorities-1
(最不重要)。
存在一个常量EVENT_MAX_PRIORITIES
,它规定了n_priorities
的上限。如果调用此函数时n_priorities
的值高于该上限,会导致错误。
你必须在任何事件进入活动状态之前调用此函数。最佳做法是在创建 event_base 之后立即调用它。
要查看某个 event_base 当前支持的优先级数量,可以调用 event_base_getnpriorities () 函数。
c
int event_base_get_npriorities(struct event_base *base);
该函数的返回值等于 event_base 中配置的优先级数量。例如,如果event_base_get_npriorities()返回 3,那么有效的优先级值为 0、1 和 2。
默认情况下,所有与该 event_base 关联的新事件都会被初始化为n_priorities / 2
的优先级。
event_base_priority_init
函数定义在<event2/event.h>
中,自 Libevent 1.0 版本起可用。event_base_get_npriorities()
函数则是在 Libevent 2.1.1-alpha 版本中新增的。
4.6 Libevent 旧版本中的 "当前"event_base
早期版本的 Libevent 严重依赖 "当前"event_base 的概念。"当前"event_base 是一个跨所有线程共享的全局设置。如果你忘记指定想要使用的 event_base,系统会默认使用当前的 event_base。由于 event_base 本身并非线程安全的,这种设计很容易导致错误。
在早期版本中,替代event_base_new()
的函数是:
c
struct event_base *event_init(void);
该函数的功能类似于event_base_new()
,但它会将新创建的 event_base 设置为当前的 event_base。而且,当时没有其他方法可以更改当前的 event_base。
本节中介绍的一些 event_base 函数在早期版本中存在操作当前 event_base 的变体。这些变体函数的行为与当前版本的对应函数相同,只是它们不需要传入 base 参数。
5. 使用event loop
5.1 运行 event_base 事件循环
一旦有了一个注册了某些事件的 event_base(关于如何创建和注册事件,请参见下一节),可能希望 Libevent 等待事件发生并通知事件。
c
#define EVLOOP_ONCE 0x01
#define EVLOOP_NONBLOCK 0x02
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04
int event_base_loop(struct event_base *base, int flags);
默认情况下,event_base_loop()
函数会运行 event_base,直到其中不再有任何已注册的事件。为了运行事件循环,它会反复检查是否有任何已注册的事件被触发(例如,读事件的文件描述符是否准备好读取,或者超时事件的超时时间是否已到期)。一旦事件被触发,它会将所有触发的事件标记为 "活动的",并开始执行这些事件的回调函数。
可以通过在flags
参数中设置一个或多个标志来改变event_base_loop()
的行为:
- 如果设置了
EVLOOP_ONCE
,事件循环会等待直到有事件变为活动状态,然后执行所有活动事件,直到没有更多可执行的事件后返回。 - 如果设置了
EVLOOP_NONBLOCK
,事件循环不会等待事件触发,只会检查是否有事件可以立即触发,如果有则执行它们的回调函数。
通常情况下,一旦没有挂起或活动的事件,事件循环就会退出。可以通过传递EVLOOP_NO_EXIT_ON_EMPTY
标志来覆盖此行为 ------ 例如,当打算从其他线程添加事件时。如果设置了EVLOOP_NO_EXIT_ON_EMPTY
,事件循环会一直运行,直到有人调用event_base_loopbreak()
、event_base_loopexit()
,或者发生错误。
event_base_loop()
完成运行时,返回值规则如下:
- 正常退出时返回 0;
- 因后端出现未处理的错误而退出时返回 - 1;
- 因不再有挂起或活动的事件而退出时返回 1。
为了便于理解,以下是event_base_loop
算法的大致总结:
c
while (any events are registered with the loop,
or EVLOOP_NO_EXIT_ON_EMPTY was set) {
if (EVLOOP_NONBLOCK was set, or any events are already active)
If any registered events have triggered, mark them active.
else
Wait until at least one event has triggered, and mark it active.
for (p = 0; p < n_priorities; ++p) {
if (any event with priority of p is active) {
Run all active events with priority of p.
break; /* Do not run any events of a less important priority */
}
}
if (EVLOOP_ONCE was set or EVLOOP_NONBLOCK was set)
break;
}
为方便使用,也可以调用:
c
int event_base_dispatch(struct event_base *base);
event_base_dispatch()
调用与event_base_loop()
功能相同,但不设置任何标志。因此,它会一直运行,直到没有更多已注册的事件,或者调用了event_base_loopbreak()
或event_base_loopexit()
。
这些函数都定义在<event2/event.h>
中,自 Libevent 1.0 版本起就已存在。
5.2 停止循环
如果希望正在运行的事件循环在所有事件被移除之前停止,可以调用两个略有差异的函数。
c
int event_base_loopexit(struct event_base *base, const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);
event_base_loopexit()
函数会告知 event_base 在经过指定时间后停止循环。如果tv
参数为 NULL,event_base 会立即停止循环(无延迟)。如果 event_base 当前正在为任何活动事件运行回调函数,它会继续执行这些回调,直到全部完成后才退出。
event_base_loopbreak()
函数则会告知 event_base 立即退出循环。它与event_base_loopexit(base, NULL)
的区别在于:如果 event_base 当前正在为活动事件运行回调函数,event_base_loopbreak()
会在完成当前正在处理的回调后立即退出。
还需注意,当事件循环未运行时,event_base_loopexit(base, NULL)
和event_base_loopbreak(base)
的行为不同:loopexit
会安排下一次事件循环在执行完下一轮回调后停止(类似以EVLOOP_ONCE
标志调用的效果);而loopbreak
仅能停止当前正在运行的循环,若事件循环未运行则无效果。
这两个函数成功时均返回 0,失败时返回 - 1。
示例:立即关闭事件循环:
c
#include <event2/event.h>
/* Here's a callback function that calls loopbreak */
void cb(int sock, short what, void *arg)
{
struct event_base *base = arg;
event_base_loopbreak(base);
}
void main_loop(struct event_base *base, evutil_socket_t watchdog_fd)
{
struct event *watchdog_event;
/* Construct a new event to trigger whenever there are any bytes to
read from a watchdog socket. When that happens, we'll call the
cb function, which will make the loop exit immediately without
running any other active events at all.
*/
watchdog_event = event_new(base, watchdog_fd, EV_READ, cb, base);
event_add(watchdog_event, NULL);
event_base_dispatch(base);
}
示例:运行事件循环 10 秒后退出:
c
#include <event2/event.h>
void run_base_with_ticks(struct event_base *base)
{
struct timeval ten_sec;
ten_sec.tv_sec = 10;
ten_sec.tv_usec = 0;
/* Now we run the event_base for a series of 10-second intervals, printing
"Tick" after each. For a much better way to implement a 10-second
timer, see the section below about persistent timer events. */
while (1) {
/* This schedules an exit ten seconds from now. */
event_base_loopexit(base, &ten_sec);
event_base_dispatch(base);
puts("Tick");
}
}
有时你可能需要判断event_base_dispatch()
或event_base_loop()
是正常退出,还是因调用event_base_loopexit()
或event_base_break()
而退出。可以使用以下函数来判断是loopexit
还是break
被调用:
c
int event_base_got_exit(struct event_base *base);
int event_base_got_break(struct event_base *base);
这两个函数分别在循环被event_base_loopexit()
或event_base_break()
停止时返回true
,否则返回false
。它们的值会在下次启动事件循环时重置。
这些函数均声明在<event2/event.h>
中。event_base_loopexit()
最早在 Libevent 1.0c 版本中实现;event_base_loopbreak()
最早在 Libevent 1.4.3 版本中实现。
5.3 重新检查事件
通常情况下,Libevent 会先检查事件,然后执行所有最高优先级的活动事件,接着再次检查事件,依此类推。但有时你可能希望在当前回调执行完毕后立即暂停 Libevent,并要求它重新扫描事件。类似于event_base_loopbreak()
,你可以使用event_base_loopcontinue()
函数来实现这一点。
c
int event_base_loopcontinue(struct event_base *);
如果当前没有在执行事件回调,调用event_base_loopcontinue()
不会产生任何效果。
该函数是在 Libevent 2.1.2-alpha 版本中引入的。
5.4 检查内部时间缓存
有时你希望在事件回调内部获取当前时间的近似值,并且不想自己调用gettimeofday()
(可能是因为你的操作系统将gettimeofday()
实现为系统调用,而你正试图避免系统调用带来的开销)。
在回调函数内部,你可以向 Libevent 询问其在开始执行本轮回调时所记录的当前时间:
c
int event_base_gettimeofday_cached(struct event_base *base, struct timeval *tv_out);
event_base_gettimeofday_cached()
函数会在 event_base 当前正在执行回调时,将其 tv_out 参数的值设置为缓存的时间。否则,它会调用evutil_gettimeofday()
来获取实际的当前时间。该函数成功时返回 0,失败时返回负值。
需要注意的是,由于 timeval 是在 Libevent 开始运行回调时缓存的,所以它至少会有一点不准确。如果你的回调函数执行时间很长,这个时间可能会非常不准确。要强制立即更新缓存,可以调用以下函数:
c
int event_base_update_cache_time(struct event_base *base);
该函数成功时返回 0,失败时返回 - 1。如果 event_base 没有运行其事件循环,调用该函数没有任何效果。
event_base_gettimeofday_cached()
函数是在 Libevent 2.0.4-alpha 版本中新增的。Libevent 2.1.1-alpha 版本添加了event_base_update_cache_time()
函数。
5.5 输出 event_base 的状态
c
void event_base_dump_events(struct event_base *base, FILE *f);
为了帮助调试程序(或调试 Libevent 本身),有时你可能需要获取 event_base 中所有已添加事件及其状态的完整列表。调用event_base_dump_events()
函数可以将该列表写入指定的标准 I/O 文件。
该列表旨在方便人类阅读,其格式在 Libevent 的未来版本中可能会发生变化。
此函数是在 Libevent 2.0.1-alpha 版本中引入的。
5.6 遍历 event_base 中的所有事件
c
typedef int (*event_base_foreach_event_cb)(const struct event_base *,
const struct event *, void *);
int event_base_foreach_event(struct event_base *base,
event_base_foreach_event_cb fn,
void *arg);
你可以使用event_base_foreach_event()
函数遍历与某个 event_base 关联的所有当前活动或挂起的事件。所提供的回调函数会对每个事件调用一次,调用顺序不确定。event_base_foreach_event()
的第三个参数会作为第三个参数传递给每次回调调用。
回调函数必须返回 0 以继续遍历,或返回其他整数以停止遍历。回调函数最终返回的值也会成为event_base_foreach_event()
的返回值。
注意:你的回调函数不得修改它所接收的任何事件,不得向该 event_base 添加或移除任何事件,也不得通过其他方式修改与该 event_base 关联的任何事件,否则可能会导致未定义行为,包括但不限于程序崩溃和堆破坏。
在event_base_foreach_event()
调用期间,event_base 的锁会被持有 ------ 这会阻止其他线程对该 event_base 执行任何有效操作,因此请确保你的回调函数不会耗时过长。

Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~