Cocos2dx-v3.10 事件分发机制源码解析

Cocos的事件分发机制,怎么说呢,总感觉有些乱,借此整理一下。先看看与事件分发相关的类。

事件相关的类

Event 相关类, 分发事件的中的事件:

Event(基类), EventCustom(自定义事件), EventTouch(触摸事件), EventMouse(鼠标事件), EventKeyboard(键盘事件), EventFocus(控件获取焦点事件), EventAcceleration(加速计事件)

事件监听器:

EventListener, EventListenerCustom, EventListenerFocus, EventListenerMouse, EventListenerTouch, EventListenerKayboard, EventListenerAcceleration

事件ID

ListenerID(事件的区分标志,其实就是std::string

事件分发器:

EventDispatcher(事件分发机制逻辑集合体)

创建事件

创建事件就简单的new一个Event的子类即可。

事件监听器的创建与监听

事件监听器,也就是说EventListener。添加事件监听器有三个方法,都在EventDispatch中,分别是:

/** Adds a event listener for a specified event with the priority of scene graph.
 *  @param listener The listener of a specified event.
 *  @param node The priority of the listener is based on the draw order of this node.
 *  @note  The priority of scene graph will be fixed value 0. So the order of listener item
 *          in the vector will be ' <0, scene graph (0 priority), >0'.
*/
void addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node);
 
/** Adds a event listener for a specified event with the fixed priority.
 *  @param listener The listener of a specified event.
 *  @param fixedPriority The fixed priority of the listener.
 *  @note A lower priority will be called before the ones that have a higher value.
 *        0 priority is forbidden for fixed priority since it's used for scene graph based priority.
*/
void addEventListenerWithFixedPriority(EventListener* listener, int fixedPriority);
 
/** Adds a Custom event listener.
It will use a fixed priority of 1.
* @param eventName A given name of the event.
* @param callback A given callback method that associated the event name.
* @return the generated event. Needed in order to remove the event from the dispatcher
*/
EventListenerCustom* addCustomEventListener(const std::string &eventName, const std::function<void(EventCustom*)>& callback);

事件分发的时候主要有两种优先级
第一种是:ScenePriority
第二种是:FixedPriority
仔细跳到addEventListenerWithSceneGraphPriority函数去看看会发现:

void EventDispatcher::addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node)
{
    ...
    // 设置优先级
    listener->setFixedPriority(0);
    ...
 
    addEventListener(listener);
}
 
EventListenerCustom* EventDispatcher::addCustomEventListener(const std::string &eventName, const std::function<void(EventCustom*)>& callback)
{
    EventListenerCustom *listener = EventListenerCustom::create(eventName, callback);\
    // 设置优先级
    addEventListenerWithFixedPriority(listener, 1);
    return listener;
}

其实事件监听器保存的优先级其实只有 FixPriority

EventListener这个类的方法中也可以看出来,里面涉及到保存优先级的只有setFixedPriority这一个函数。

说到优先级,那么事件分发的时候是怎么处理这些优先级的呢?官网只有写 SceneGraphPriority 的优先级是怎么处理的,那么 FixPriority 的优先程度和数值是什么关系,这就去偷窥内部了。

事件分发优先级顺序:
我们来脱掉 EventDispatcher 的衣服看看:

void EventDispatcher::dispatchEvent(Event* event)
{
    ...
    // 先通过event获取到事件的标志ListenerID
    auto listenerID = __getListenerID(event);
    // 排序此事件的所有的监听器
    sortEventListeners(listenerID);
    // 分发事件逻辑的函数指针
    auto pfnDispatchEventToListeners = &EventDispatcher::dispatchEventToListeners;
    if (event->getType() == Event::Type::MOUSE) {
        // 如果是鼠标事件重新赋值分发事件的函数指针
        pfnDispatchEventToListeners = &EventDispatcher::dispatchTouchEventToListeners;
    }
    // 获取改事件的所有的监听器
    auto iter = _listenerMap.find(listenerID);
    if (iter != _listenerMap.end())
    {
        // 如果有,取出里面监听器的Vector
        auto listeners = iter->second;
        // 找到对应的监听器的时候会触发的回调函数
        auto onEvent = [&event](EventListener* listener) -> bool{
            event->setCurrentTarget(listener->getAssociatedNode());
            // 触发onEvent回调
            listener->_onEvent(event);
            return event->isStopped();
        };
        // 调用函数指针分发事件
        (this->*pfnDispatchEventToListeners)(listeners, onEvent);
    }
 
    ...
}

