news 2026/4/15 14:20:11

ZLToolKit模块(三) NoticeCenter(事件广播)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZLToolKit模块(三) NoticeCenter(事件广播)

在 ZLMediaKit/ZLToolKit 的架构中,模块之间的解耦至关重要。NoticeCenter(通知中心)正是为此而生。它实现了一个观察者模式发布-订阅模式(Publish-Subscribe Pattern),充当了整个系统的“中枢神经”,负责将事件从发生地精准地投递给感兴趣的监听者。

1. 核心设计理念与原理

NoticeCenter的核心目标是:让事件的产生者(Emitter)不需要知道谁在监听,事件的消费者(Listener)也不需要知道谁发出的事件。

1.1 双层映射结构

为了高效管理成千上万的事件,NoticeCenter采用了双层 Hash Map的结构:

  1. 第一层(事件层)NoticeCenter内部维护一个unordered_map,Key 是事件名称(string),Value 是EventDispatcher(事件分发器)。
    • 作用:根据事件名快速找到对应的分发器。
  2. 第二层(监听层):每个EventDispatcher内部维护一个unordered_multimap,Key 是监听者标识(void* tag),Value 是回调函数(Any)
    • 作用:存储该特定事件的所有监听回调。

1.2 类型擦除与变参模板

C++ 是强类型语言,但通用的事件中心需要支持任意参数的回调。NoticeCenter结合了以下技术:

  • Any类型:类似于 C++17 的std::any,用于存储任意类型的函数对象(std::function),实现了回调函数的类型擦除。
  • 变参模板(Variadic Templates)emitEvent方法接受ArgsType &&...args,允许发射携带任意数量和类型参数的事件。

2. 核心类分析

2.1EventDispatcher(幕后英雄)

它是实际干活的类,对应“某一个特定事件”的管理者。

  • 职责:存储、删除、触发监听器。
  • 关键点
    • 防死锁机制:在emitEvent触发回调时,它会先拷贝一份监听列表,然后释放锁,再遍历拷贝的列表执行回调。
    • 为什么这么做?如果在回调函数中又调用了addListenerdelListener,如果不释放锁,就会导致死锁(Self-Deadlock)。
    • 中断机制:支持InterruptException。如果某个监听器抛出此异常,事件广播将立即停止,不再通知后续监听器。

2.2NoticeCenter(大管家)

它是单例模式(Singleton)的对外入口。

  • 职责:管理所有的EventDispatcher
  • 接口
    • addListener: 注册监听。
    • delListener: 移除监听(支持按 tag 移除单个,或按 tag 移除所有事件的监听)。
    • emitEvent: 发射事件。

2.3NoticeHelper(语法糖)

这是一个模板辅助类,配合宏NOTICE_EMIT使用。

  • 作用:简化代码书写,利用模板推导自动匹配参数类型,让调用看起来更像函数调用。

3. 类图概览

manages >
1
n
stores callbacks >
1
n
«Singleton»
NoticeCenter
-std::unordered_map _mapListener
-std::recursive_mutex _mtxListener
+static Instance()
+addListener(tag, event, func)
+delListener(tag, event)
+emitEvent(event, args...)
EventDispatcher
-std::unordered_multimap _mapListener
-std::recursive_mutex _mtxListener
+addListener(tag, func)
+delListener(tag, empty)
+emitEvent(safe, args...)
«Type Erasure»
Any
+set(value)
+get(safe)

4. 关键实现细节解析

4.1 线程安全与性能平衡

