原文地址:http://blog.csdn.net/jinzhuojun/article/details/40264449
Wayland协议主要提供了Client端应用与Server端Compositor的通信机制,Weston是Server端Compositor的一个参考实现。Wayland协议中最基础的是提供了一种面向对象的跨进程过程调用的功能,在作用上类似于Android中的Binder。与Binder不同的是,在Wayland中Client和Server底层通过domain socket进行连接。和Binder一样,domain socket支持在进程间传递fd,这为传递graphic buffer和shared memory等提供了基础。Client和Server端一方面各自在消息循环中等待socket上的数据,拿到数据后经过反序列化处理生成本地的函数调用,然后进行调用;另一方面将本地的远端调用请求封装序列化后通过socket发出。另外,由于是基于消息循环的处理模型,意味着这种调用不是同步,但通过等待Server端处理完成后的事件再返回可以达到同步调用的效果。
下面从几个基本方面介绍Wayland协议的跨进程过程调用是如何实现的。
一、基本工作流程
以Weston自带的例程simple-shm为例,先感受一下Client如何通过Wayland协议和Compositor通信。
- 1.连接Server,绑定服务
1 2 3 4 |
display->display = wl_display_connect()// 通过socket建立与Server端的连接,得到wl_display。它即代表了Server端的display资源,同时也是代理对象wl_proxy。Client可以通过它来向Server端提交调用请求和接收事件。 display->registry = wl_display_get_registry(display->display) // 申请创建registry,得到代理对象wl_registry。这个对象相当于Client在Server端放的一个用于嗅探资源的Observer。Client通过它得到Server端有哪些Global对象的信息。Server端有一系列的Global对象,如wl_compositor, wl_shm等,串在display->global_list链表里。Global对象在概念上类似于Service服务,因此Server端相当于充当了ServiceManager的角色。 wl_registry_add_listener(display->registry, ®istry_listener,...) // 让Client监听刚才创建的wl_registry代理对象。这样,当Client调用wl_display_get_registry()函数或者有新的Global对象加入到Server端时,Client就会收到event通知。 wl_display_roundtrip() // 等待前面的请求全被Server端处理完,它同步了Client和Server端。这意味着到这个函数返回时,Server端有几个Global对象,回调处理函数registry_handle_global()应该就已经被调用过几次了。registry_handle_global()中会判断是当前这次event代表何种Global对象,然后调用wl_registry_bind()进行绑定,得到远程服务对象的本地代理对象。这些代理对象类型可以是wl_shm, wl_compositor等,但本质上都是wl_proxy类型。这步的作用类似于Android中的bindService(),它会得到一个远端Service的本地代理。 |
- 2.创建窗口
1 2 |
window->surface = wl_compositor_create_surface() // 通过刚才绑定的wl_compositor服务创建Server端的weston_surface,返回代理对象wl_surface。 xdg_shell_get_xdg_surface(..., window->surface, ...) // 通过刚才绑定的xdg_shell服务创建Server端的shell_surface,返回代理对象xdg_surface。有些例子中用的是wl_shell_surface,它和xdg_surface的作用是一样的。xdg_surface是作为wl_shell_surface将来的替代品,但还没进Wayland核心协议。 |
为什么一个窗口要创建两个surface呢?因为Wayland协议假设Server端对Surface的管理分两个层次。以Weston为例,Compositor只负责合成(代码主要在compositor.c),它相当于Android中的SurfaceFligner,它所看到的主要是weston_surface。而Weston在启动时会加载shell模块(如desktop-shell.so,代码主要在desktop-shell/shell.c),它相当于Android中的WindowManagerService,它所看到的主要是shell_surface。shell_surface在结构上是weston_surface的进一步封装,为了做窗口管理。这样,合成渲染和窗口管理的模块既可以方便地相互访问又保证了较低的耦合度。
- 3.分配buffer与绘制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
wl_surface_damage() // 告诉Compositor该surface哪块区域是脏的,需要重绘。一开始是整个窗口区域。 redraw() // 接下来调用redraw()开始绘制的循环,这里是双buffer的软件渲染。 window_next_buffer() // 取一个buffer,用作绘制。 create_shm_buffer() // 如果该buffer尚未分配则用之前绑定的wl_shm服务分配一块共享内存。 fd = os_create_anonymous_file() // 为了创建共享内存,先创建一个临时文件作为内存映射的backing file。 mmap(..., fd,...) // 将该文件先映射到Client端的内存空间。 pool = wl_shm_create_pool(..., fd,...) // 通过wl_shm服务创建共享内存池。将刚才的fd作为参数传过去,这样Server端就可以和Client通过这个fd映射到同一块物理内存。 buffer->buffer = wl_shm_pool_create_buffer(pool, ...) // 通过这个共享内存池在Server端分配buffer,返回wl_buffer为其本地代理对象。 wl_buffer_add_listener(buffer->buffer, &buffer_listener,...) // 监听这个buffer代理对象,当Server端不再用这个buffer时,会发送release事件。这样,Client就可以重用这个buffer作下一帧的绘制。 paint_pixels() // Client在buffer上绘制自己的内容。 wl_surface_attach()// 将绘制好的buffer attach到surface上。作用上类似于Android中的updateTexImage(),即把某一个buffer与surface绑定。 wl_surface_damage()// 告诉Server端的Compositor这个surface哪块区域是脏区域,需要重新绘制。 window->callback = wl_surface_frame() // 在Server端创建Frame callback,它会被放在该surface下的frame_callback_list列表中。返回它的代理对象wl_callback。 wl_callback_add_listener(window->callback, &frame_listener, ...) // 监听前面得到的callback代理对象。在Server端Compositor在完成一帧的合成渲染后,会往这些callback对象发done的事件(参考weston_output_repaint())。Client收到后会调用参数中wl_callback_listener中的done事件对应的方法,这里是redraw()函数。这样,就形成了一个循环。 wl_surface_commit() // 在0.99版本后,为了保证原子性及使surface属性的改动顺序无关,Server端对于surface的属性(damage region, input region, opaque region, etc.)都是双buffer的(weston_surface_state)。所以commit前的改动都存在backing buffer中。只有当Client调用wl_surface_commit()时,这些改动才生效。 |
与Android作个类比,这里的wl_surface对应SF中的Layer,wl_buffer对应GraphicBuffer。Weston对应SF+WMS。一个surface对应用户视角看到的一个窗口。为了使Client和Server并行作业,一般会用多个buffer。和Android比较不同的是,Android是Server端来维护buffer的生命周期,而Wayland中是Client端来做的。
二、连接的建立
首先Server端的Compositor在启动时会在$XDG_RUNTIME_DIR目录下创建用于监听的socket,默认为wayland-0。然后把该socket fd加入event loop等待的fd列表。参考实现位于weston_create_listening_socket() -> wl_display_add_socket_auto() -> _wl_display_add_socket()。当有Client连接时,回调处理函数为socket_data(),其中会调用wl_os_accept_cloexec()得到与该Client相连的socket fd。然后调用wl_client_create(),创建wl_client。Server端会为每一个连接的Client创建wl_client,这些对象被串在display->client_list这个列表里。wl_client中的wl_connection代表这个与Client的连接,其中包含了in buffer和out buffer,分别作为输入和输出的缓冲区。注意这个in buffer和out buffer都是双份的,一份for普通数据,一份for fd,因为fd需要以out-of-band形式传输,要特殊处理。wl_event_loop_add_fd()会把这个与Client连接的socket fd加入到event loop等待的列表中,回调处理函数为wl_client_connection_data()。
1 2 3 4 5 6 7 8 9 |
weston_create_listening_socket() wl_display_add_socket_auto() wl_socket_init_for_display_name() // $XDG_RUNTIME_DIR/wayland-0, _wl_display_add_socket() wl_os_socket_cloexec() // create socket bind() listen() wl_event_loop_add_fd(.., socket_data,...) // 创建wl_event_source_fd,它代表一个基于socket fd的事件源。处理函数是wl_event_source_fd_dispatch(),其中会调用这里参数里的回调函数socket_data()。 add_source() // 把刚创建的监听socket fd,通过epoll_ctl()附加到loop->epoll_fd上。这样消息循环就可以在上面等待Client的连接了。 |
当有Client连接到Server的监听socket上,会调用刚才注册的回调函数socket_data(),然后会调用wl_os_accept_cloexec()->accept()创建与Client连接的fd。接着调用wl_client_create()创建Client对象。
1 2 3 4 5 6 7 8 |
socket_data() wl_os_accept_cloexec() wl_client_create() client->connection = wl_connection_create() wl_map_init(&client->objects) // 初始化wl_map对象,用于映射Server和Client端的对象。 bind_display() // 绑定Client端的wl_display代理对象与Server端的display资源对象。 client->display_resource = wl_resource_create(.., &wl_display_interface,...) // display资源对象的接口就是wl_display_interface,request的实现在display_interface中。这里创建wl_resource结构,其中resource->object是一个可供Client调用的远端对象的抽象,其中的成员interface和implementation分别代表其接口和实现。然后用wl_map_insert_at()插入到client->objects的wl_map结构中供以后查询。 wl_resource_set_implementation(..., &display_interface, ...) // 对Client而言,Server端提供的接口实现是request部分。 |
Client端要连接Server端,是通过调用wl_display_connect()。其中会创建socket并且调用connect()连接Server端创建的监听端口。得到与Server端连接的socket fd后调用wl_display_connect_to_fd()创建
wl_display。wl_display是Server端的display资源在Client端的代理对象,它的第一个元素wl_proxy,因此它可以与wl_proxy互转。和Server端一样,也会创建一个wl_connection包含读写缓冲区。
1 2 3 4 5 6 7 8 |
wl_display_connect() fd =connect_to_socket() // 尝试连接$XDG_RUNTIME_DIR/wayland-0,返回与Server端相连的socket fd。 wl_os_socket_cloexec() connect() wl_display_connect_to_fd()// 创建和返回wl_display。 display->proxy.object_interface = &wl_display_interface; // 设置wl_display的接口。 display->proxy.object.implementation = (void(**)(void)) &display_listener // 对Server而言,Client端提供的接口实现是event部分。 display->connection = wl_connection_create() |
可以看到display在Client端的wl_proxy和Server端wl_resource都包含了它完整的接口描述wl_display_interface。但wl_proxy只包含了event的实现display_listener,wl_resource只包含了request的实现display_interface。
三、消息处理模型
在Server端,Compositor初始化完后会调用wl_display_run()进入大循环。这个函数主体是:
1 2 3 4 |
while (...) { wl_display_flush_clients() // 将对应的out buffer通过socket发出去。 wl_event_loop_dispatch() // 处理消息循环。 } |
wl_event_loop代表主消息循环,wl_event_loop_dispatch()的大多数时间会通过epoll_wait()等待在wl_event_loop的epoll_fd上。epoll是类似于select, poll的机制,可以让调用者等待在一坨fd上,直到其中有fd可读、可写或错误。这个event loop和其中的epoll_fd是Compositor在wl_display_create() -> wl_event_loop_create()时创建的。
wl_event_source代表wl_event_loop中等待的事件源。它有多种类型,比如wl_event_source_fd, wl_event_source_timer和wl_event_source_signal。它们分别代表由socket fd, timerfd和signalfd触发的事件源。wl_event_source_idle比较特殊,当消息循环处理完那些epoll_fd上等到的事件后,在下一次阻塞在epoll_wait()上前,会先处理这个idle list里的事件。比如有Client更新graphic buffer后会调用weston_output_schedule_repaint() -> wl_event_loop_add_idle(),就是往这个idle list里加一个消息。wl_event_source_fd的创建为例,在Client连接到Server时,Server会调用wl_client_create() -> wl_event_loop_add_fd()->add_source()将之加入到display的loop上,其处理回调函数为wl_client_connection_data(),意味着当主消息循环在这个Client对应的socket上读到数据时,就会调用wl_client_connection_data()进行处理。
在Client端,当需要与Server端交换数据时,最终会调用wl_display_dispatch_queue()。其中最主要的是三件事:
- 1.wl_connection_flush()将当前out buffer中的数据通过socket发往Server端。这些数据是之前在wl_connection_write()中写入的。
- 2.通过poll()在socket上等待数据,并通过read_events()将这些数据处理生成函数闭包结构wl_closure,然后放到display的wl_event_queue.event_list事件列表中。wl_closure可以看作是一个函数调用实例,里面包含了一个函数调用需要的所有信息。
- 3.dispatch_queue()->dispatch_event()用于处理前面添加到队列的事件。这里就是把队列中的wl_closure拿出来生成trampoline后进行调用。
四、跨进程过程调用
术语上,Wayland中把Client发给Server的跨进程函数调用称为request,反方向的跨进程函数调用称为event。本质上,它们处理的方式是类似的。要让两个进程通过socket进行函数调用,首先需要将调用抽象成数据流的形式。函数的接口部分是同时链接到Client和Server端的库中的,其中包含了对象所支持的request和event的函数签名。因此这部分不用传输,只要传输目标对象id,方法id和参数列表这些信息就可以了。这些信息会通过wl_closure_marshal()写入wl_closure结构,再由serialize_closure()变成数据流。等到了目标进程后,会从数据流通过wl_connection_demarshal()转回wl_closure。这个过程类似于Android中的Parcel机制。那么问题来了,参数中的整形,字符串什么的都好搞,拷贝就行。但如果参数中包含对象,我们不能把整个对象拷贝过去,也不能传引用过去。那么需要一种机制来作同一对象在Server和Client端的映射,这是通过wl_map实现的。wl_map在Client和Server端各有一个,它们分别存了wl_proxy和wl_resource的数组,且是一一对应的。这些对象在这个数组中的索引作为它们的id。这样,参数中的对象只要传id,这个id被传到目的地后会通过查找这个wl_map表来得到本地相应的对象。在功能上类似于Android中的BpXXX和BnXXX。wl_proxy和wl_resource都包含wl_object对象。这个wl_object和面向对象语言里的对象概念类似,它有interface成员描述了这个对象所实现的接口,implementation是这些接口的实现函数的函数指针数组,id就是在wl_map结构里数组中的索引。前面所说的Client绑定Server端资源的过程就是在Client端创建wl_proxy,在Server端创建wl_resource。然后Client就可以通过wl_proxy调用Server端对应wl_resource的request,Server端就可以通过wl_resource调用Client端对应wl_proxy的event。这个映射过程如下图所示(以wl_registry为例):
与Android不同的是,Android中请求到达Server端,调用时需要在目标对象中有一段Stub来生成调用的上下文。而Wayland中,这是由libffi完成的。
Wayland核心协议是通过protocol/wayland.xml这个文件定义的。它通过wayland_scanner这个程序扫描后会生成wayland-protocol.c, wayland-client-protocol.h和wayland-server-protocol.h三个文件。wayland-client-protocol.h是给Client用的;wayland-server-protocol.h是给Server用的; wayland-protocol.c描述了接口,Client和Server都会用。这里以wl_display的get_registry()这个request为例,分析下跨进程的过程调用是如何实现的。
首先在wayland.xml中申明wl_display有get_registry这个request:
1 2 3 4 5 6 7 8 |
<request name="get_registry"> <description summary="get global registry object"> This request creates a registry object that allows the client to list and bind the global objects available from the compositor. </description> <arg name="registry" type="new_id" interface="wl_registry"/> </request> |
这里的参数类型是new_id,说明需要新建一个代理对象。其它的如object代表一个对象,fd代表代表文件描述符等。
wayland-protocol.c中描述了wl_display这个对象的request和event信息,其中包含了它们的函数签名。get_registry是request中的一项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static const struct wl_message wl_display_requests[] = { { "sync", "n", types + 8 }, { "get_registry", "n", types + 9 }, }; static const struct wl_message wl_display_events[] = { { "error", "ous", types + 0 }, { "delete_id", "u", types + 0 }, }; WL_EXPORT const struct wl_interface wl_display_interface = { "wl_display", 1, 2, wl_display_requests, 2, wl_display_events, }; |
wayland-server-protocol.h中:
1 2 3 4 5 |
struct wl_display_interface { ... void (*get_registry)(struct wl_client *client, struct wl_resource *resource, uint32_t registry); |
这个声明是让Server端定义implementation中的实现函数列表用的,如:
1 2 3 4 |
static const struct wl_display_interface display_interface = { display_sync, display_get_registry }; |
wayland-client-protocol.h中:
1 2 3 4 5 6 7 8 9 10 |
static inline struct wl_registry * wl_display_get_registry(struct wl_display *wl_display) { struct wl_proxy *registry; registry = wl_proxy_marshal_constructor((struct wl_proxy *) wl_display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, NULL); return (struct wl_registry *) registry; } |
这是给Client端用来发起request的。当客户端调用wl_display_get_registry(),由于要返回代理对象,所以调用wl_proxy_mashal_constructor()。返回的wl_registry是一个代理对象。
1 2 3 4 5 6 7 8 9 10 11 12 |
wl_display_get_registry() wl_proxy_marshal_constructor() wl_argument_from_va_list() // 将上面传来的参数按wl_display_interface->methods[WL_DISPLAY_GET_REGISTRY]中签名描述的类型放到wl_argument数组中。 wl_proxy_marshal_array_constructor() new_proxy = create_outgoing_proxy() // 因为get_registry()的request参数中有new_id类型,所以要创建一个代理对象。 proxy_create() //创建wl_proxy。设置interface等信息,然后将该wl_proxy插入到display->objects的wl_map中,返回值为id,其实就是在wl_map中数组中的索引值。这个值是会被发到Server端的,这样Server端就可以在Server端的wl_map数组的相同索引值的位置插入相应的wl_resource。这样逻辑上,就创建了wl_proxy和wl_resource的映射关系。以后,Client和Server间要相互引用对象只要传这个id就可以了。 closure = wl_closure_marshal() //创建wl_closure并根据前面的参数列表初始化。先将前面生成的wl_argument数组拷贝到wl_closure的args成员中。然后根据类型做调整,如将wl_proxy的对象指针改为id,因为传个本地指针到另一个进程是没意义的。 wl_closure_send() // 发送前面生成的wl_closure。 copy_fds_to_connection() // 将参数中的fd放到专门的fd out buffer中。因为它们在发送时是要特殊处理的。 serialize_closure() //将wl_closure转化为byte stream。像类型为object的参数会转化为id。 wl_connection_write() // 放到connection的out buffer,准备通过socket发送。 到这里本地的wl_registry代理对象创建完成,并且准备向Server发出request。当下次执行到wl_display_dispatch_queue()时,会调用wl_connection_flush()把connection中out buffer的request通过socket发出去。当然,在往out buffer写时发现满了也会调用wl_connection_flush()往socket发数据。 |
到了Server端,前面提到会调用处理函数wl_client_connection_data()进行处理:
1 2 3 4 5 6 7 8 9 |
wl_client_connection_data() wl_connection_flush() //向Client发送数据。 wl_connection_read() //从Client接收处理。 while (...) // 循环处理从socket中读到的数据。 wl_connection_copy() // 每次从缓冲中读入64字节。它相当于一个request的header,后面会跟参数数据。其中前4个字节代表是向哪个对象发出request的。后面4个字节包含opcode(代表是这个object的哪个request),及后面参数的长度。 wl_map_lookup() // 在wl_map中查找目标资源对象wl_resource。其成员object中有该对象的接口和实现列表。结合前面的opcode就可以得到相应的request的描述,用wl_message表示。如 { "get_registry", "n", types + 9 }。 wl_connection_demarshal() // 根据interface中的函数签名信息生成函数闭包wl_closure。主要是通过wl_message中对参数的描述从缓冲区中把参数读到wl_closure的args成员中。wl_closure的args成员是wl_argument的数组。因为这里无法预先知道参数类型,所以wl_argument是个union。 wl_closure_lookup_objects() // wl_closure中的参数中如果有object的话,现在还只有id号。这步会通过wl_map把id号转为wl_object。 wl_closure_invoke() //使用libffi库生成trampoline code,跳过去执行。 |
在这个场景下,由于之前在bind_display()中把client->display_resource的implementation设为:
1 2 3 4 |
static const struct wl_display_interface display_interface = { display_sync, display_get_registry }; |
所以接下来会调用到display_get_registry()。这个函数里会创建wl_registry对应的wl_resource对象。创建好后会放到display->registry_resource_list中。前面提到过,这个registry资源逻辑上的作用是Client放在Server端的Observer,它用于监听Server端有哪些Global对象(Service服务)。display_get_registry()函数接下去会对于每一个Global对象向该Client新建的registry发送事件。另外在有Global对象创建和销毁时(wl_global_create()和wl_global_destroy()),Server会向所有的registry发送事件进行通知。因此,Global对象可以理解为可动态加载的Service。
那么,这些Global对象具体都是些什么呢?为了故事的完整性,这里就插播段题外话。Server端的Compositor在启动时一般会注册一些Global对象,逻辑上其实就是一些服务。通过Wayland提供的wl_global_create()添加:
1 2 3 4 5 6 |
wl_global_create() global->name = display->id++; // Global对象的id号。 global->interface = interface; wl_list_insert(display->global_list.prev, &global->link); // display->global_list保存了Global对象的列表。 wl_list_for_each(resource, &display->registry_resource_list, link) // 向之前注册过的registry对象发送这个新创建Global对象的event。 wl_resource_post_event(resource, WL_REGISTRY_GLOBAL, global->name, global->interface->name, global->version); |
以wl_compositor这个Global对象为例, Server端调用wl_global_create(display, &wl_compositor_interface, 3, ec, compositor_bind)。然后当Client端调用wl_display_get_registry()时,Server端的display_get_registry()会对每个Global对象向Client发送global事件,因此Server端有几个Global对象就会发几个event。Client收到event后调用先前注册的回调registry_handle_global()。根据interface name判断当前发来的是哪一个,然后调用wl_reigistry_bind(…, &wl_compositor_interface,..)绑定资源,同时创建本地代理对象。接着Server端相应地调用registry_bind(),其中会调用先前在wl_global_create()中注册的回调函数,即compositor_bind()。接着经过wl_resource_create(), wl_resource_set_implementation()等创建wl_resource对象。也就是说,对于同一个Global对象,每有Client绑定一次,就会创建一个wl_resource对象。换句话说,对于Server来说,每一个Client有一个命名空间,同一个Global对象在每一个Client命名空间的wl_resource是不一样的。这样,对于一个Global对象(Service服务),在Client端创建了wl_proxy,在Server端创建了wl_resource,它们就这样绑定起来了。wl_proxy.object包含了event的处理函数,这是对Server端暴露的接口,而wl_resource.object包含了request的处理函数,这是对Client暴露的接口。
回到故事主线上,前面是从Client端调用Server端对象的request的流程,从Server端向Client端对象发送event并调用其回调函数的过程也是类似的。下面以display_get_registry()中向Client端发送global事件为例分析下流程。Server端通过wl_resource_post_event()来向Client发送event。
1 2 3 4 5 6 7 |
wl_resource_post_event() wl_resource_post_event_array() wl_closure_marshal() // 封装成wl_closure,其中会转化object等对象。 wl_closure_send() copy_fds_to_connection() serialize_closure() // 将closure序列化成数据流,因为将要放到socket上传输。 wl_connection_write() |
这样event就被放到connection的out buffer中,等待从socket上发送。那么,Client是怎么读取和处理这些event呢?首先Client端需要监听这个wl_proxy,这是通过调用wl_registry_add_listener()->wl_proxy_add_listener()设置的。该函数的参数中包含了这个event的处理函数列表registry_listener,它对应的接口在前面调用wl_display_get_registry()时已设置成wl_registry_interface。wl_registry_interface是在前面根据wayland.xml自动生成的一部分。这里体现了event与request的一点细微差别,request是Server端都要处理的,而event在Client可以选择不监听。
然后在Client的主循环中会调用wl_display_dispatch_queue()来处理收到的event和发出out buffer中的request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
wl_display_dispatch_queue() dispatch_queue() wl_connection_flush() read_events() // 从connection的in buffer中读出数据,转为wl_closure,插入到queue->event_list,等待后续处理。 wl_connection_read() queue_event() //这块处理有点像Server端的wl_client_connection_data(),区别在于这里用的是wl_reigstry_interface的events列表而不是methods列表。 wl_connection_copy() wl_map_lookup() // 查找目标代理对象wl_proxy。 wl_connection_demarshal() // 从connection的缓冲区中读入数据,结合函数签名生成wl_closure。 create_proxies() wl_closure_lookup_objects() dispatch_queue() // 将前面插入到queue当中的event(wl_closure)依次拿出来处理。 dispatch_event(queue) // display->display_queue->event_list的每一个元素是一个wl_closure,代表一个函数调用实例,最后通过wl_closure_invoke()进行调用。 wl_closure_invoke() |
这样该event的相应处理回调函数就被调用了,在这个场景中,即registry_handle_global()。下图简单地描绘了整个流程。
0 条评论