偷窥代码分析后发现这段代码并没有详细指出分发事件的时候的优先级。仔细想想应该是 sortEventListenerpfnDispatchEventToListeners 中在搞鬼。
先分析 sortEventListener,脱掉她的胖次去里面看看。

void EventDispatcher::sortEventListeners(const EventListener::ListenerID& listenerID)
{
    ...
        ...
 
        if ((int)dirtyFlag & (int)DirtyFlag::FIXED_PRIORITY)
        {
            // 对FixPriority优先级类型的监听器排序
            sortEventListenersOfFixedPriority(listenerID);
        }
 
        if ((int)dirtyFlag & (int)DirtyFlag::SCENE_GRAPH_PRIORITY)
        {
            auto rootNode = Director::getInstance()->getRunningScene();
            if (rootNode)
            {
                // 对SceneGraphPriority类型的监听器排序
                sortEventListenersOfSceneGraphPriority(listenerID, rootNode);
            }
            ...
        }
    }
}
 
void EventDispatcher::sortEventListenersOfFixedPriority(const EventListener::ListenerID& listenerID)
{
    auto listeners = getListeners(listenerID);
 
    ...
 
    // After sort: priority < 0, > 0 排序,根据FixedPriority的数值大小,越小的优先级越高
    std::sort(fixedListeners->begin(), fixedListeners->end(), [](const EventListener* l1, const EventListener* l2) {
        return l1->getFixedPriority() < l2->getFixedPriority();
    });
 
    ...
}
 
void EventDispatcher::sortEventListenersOfSceneGraphPriority(const EventListener::ListenerID& listenerID, Node* rootNode)
{
    auto listeners = getListeners(listenerID);
 
    ...
    auto sceneGraphListeners = listeners->getSceneGraphPriorityListeners();
 
    ...
 
    // Reset priority index
    _nodePriorityIndex = 0;
    _nodePriorityMap.clear();
 
    // 从当前根节点rootNode开始,遍历整棵节点树,并标记上相应的节点等级,父节点的优先级比子节点的大
    visitTarget(rootNode, true);
 
    // After sort: priority < 0, > 0 因为SceneGraphPriority都是FixPriority为0的事件类型,所以比较节点在渲染书中的优先级
    std::sort(sceneGraphListeners->begin(), sceneGraphListeners->end(), [this](const EventListener* l1, const EventListener* l2) {
        return _nodePriorityMap[l1->getAssociatedNode()] > _nodePriorityMap[l2->getAssociatedNode()];
    });
 
    ...
}

上面写了两种优先级的排序,一种是 FixPriority,优先级根据 fixedPriority 的数值从小往大排序、另一种是 SceneGraphPriority,根据节点在渲染树种的优先级排序,具体怎么样官网有解释,这里不做展开。

值的注意的是 sortEventListener 的时候,判断当前 ListenerID 的类型是用位标记来判断的,一个 int 类型的 flag。也就是说明一个 listenerID 既可以是 SceneGraphPriority 也可以是 FixedPriority,那么实际分发的时候这两个的优先级怎么排?答案就在 pfnDispatchEventToListeners

pfnDispatchEventToListeners 可以指向两个方法:dispatchEventToListenersdispatchTouchEventToListeners。其中两个方法除了分发SceneGraphPriority的时候不一样外,其他的一样,为了方便起见,这里只分析 dispatchEventToListeners

void EventDispatcher::dispatchEventToListeners(EventListenerVector* listeners, const std::function<bool(EventListener*)>& onEvent)
{
    bool shouldStopPropagation = false;
    auto fixedPriorityListeners = listeners->getFixedPriorityListeners();
    auto sceneGraphPriorityListeners = listeners->getSceneGraphPriorityListeners();
 
    ssize_t i = 0;
    // priority < 0 优先处理priority小于0的时候的事件监听器
    if (fixedPriorityListeners)
    {
        CCASSERT(listeners->getGt0Index() <= static_cast<ssize_t>(fixedPriorityListeners->size()), "Out of range exception!");
 
        if (!fixedPriorityListeners->empty())
        {
            for (; i < listeners->getGt0Index(); ++i)
            {
                auto l = fixedPriorityListeners->at(i);
                // 判断是否可以执行事件,如果可以最后调用onEvent执行,如果onEvent返回true,说明吞噬事件,结束分发。
                if (l->isEnabled() && !l->isPaused() && l->isRegistered() && onEvent(l))
                {
                    shouldStopPropagation = true;
                    break;
                }
            }
        }
    }
    // 接下来分发SceneGraphPriority的事件
    if (sceneGraphPriorityListeners)
    {
        // 判断事件是否已经终止发送
        if (!shouldStopPropagation)
        {
            // priority == 0, scene graph priority
            for (auto& l : *sceneGraphPriorityListeners)
            {
                if (l->isEnabled() && !l->isPaused() && l->isRegistered() && onEvent(l))
                {
                    shouldStopPropagation = true;
                    break;
                }
            }
        }
    }
    // 最后分发到fixedPriority > 0 的监听器
    if (fixedPriorityListeners)
    {
        if (!shouldStopPropagation)
        {
            // priority > 0
            ssize_t size = fixedPriorityListeners->size();
            for (; i < size; ++i)
            {
                auto l = fixedPriorityListeners->at(i);
 
                if (l->isEnabled() && !l->isPaused() && l->isRegistered() && onEvent(l))
                {
                    shouldStopPropagation = true;
                    break;
                }
            }
        }
    }
}