NoticeCenter在多线程环境下被高频使用,因此线程安全至关重要。

  • 使用了std::recursive_mutex(递归锁)。这允许同一个线程在持有锁的情况下再次获取锁,防止在复杂的嵌套调用(如回调中移除监听)中发生死锁。
  • Copy-On-Write (类似思想):如前所述,EventDispatcher::emitEvent中:
{std::lock_guard<std::recursive_mutex>lck(_mtxListener);copy=_mapListener;// 1. 临界区内只做拷贝,速度快}// 2. 临界区外执行回调,避免阻塞其他线程的添加/删除操作for(auto&pr:copy){...}

4.2tag指针的作用

addListener的第一个参数是void *tag

  • 通常用法:传入this指针。
  • 目的:当对象析构时,需要取消它注册的所有监听。通过tag(即对象的地址),NoticeCenter可以快速定位并移除该对象注册的所有回调,防止悬垂指针导致的崩溃。

5. 使用场景与示例

场景一:系统启动/停止通知

当服务器初始化完成或准备关闭时,通知各个模块。

监听者 (Listener):

classPlayer{public:Player(){// 关注 "SERVER_INIT" 事件// tag 传 this,方便析构时移除NoticeCenter::Instance().addListener(this,"SERVER_INIT",[](intport,conststring&ip){printf("Server started at %s:%d\n",ip.c_str(),port);});}~Player(){// 移除该对象的所有监听NoticeCenter::Instance().delListener(this);}};

触发者 (Emitter):

voidstartServer(){intport=8080;string ip="0.0.0.0";// 广播事件,参数自动透传NoticeCenter::Instance().emitEvent("SERVER_INIT",port,ip);}

场景二:使用宏简化调用

ZLToolKit 提供了NOTICE_EMIT宏,让代码更优雅。

// 定义事件参数签名// 这里的 void(int, string) 对应回调函数的签名NOTICE_EMIT(void(int,string),"SERVER_INIT",8080,"0.0.0.0");

场景三:中断事件传播

假设有一个鉴权事件,如果第一个监听者鉴权失败,不希望后续监听者继续处理。

NoticeCenter::Instance().addListener(this,"AUTH_USER",[](conststring&user){if(user=="admin"){// 鉴权通过}else{// 抛出中断异常,后续的回调将不会被执行throwEventDispatcher::InterruptException();}});

6. 总结

NoticeCenter是 ZLToolKit 中一个短小精悍但功能强大的组件。

  • 优点:解耦性强、线程安全、支持任意参数、支持事件中断。
  • 注意
    • 回调函数是在触发线程中执行的。如果回调处理耗时过长,会阻塞触发线程。对于耗时操作,建议在回调中抛转到线程池处理。
    • 务必在对象析构时调用delListener(this),否则会导致野指针回调崩溃。

通过理解NoticeCenter,你就能掌握 ZLMediaKit 中流媒体状态变化、Hook 机制等底层通信的脉络。

7. C++11

7.1 using和typedef的区别

using可以直接定义模板函数的别名:

template<typename T> using ptr = std::unique_ptr<T>; ptr<int> p(new int(10)); //正确 template<typename T> typedef std::unique_ptr<T> ptr; //错误 template<typename T> struct A { typedef std::unique_ptr<T> ptr; }; A<int>::ptr x(new int(10)); //正确

7.2 function_traits

function_traits是 ZLToolKit 中一个非常核心的 C++模板元编程(Template Metaprogramming)工具类。

它的主要作用是类型萃取(Type Extraction):在编译期解析出任何“可调用对象”(函数、函数指针、std::function、Lambda 表达式、仿函数、成员函数)的详细类型信息,例如返回值类型、参数个数、参数类型等。

template<typename T> struct function_traits; //普通函数, lambda表达式 template<typename Ret, typename... Args> struct function_traits<Ret(Args...)> //特化版本 { public: enum { arity = sizeof...(Args) }; typedef Ret function_type(Args...); typedef Ret return_type; using stl_function_type = std::function<function_type>; typedef Ret(*pointer)(Args...); template<size_t I> struct args { static_assert(I < arity, "index is out of range, index must less than sizeof Args"); using type = typename std::tuple_element<I, std::tuple<Args...> >::type; }; }; //函数指针 template<typename Ret, typename... Args> struct function_traits<Ret(*)(Args...)> : function_traits<Ret(Args...)>{}; //std::function template <typename Ret, typename... Args> struct function_traits<std::function<Ret(Args...)>> : function_traits<Ret(Args...)>{}; //member function #define FUNCTION_TRAITS(...) \ template <typename ReturnType, typename ClassType, typename... Args>\ struct function_traits<ReturnType(ClassType::*)(Args...) __VA_ARGS__> : function_traits<ReturnType(Args...)>{}; \ FUNCTION_TRAITS() FUNCTION_TRAITS(const) FUNCTION_TRAITS(volatile) FUNCTION_TRAITS(const volatile) //函数对象 template<typename Callable> struct function_traits : function_traits<decltype(&Callable::operator())>{};

可以通过这个类获取将任何函数类型转为stl的function类型。也可以活获取参数类型。
noticeCenter里有如下使用:

template<typename FUNC> void addListener(void *tag, FUNC &&func) { using funType = typename function_traits<typename std::remove_reference<FUNC>::type>::stl_function_type; std::shared_ptr<void> pListener(new funType(std::forward<FUNC>(func)), [](void *ptr) { funType *obj = (funType *) ptr; delete obj; }); std::lock_guard<std::recursive_mutex> lck(_mtxListener); _mapListener.emplace(tag, pListener); }

这里通过function_traitsstl_function_type得到stl的function类型,并构造了该类型的对象。

7.3 function

可以存储各种函数、函数指针、lambda 表达式等可调用对象。
例如,你可以使用std::function<int(int, float)>来存储一个参数类型为intfloat,返回值类型为int的可调用对象。

  • 将函数、函数指针、lambda 表达式等存储在一个变量中,方便传递和调用;
  • 在不确定可调用对象的具体类型的情况下,接受各种类型的可调用对象;
  • 在运行时动态绑定可调用对象;
  • 用于实现回调函数等;
  • 可以使用std::bind将函数或函数对象绑定到一个function变量中

例如:

#include <functional> int func(int x, float y) { return static_cast<int>(x + y); } int func2(int x, int y, int z) { return x + y + z; } struct FuncObj { int operator()(int x, float y) { return static_cast<int>(x * y); } }; int main() { std::function<int(int, float)> f1 = func; f1(1, 2.5f); // Output: 3 std::function<int(int, float)> f2 = [](int x, float y) { return static_cast<int>(x * y); }; f2(3, 0.5f); // Output: 1 std::function<int(int, float)> f3 = std::bind(func, std::placeholders::_1, std::placeholders::_2); std::cout << f1(1, 2.5f) << std::endl; // Output: 3 std::function<int(int, float)> f4 = std::bind(FuncObj(), std::placeholders::_1, std::placeholders::_2); std::cout << f4(3, 0.5f) << std::endl; // Output: 1 std::function<int(int)> f5 = std::bind(func2, 1, 2, std::placeholders::_1); std::cout << f5(3) << std::endl; // Output: 6 return 0; }

7.4 tuple_element

可以用于提取元组类型中某一位置的元素的类型。

using TupleType = std::tuple<int, float, double>; using ElementType = std::tuple_element<1, TupleType>::type; std::cout << typeid(ElementType).name() << std::endl; // Output: "float"
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/12 8:53:04

36.泛化建模进阶(下)-不使用泛化的替代方案如何权衡-附决策树

36 泛化建模进阶(下):不使用泛化的替代方案,如何权衡? 你好,欢迎来到第 36 讲。 在上一讲中,我们学习了泛化建模的适用场景和判断标准。我们知道,当业务概念存在稳定的“角色”或“种类”划分,并且子类之间存在独有的状态差异时,使用泛化(继承)是一个自然而优雅的…

作者头像 李华
网站建设 2026/4/14 15:22:32

ModernWMS部署实战:中小企业如何快速构建专业级仓储管理系统

ModernWMS部署实战&#xff1a;中小企业如何快速构建专业级仓储管理系统 【免费下载链接】ModernWMS The open source simple and complete warehouse management system is derived from our many years of experience in implementing erp projects. We stripped the origina…

作者头像 李华
网站建设 2026/4/11 9:20:47

50.CQRS进阶(上)-CQRS代码架构设计命令端+查询端-附完整实现

50 CQRS 进阶(上):CQRS 的代码架构设计(命令端 + 查询端) 你好,欢迎来到第 50 讲。 在入门篇中,我们已经理解了 CQRS 的核心思想——读写分离,以及它的三大适用场景。我们知道,在逻辑上,CQRS 将系统划分为“命令处理端”和“查询处理端”。 现在,是时候将这个逻辑…

作者头像 李华
网站建设 2026/4/16 2:23:25

Conductor微服务编排引擎终极实战指南:从入门到精通

Conductor微服务编排引擎终极实战指南&#xff1a;从入门到精通 【免费下载链接】conductor Conductor is a microservices orchestration engine. 项目地址: https://gitcode.com/GitHub_Trending/co/conductor 微服务编排引擎Conductor是Netflix开源的核心项目&#x…

作者头像 李华
网站建设 2026/4/15 6:17:20

常用的 SQL 语句分类及其语法:

好的&#xff0c;以下是常用的 SQL 语句分类及其语法&#xff1a;1. 数据查询 (SELECT)基本查询&#xff1a;SELECT 列名1, 列名2, ... FROM 表名;查询所有列&#xff1a;SELECT * FROM 表名;查询表数据&#xff1a;SELECT COUNT(*) FROM 表名带条件查询 (WHERE)&#xff1a;SE…

作者头像 李华
网站建设 2026/4/14 17:20:39

ECM CCCM

✅ ECM19.0 中的 CCCM&#xff1a;跨分量相关模型详解 在 VVC&#xff08;H.266&#xff09; 的实验参考模型 ECM&#xff08;Experimental Common Model&#xff09;19.0 中&#xff0c;CCCM&#xff08;Cross-Component Correlation Model&#xff09; 是一项关键的屏幕内容编…

作者头像 李华