1 事件的工作机制
图1
传统事件系统如上图,模块A为事件触发者,模块B为事件响应者。A的实现依赖于模块B的实现,如果B的实现发生变化,A也可能需要作出相应调整。Cocos用订阅者模式将事件的触发者和响应者分开。触发者向一个公共的事件分发器发送一个事件消息,事件响应者向事件分发器订阅一个特定类型的消息来响应事件。以图1为例,B创建一个订阅者(ListenerB)并将此订阅者注册至事件分发器中,其中ListenerB附带了响应事件时需要执行的回调函数地址callBackFunc。当事件发生时,A使得事件发生器发出消息通知,以此触发B中的回调函数。
// 事件触发者A 以按下事件 TouchesBegin 为例
void GLView::handleTouchesBegin(int num,intptr_t ids[],float xs[],float ys[])
{
// ...
touchEvent._eventCode = EventTouch::EventCode::BEGAN;
// 当游戏发生 TouchesBegin 事件时,通知分发器发送相关事件
auto dispatcher = Director::getInstance()->getEventDispatcher();
dispatcher->dispatchEvent(&touchEvent);
}
// 事件响应者B
void Widget::setTouchEnabled(bool enable)
{
if (enable == _touchEnabled)
{
return;
}
_touchEnabled = enable;
if (_touchEnabled)
{
// 为widget创建一个订阅者
_touchListener = EventListenerTouchOneByOne::create();
CC_SAFE_RETAIN(_touchListener);
_touchListener->setSwallowTouches(@H_301_37@true);
// 将响应事件时需要回调的函数加至订阅者中
_touchListener->onTouchBegan = CC_CALLBACK_2(Widget::onTouchBegan,this);
_touchListener->onTouchMoved = CC_CALLBACK_2(Widget::onTouchMoved,this);
_touchListener->onTouchEnded = CC_CALLBACK_2(Widget::onTouchEnded,this);
_touchListener->onTouchCancelled = CC_CALLBACK_2(Widget::onTouchCancelled,this);
// 将此订阅者注册至事件分发器中
_eventDispatcher->addEventListenerWithSceneGraPHPriority(_touchListener,this);
}
else
{
_eventDispatcher->removeEventListener(_touchListener);
CC_SAFE_RELEASE_NULL(_touchListener);
}
}
2 Cocos2dx中的事件分发器
2.1 基本组成
事件处理机制的组成部分包括:事件源、订阅者与分发者。事件源包含了该事件的类型Type与listenerID。订阅者包含了订阅者类型与listenerID,因此三者之间的关系是:分发器EventDispatch根据事件的类型找到对应的listenerID,进而找到所有该事件的订阅者。
2.2 注册订阅者
2.2.1 事件优先级
指定事件优先级有两个作用:1)让某些元素优先处理,并不再向后面的订阅者传递;2)控制元素间逻辑处理上的优先级。
// 方式1:通过关联UI指定事件分发优先级
@H_301_37@void EventDispatcher::addEventListenerWithSceneGraPHPriority(EventListener* listener,Node* node)
{
if (!listener->checkAvailable())
return;
// 关联到特定node
listener->setAssociatedNode(node);
// 优先级数字默认为0
listener->setFixedPriority(0);
listener->setRegistered(@H_301_37@true);
addEventListener(listener);
}
// 方式2:指定一个整数的优先级
@H_301_37@void EventDispatcher::addEventListenerWithFixedPriority(EventListener* listener,int fixedPriority)
{
if (!listener->checkAvailable())
return;
listener->setAssociatedNode(nullptr);
listener->setFixedPriority(fixedPriority);
listener->setRegistered(@H_301_37@true);
listener->setPaused(@H_301_37@false);
addEventListener(listener);
}
关联到具体UI的优先级指定方式很多时候要优于指定整数优先级的方式。后者需要开发者创建并关注一堆毫无意义的优先级枚举变量,这很有可能会导致某些问题,如:UI层级低的事件优先级数值比层级高的数值大,具体表现为:被遮挡的UI响应了触发事件而位于前面的UI却无任何反应。这肯定是不合理的。实际上,通过UI设置事件优先级的机制是在Cocos2dx 3.x之后引入的。
2.2.2 添加订阅者
上述关联代码需要关注的一个函数是addEventListener
。事件的分发是可以嵌套的,即可以在一个事件中触发另一个事件。_inDispatch记录了事件的嵌套数目,0表示没有事件需要分发。何时会发生事件的循环嵌套?举例说明:当按下A节点时,分发Touch事件,执行相关onTouchEvent
函数,该函数内实现了一个自定义事件,并再次调用事件分发函数,此时就产生了嵌套。
@H_301_37@void EventDispatcher::addEventListener(EventListener* listener)
{
// _inDispatch:事件嵌套数
if (_inDispatch == 0)
{
// 无嵌套时调用
forceAddEventListener(listener);
}
else
{
// 存在嵌套时调用
_toAddedListeners.push_back(listener);
}
listener->retain();
}
无嵌套时的添加过程
@H_301_37@void EventDispatcher::forceAddEventListener(EventListener* listener) { EventListenerVector* listeners = nullptr; EventListener::ListenerID listenerID = listener->getListenerID(); auto itr = _listenerMap.find(listenerID); if (itr == _listenerMap.end()) { // 如果_listenerMap中没有当前订阅者类型 创建一个新的订阅者数组 listeners = @H_301_37@new (std::nothrow) EventListenerVector(); _listenerMap.emplace(listenerID,listeners); } else { listeners = itr->second; } // 将订阅者加入至当前类型的容器中 listeners->push_back(listener); // 订阅者与UI绑定 if (listener->getFixedPriority() == 0) { // 标记当前订阅者类型 该标记用于加速事件排序 setDirty(listenerID,DirtyFlag::SCENE_GRAPH_PRIORITY); auto node = listener->getAssociatedNode(); // 添加至nodeListenerMap,该map的key为node,value为所有关联到该node的订阅者 associateNodeAndEventListener(node,listener); if (node->isRunning()) { // 节点关联的所有订阅者:setPause(false)|setDirty() resumeEventListenersForTarget(node); } } else { setDirty(listenerID,DirtyFlag::FIXED_PRIORITY); } }
该过程主要做了两件事:
将传入的订阅者添加至两个容器中:_listenerMap 与 _nodeListenersMap。前者以ListenerID 为key,后者以node为key。使用两个容器,以空间开销换时间检索效率;
标记订阅器:1) 将当前类型的订阅器做标记;2) 将node关联到的所有订阅器做标记。为何要做标记?这是用于加速订阅器的排序。订阅器的优先级随时会发生变动,为了保证事件分发能够按照正确顺序进行,事件分发时必须首先进行订阅器的排序。但为了避免频繁且重复的排序导致的性能问题,在订阅器发生变动时打上标记。排序时仅操作标记过的订阅者。具体排序实现后续会介绍。
// 标记传入类型的订阅者 void EventDispatcher::setDirty(const EventListener::ListenerID& listenerID,DirtyFlag flag) { // 标记的类型存入 _priorityDirtyFlagMap 映射表 auto iter = _priorityDirtyFlagMap.find(listenerID); if (iter == _priorityDirtyFlagMap.end()) { // 映射表未找到 存入 _priorityDirtyFlagMap.emplace(listenerID,flag); } else { //映射表找到 基于位的或操作更新 int ret = (int)flag | (int)iter->second; iter->second = (DirtyFlag) ret; } } // 标记传入node关联到的所有订阅者 void EventDispatcher::setDirtyForNode(Node* node) { // Mark the node dirty only when there is an eventlistener associated with it. if (_nodeListenersMap.find(node) != _nodeListenersMap.end()) { _dirtyNodes.insert(node); } // Also set the dirty flag for node's children const auto& children = node->getChildren(); for (const auto& child : children) { setDirtyForNode(child); } }
- 事件嵌套时的添加过程
当事件嵌套时,传入的订阅者不会立即被立即添加至相关容器中,而是先放置在待处理容器_toAddedListeners中。这些待处理的订阅者将在当前分发过程结束时加入,具体实现在后文的事件分发中描述。
2.3 事件分发
Touch事件是所有类型中最常用也是最复杂的一种事件,下文将以Touch事件为例详细剖析事件分发的核心过程。
2.3.1 事件触发源
touch事件的触发源在GLView中发生。
// touch begin事件
void GLView::handleTouchesBegin(int num,float ys[])
{
touchEvent._eventCode = EventTouch::EventCode::BEGAN;
auto dispatcher = Director::getInstance()->getEventDispatcher();
dispatcher->dispatchEvent(&touchEvent);
}
// touch move事件
void GLView::handleTouchesMove(int num,float ys[],float fs[],float ms[])
{
touchEvent._eventCode = EventTouch::EventCode::MOVED;
auto dispatcher = Director::getInstance()->getEventDispatcher();
dispatcher->dispatchEvent(&touchEvent);
}
// end or cancel ...
2.3.2 分发过程
事件分发的入口为dispatchEvent
,这一函数包含了事件分发的主要过程。我们将逐步研究函数内部细节实现。
void EventDispatcher::dispatchEvent(Event* event)
{
if (!_isEnabled)
return;
updateDirtyFlagForSceneGraph();
DispatchGuard guard(_inDispatch);
if (event->getType() == Event::Type::TOUCH)
{
dispatchTouchEvent(static_cast<EventTouch*>(event));
return;
}
// ... 其他类型事件处理 略
}
updateDirtyFlagForSceneGraph
当关联的UI发生层级变化时,需要更新该UI节点对应的所有事件分发顺序。如在父控件上有A B两个子节点。起初A遮盖B,之后基于逻辑调整,B层级被调整并高过A,此时B事件响应等级也应当高于A。void EventDispatcher::updateDirtyFlagForSceneGraph() { if (!_dirtyNodes.empty()) { for (auto& node : _dirtyNodes) { auto iter = _nodeListenersMap.find(node); if (iter != _nodeListenersMap.end()) { for (auto& l : *iter->second) { // 标记node中所有订阅者 setDirty(l->getListenerID(),DirtyFlag::SCENE_GRAPH_PRIORITY); } } } _dirtyNodes.clear(); } }
_dirtyNodes存放了一堆需要更新其订阅者的节点。这些节点什么时候会被加入至_dirtyNodes中呢?1) 节点的某个订阅者发生变化,如向该节点加入一个订阅者;2)节点的层级发生变化,实现过程如下。
void Node::setLocalZOrder(int z) { if (getLocalZOrder() == z) return; // 设置父节点下的层次 _setLocalZOrder(z); if (_parent) { _parent->reorderChild(this,z); } _eventDispatcher->setDirtyForNode(this); }
DispatchGuard
在函数内部创建一个DispatchGuard,该变量被分配在栈上,创建时_inDispatch嵌套数量加1,当前事件分发函数结束时变量自动析构,嵌套数减1。class DispatchGuard { public: DispatchGuard(int& count):_count(count) { ++_count; } ~DispatchGuard() { --_count; } private: int& _count; };
dispatchTouchEvent
该函数包含了Touch触摸事件分发的全部过程,包括:订阅者排序、将事件处理函数分发至订阅者以及更新订阅者。void EventDispatcher::dispatchTouchEvent(EventTouch* event) { // 不同类型分开排序 sortEventListeners(EventListenerTouchOneByOne::LISTENER_ID); sortEventListeners(EventListenerTouchAllAtOnce::LISTENER_ID); auto oneByOneListeners = getListeners(EventListenerTouchOneByOne::LISTENER_ID); auto allAtOnceListeners = getListeners(EventListenerTouchAllAtOnce::LISTENER_ID); // If there aren't any touch listeners,return directly. if (nullptr == oneByOneListeners && nullptr == allAtOnceListeners) return; bool isNeedsMutableSet = (oneByOneListeners && allAtOnceListeners); const std::vector<Touch*>& originalTouches = event->getTouches(); std::vector<Touch*> mutableTouches(originalTouches.size()); std::copy(originalTouches.begin(),originalTouches.end(),mutableTouches.begin()); // process the target handlers 1st if (oneByOneListeners) { auto mutableTouchesIter = mutableTouches.begin(); for (auto& touches : originalTouches) { bool isSwallowed = false; auto onTouchEvent = [&](EventListener* l) -> bool { // Return true to break EventListenerTouchOneByOne* listener = static_cast<EventListenerTouchOneByOne*>(l); // Skip if the listener was removed. if (!listener->_isRegistered) return false; event->setCurrentTarget(listener->_node); bool isClaimed = false; std::vector<Touch*>::iterator removedIter; EventTouch::EventCode eventCode = event->getEventCode(); if (eventCode == EventTouch::EventCode::BEGAN) { if (listener->onTouchBegan) { isClaimed = listener->onTouchBegan(touches,event); if (isClaimed && listener->_isRegistered) { listener->_claimedTouches.push_back(touches); } } } else if (listener->_claimedTouches.size() > 0 && ((removedIter = std::find(listener->_claimedTouches.begin(),listener->_claimedTouches.end(),touches)) != listener->_claimedTouches.end())) { isClaimed = true; switch (eventCode) { case EventTouch::EventCode::MOVED: if (listener->onTouchMoved) { listener->onTouchMoved(touches,event); } break; case EventTouch::EventCode::ENDED: if (listener->onTouchEnded) { listener->onTouchEnded(touches,event); } if (listener->_isRegistered) { listener->_claimedTouches.erase(removedIter); } break; case EventTouch::EventCode::CANCELLED: if (listener->onTouchCancelled) { listener->onTouchCancelled(touches,event); } if (listener->_isRegistered) { listener->_claimedTouches.erase(removedIter); } break; default: CCASSERT(false,"The eventcode is invalid."); break; } } // If the event was stopped,return directly. if (event->isStopped()) { updateListeners(event); return true; } if (isClaimed && listener->_isRegistered && listener->_needSwallow) { if (isNeedsMutableSet) { mutableTouchesIter = mutableTouches.erase(mutableTouchesIter); isSwallowed = true; } return true; } return false; }; // dispatchTouchEventToListeners(oneByOneListeners,onTouchEvent); if (event->isStopped()) { return; } if (!isSwallowed) ++mutableTouchesIter; } } // ... allAtOnceListeners 处理部分 略 updateListeners(event); }
1)订阅者排序
EventListenerTouchOneByOne 与 EventListenerTouchAllAtOnce的相关逻辑是分开处理的,因此排序方面也是独立进行。排序前,首先判断当前订阅者类型是否被记录在标记映射表内,如未标记则不进行任何操作;之后基于标记类型判断订阅者需要进行何种类型(数值指定优先级类型 与 节点赋予的优先级类型)的排序。
@H_301_37@void EventDispatcher::sortEventListeners(const EventListener::ListenerID& listenerID) { DirtyFlag dirtyFlag = DirtyFlag::NONE; // 先检测当前类型订阅器是否需要排序 auto dirtyIter = _priorityDirtyFlagMap.find(listenerID); if (dirtyIter != _priorityDirtyFlagMap.end()) { dirtyFlag = dirtyIter->second; } // 仅当当前类型被标记时 再排序,这一优化能较大的提升排序效率 if (dirtyFlag != DirtyFlag::NONE) { // Clear the dirty flag first,if `rootNode` is nullptr,then set its dirty flag of scene graph priority dirtyIter->second = DirtyFlag::NONE; // 订阅者优先级通过数值指定且被标记 if ((int)dirtyFlag & (int)DirtyFlag::FIXED_PRIORITY) { sortEventListenersOfFixedPriority(listenerID); } // 订阅者优先级通过node指定且被标记 if ((int)dirtyFlag & (int)DirtyFlag::SCENE_GRAPH_PRIORITY) { auto rootNode = Director::getInstance()->getRunningScene(); if (rootNode) { sortEventListenersOfSceneGraPHPriority(listenerID,rootNode); } else { dirtyIter->second = DirtyFlag::SCENE_GRAPH_PRIORITY; } } } }
下面来看如何对Node指定优先级类型的订阅者排序过程进行分析。该过程首先将需要排序的订阅者筛选出来。可以发现,检索操作十分频繁,检索得事件复杂度至多为O(n),而排序的最优效率最高为O(nlog(n)),因此排序相较于检索要更加耗时。为保证排序高效完成,在排序前要采用响应手段尽量将无需参与排序的元素剔除;完成筛选后,更新_nodePriorityMap,该表内存储了所有节点对应的优先级,作为后续排序的凭证。为保证节点优先级的实时性与有效性,每次进行排序时都需要从根节点深度遍历一次所有UI;最后基于最新的优先级排序订阅者。
void ventDispatcher::sortEventListenersOfSceneGraPHPriority(const EventListener::ListenerID& listenerID,Node* rootNode) { auto listeners = getListeners(listenerID); if (listeners == nullptr) return; auto sceneGraphListeners = listeners->getSceneGraPHPriorityListeners(); if (sceneGraphListeners == nullptr) return; // Reset priority index _nodePriorityIndex = 0; _nodePriorityMap.clear(); visitTarget(rootNode,true); // After sort: priority < 0,> 0 std::stable_sort(sceneGraphListeners->begin(),sceneGraphListeners->end(),[this](const EventListener* l1,const EventListener* l2) { return _nodePriorityMap[l1->getAssociatedNode()] > _nodePriorityMap[l2->getAssociatedNode()]; } ); }
遍历一遍UI树并获得最新的层级表_nodePriorityMap。为什么要做层级表更新?主要有两个原因:1)保证节点局部层级的准确性:一个父节点下包含一些层级小于0的子节点与一些层级大于等于0的子节点,绘制的时候先绘制层级小于0的,之后是父节点,之后是层级大于等于0的;2)保证节点全局层级的准确性:cocos2dx 3.x版本之后可指定任意节点的全局层级,绘制时会优先绘制全局层级高的。如何满足第一点?很简单,首先对相当节点所有子节点排序以保证层次有序,之后采用中序遍历遍历。巧妙之处在于:中序遍历的结果与节点绘制顺序完全一致。如何满足第二点?借助于_globalZOrderNodeMap映射表容器,以全局层次为key,node为value。没有设置全局层次的默认为0,在中序遍历是按照访问顺序被依次加入容器中,设置了全局层次的,也同理。在最后的节点优先级统计阶段,首先将_globalZOrderNodeMap依照key排序,然后从小到大遍历,将每个关联了订阅器的节点优先级加1。
// 从根节点中序(深度)遍历UI树,更新_nodePriorityMap void EventDispatcher::visitTarget(Node* node,bool isRootNode) { // 排序子节点 node->sortAllChildren(); int i = 0; auto& children = node->getChildren(); auto childrenCount = children.size(); // 有子节点 向下遍历 if(childrenCount > 0) { Node* child = nullptr; // visit children zOrder < 0 for( ; i < childrenCount; i++ ) { child = children.at(i); if ( child && child->getLocalZOrder() < 0 ) visitTarget(child,false); else break; } // 判断该节点是否关联了订阅器 if (_nodeListenersMap.find(node) != _nodeListenersMap.end()) { _globalZOrderNodeMap[node->getGlobalZOrder()].push_back(node); } // visit children zOrder >= 0 for( ; i < childrenCount; i++ ) { child = children.at(i); if (child) visitTarget(child,false); } } else { if (_nodeListenersMap.find(node) != _nodeListenersMap.end()) { _globalZOrderNodeMap[node->getGlobalZOrder()].push_back(node); } } // 所有子节点均访问结束 if (isRootNode) { std::vector<float> globalZOrders; globalZOrders.reserve(_globalZOrderNodeMap.size()); for (const auto& e : _globalZOrderNodeMap) { globalZOrders.push_back(e.first); } std::stable_sort(globalZOrders.begin(),globalZOrders.end(),[](const float a,const float b){ return a < b; }); for (const auto& globalZ : globalZOrders) { for (const auto& n : _globalZOrderNodeMap[globalZ]) { // 依照遍历顺序 优先级+1 _nodePriorityMap[n] = ++_nodePriorityIndex; } } _globalZOrderNodeMap.clear(); } }