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);
}
...
}
偷窥代码分析后发现这段代码并没有详细指出分发事件的时候的优先级。仔细想想应该是 sortEventListener
和 pfnDispatchEventToListeners
中在搞鬼。
先分析 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
可以指向两个方法:dispatchEventToListeners
和 dispatchTouchEventToListeners
。其中两个方法除了分发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
的时候,就会给当前 Listener
的 ListenerID
添加一个 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;
这时候分发自定义事件是无效的。