这里面清楚的写出了事件分发时候的逻辑处理,先分发事件到 fixedPriority < 0 的监听器中,然后再分发到 = 0 的监听器(SceneGraphPriority)中,最后在分发到 > 0 的监听器中,如果中途出现 onEvent 返回为 true 的结果,则终止分发。

Ps:这里的 onEvent 调用的其实就是上面 dispatchEvent 代码中的 lambda 表达式。如果想要创建一个触摸事件的优先级比当前所有的触摸事件优先级都高话,只需要把 fixedPriority 的数值设为 < 0 即可。

当正在分发事件的时候(_inDispath > 0),添加和删除监听器事件

EventDispatcher 中有表示当前分发的事件数的私有成员变量 _inDispatch ,它是一个 int 类型的数据,用于表示当前正有多少事件正在分发。既然是表示事件正在分发的数量,可定有 ++, -- 的操作,在DispatchEvent中会有+1和-1的操作,但是藏得比较深,怎么个深法,看下述源码。

void EventDispatcher::dispatchEvent(Event* event)
{
    ...
 
    DispatchGuard guard(_inDispatch);
 
    ...
}

我去,这啥都都没有啊???发现这里面只有使用 _inDispatch 创建了一个变量,并没有++,- -操作。其实 DispatchGuard 这个类的构造函数的参数是一个 int 类型的引用,构造时候对其+1,析构函数的时候会-1操作。用法很妙,借助了局部变量是在栈里面这个特性,当方法DispatchEvent结束的时候,局部变量guard会析构,此时会-1操作。下面是 DispatchGuard 的源码。

class DispatchGuard
{
public:
    DispatchGuard(int& count):
            _count(count)
    {
        ++_count;
    }
 
    ~DispatchGuard()
    {
        --_count;
    }
 
private:
    int& _count;
};

了解了_inDispatch的意义,我们来看看添加事件监听器和删除事件监听器的时候,如果正在分发事件(_inDispatch > 0)会怎么处理。

void EventDispatcher::addEventListener(EventListener* listener)
{
    if (_inDispatch == 0)
    {
        forceAddEventListener(listener);
    }
    else
    {
        _toAddedListeners.push_back(listener);
    }
 
    listener->retain();
}

上述代码是监听器添加的时候的代码,可以看到如果当时正在分发事件,会把当前需要添加的监听器添加到待添加向量(_toAddedListeners)中,那么也就是说在事件分发完毕之后监听器需要从toAddedListeners中转移到正式向量中,这部分代码可以在updateListeners中看到,此方法会在事件分发结束之后调用。

void EventDispatcher::updateListeners(Event* event)
{
    CCASSERT(_inDispatch > 0, "If program goes here, there should be event in dispatch.");
 
    if (_inDispatch > 1)
        return;
 
    ...
 
    if (!_toAddedListeners.empty())
    {
        for (auto& listener : _toAddedListeners)
        {
            forceAddEventListener(listener);
        }
        _toAddedListeners.clear();
    }
 
    if (!_toRemovedListeners.empty())
    {
        cleanToRemovedListeners();
    }
}

代码中,除了添加事件监听器有个待加入向量外,删除事件监听器也有一个待删除向量(不过这个好像是废话),开头有判断当前是否还是处于事件分发中。

Cocos Bug 之 DirtyFlag

上面的代码中,我删了一些代码,因为代码太多影响阅读。其中就有很多涉及到了Dirty Flag,望文取义的话是脏标记,问题是这个脏是什么脏?

