Libevent(3)之使用教程(2)創建事件
Author: Once Day Date: 2025年6月29日
一位熱衷于Linux學習和開發的菜鳥,試圖譜寫一場冒險之旅,也許終點只是一場白日夢…
漫漫長路,有人對你微笑過嘛…
本文檔翻譯于:Fast portable non-blocking network programming with Libevent
全系列文章可參考專欄: 十年代碼訓練_Once-Day的博客-CSDN博客
參考文章:
- 詳解libevent網絡庫(一)—框架的搭建_libevent詳解-CSDN博客
- 深度思考高性能網絡庫Libevent,從13個維度來解析Libevent到底是怎么回事 - 知乎
- 深入淺出理解libevent——2萬字總結_libev 堆-CSDN博客
- Fast portable non-blocking network programming with Libevent
- libevent
- C++網絡庫:Libevent網絡庫的原理及使用方法 - 知乎
- 深入理解libevent事件庫的原理與實踐技巧-騰訊云開發者社區-騰訊云
文章目錄
- Libevent(3)之使用教程(2)創建事件
- 4. 創建event_base
- 4.1 設置默認的event_base
- 4.2 構建復雜的event_base
- 4.3 查看 event_base 的后端方法
- 4.4 釋放 event_base
- 4.5 在 event_base 上設置優先級
- 4.6 Libevent 舊版本中的 "當前"event_base
- 5. 使用event loop
- 5.1 運行 event_base 事件循環
- 5.2 停止循環
- 5.3 重新檢查事件
- 5.4 檢查內部時間緩存
- 5.5 輸出 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。
在選擇方法時,它會挑選操作系統支持的最快方法。
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 () 函數。
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。
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。
int event_config_set_num_cpus_hint(struct event_config *cfg, int cpus)
該函數目前僅在 Windows 系統使用 IOCP 時有用,但未來可能會在其他平臺上發揮作用。調用此函數會告知 event_config,其生成的 event_base 在多線程環境下應盡量充分利用指定數量的 CPU。請注意,這只是一個提示:最終事件基礎實際使用的 CPU 數量可能會多于或少于你所選擇的數量。
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 的事件。
struct event_config *cfg;
struct event_base *base;
int i;/* My program wants to use edge-triggered events if at all possible. SoI'll try to get a base twice: Once insisting on edge-triggered IO, andonce 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. Ifthis is the first time around the loop, we'll try again withoutsetting EV_FEATURE_ET. If this is the second time around theloop, we'll give up. */
}
示例:優先選擇邊緣觸發的后端:
struct event_config *cfg;
struct event_base *base;
int i;/* My program wants to use edge-triggered events if at all possible. SoI'll try to get a base twice: Once insisting on edge-triggered IO, andonce 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. Ifthis is the first time around the loop, we'll try again withoutsetting EV_FEATURE_ET. If this is the second time around theloop, we'll give up. */
}
示例:避免優先級反轉:
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 thatsome 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) beforechecking 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 實際支持哪些特性,或者它正在使用哪種方法。
const char **event_get_supported_methods(void);
event_get_supported_methods()
函數會返回一個指針,指向當前 Libevent 版本所支持的方法名稱數組。該數組的最后一個元素為 NULL。
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 可能存在嚴重漏洞而無法使用。
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 支持的特性。
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 上的優先級數量。
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 () 函數。
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()
的函數是:
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 等待事件發生并通知事件。
#define EVLOOP_ONCE 0x01
#define EVLOOP_NONBLOCK 0x02
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04int 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
算法的大致總結:
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.elseWait 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;
}
為方便使用,也可以調用:
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 停止循環
如果希望正在運行的事件循環在所有事件被移除之前停止,可以調用兩個略有差異的函數。
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。
示例:立即關閉事件循環:
#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 toread from a watchdog socket. When that happens, we'll call thecb function, which will make the loop exit immediately withoutrunning 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 秒后退出:
#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-secondtimer, 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
被調用:
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()
函數來實現這一點。
int event_base_loopcontinue(struct event_base *);
如果當前沒有在執行事件回調,調用event_base_loopcontinue()
不會產生任何效果。
該函數是在 Libevent 2.1.2-alpha 版本中引入的。
5.4 檢查內部時間緩存
有時你希望在事件回調內部獲取當前時間的近似值,并且不想自己調用gettimeofday()
(可能是因為你的操作系統將gettimeofday()
實現為系統調用,而你正試圖避免系統調用帶來的開銷)。
在回調函數內部,你可以向 Libevent 詢問其在開始執行本輪回調時所記錄的當前時間:
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 開始運行回調時緩存的,所以它至少會有一點不準確。如果你的回調函數執行時間很長,這個時間可能會非常不準確。要強制立即更新緩存,可以調用以下函數:
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 的狀態
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 中的所有事件
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
也信美人終作土,不堪幽夢太匆匆......
如果這篇文章為您帶來了幫助或啟發,不妨點個贊👍和關注,再加上一個小小的收藏?!
(。???。)感謝您的閱讀與支持~~~