下面来看一下添加监听器和移除监听器的代码部分。

// 添加监听器
void EventDispatcher::forceAddEventListener(EventListener* listener)
{
    EventListenerVector* listeners = nullptr;
    ...
 
    if (listener->getFixedPriority() == 0)
    {
        setDirty(listenerID, DirtyFlag::SCENE_GRAPH_PRIORITY);
 
        ...
    }
    else
    {
        setDirty(listenerID, DirtyFlag::FIXED_PRIORITY);
    }
}
 // 移除监听器
void EventDispatcher::removeEventListener(EventListener* listener)
{
    ...
 
    for (auto iter = _listenerMap.begin(); iter != _listenerMap.end();)
    {
        ...
 
        removeListenerInVector(sceneGraphPriorityListeners);
        if (isFound)
        {
            // fixed #4160: Dirty flag need to be updated after listeners were removed.
            setDirty(listener->getListenerID(), DirtyFlag::SCENE_GRAPH_PRIORITY);
        }
        else
        {
            removeListenerInVector(fixedPriorityListeners);
            if (isFound)
            {
                setDirty(listener->getListenerID(), DirtyFlag::FIXED_PRIORITY);
            }
        }
 
    ...
}

代码中可以看到,这个 DirtyFlag 是设置给 ListenerID 的,每当新添加一个 Listener,或则删除一个 Listener 的时候,就会给当前 ListenerListenerID 添加一个 DirtyFlag。说明这个脏是指 ListenerID 对应的监听器向量列表需要重新排序了,如果不脏就不需要排序。

那么问题来了:
照理只要出现了删除,修改,添加监听器的时候,监听器列表需要重新排序,都需要设置相应的 DirtyFlag 操作。但是 Cocos-2dx v3.10 里面的 updateListeners 函数有删除监听器的操作,然而并没有设置相应的 DirtyFlag 操作。此问题我在 Cocos2dx github 的 issues中有回答问题链接

这个就是一个 Bug 了,放着不管会抛出以下异常

CCASSERT(listeners->getGt0Index() <= static_cast<ssize_t>(fixedPriorityListeners->size()), "Out of range exception!");

代码中的 Gt0Index() 方法其实就是获取到当前监听器里诶包中 fixedPriority == 0 的监听器在监听器向量中的位置,它只有在给 Listener 排序的时候会设置,但是如果更新了对应 ListenerID 的向量(EventListenerVector),但是没有重新排序,就会出现 _gt0Index 未及时更新的情况,导致抛出这个异常。
排序的时候,会判断排序的这个 ListenerID 是否处于Dirty的状态,只有脏状态才会排序,这算优化吧,所以必须在 updateListener 的时候加上 DirtyFlag

Bug 修复

void EventDispatcher::updateListeners(Event* event)
{
   ...
 
    auto onUpdateListeners = [this](const EventListener::ListenerID& listenerID)
    {
        ...
 
        if (sceneGraphPriorityListeners)
        {
            for (auto iter = sceneGraphPriorityListeners->begin(); iter != sceneGraphPriorityListeners->end();)
            {
                auto l = *iter;
                if (!l->isRegistered())
                {
                    ...
                    // if item in toRemove list, remove it from the list
                    setDirty(l->getListenerID(), DirtyFlag::SCENE_GRAPH_PRIORITY);
                    ..
                }
                ...
            }
        }
 
        if (fixedPriorityListeners)
        {
            for (auto iter = fixedPriorityListeners->begin(); iter != fixedPriorityListeners->end();)
            {
                auto l = *iter;
                if (!l->isRegistered())
                {
                    ...
                    // if item in toRemove list, remove it from the list
                    setDirty(l->getListenerID(), DirtyFlag::FIXED_PRIORITY);
                    ...
                }
                ...
            }
        }
 
        ...
    };
 
    ...
}

PS: 场景切换的时候 EventDispatcher 会设置成 _isEnabled = false; 这时候分发自定义事件是无效的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,490评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,748评论 6 342
  • 放逐山下,青烟缭绕。 “哎,小鬼,你怎么这么小就成鬼了?”饿鬼双眼凹陷,似两个大黑窟窿,盯着他说道。 “我说饿鬼啊...
    骄傲的橘子阅读 784评论 0 0
  • 爸: 清明节前夕我和舅舅、妹妹、表姐去看您,一切尚好。只是去时我把要烧的纸忘在出租车上了,懊恼不己。好像每次去看您...
    熹喜阅读 273评论 0 0