cocos2d-x 3.x Scheduler解析

撰写于 2018-05-17 修改于 2018-05-23 分类 游戏开发 标签 cocos2d-x

初见

Scheduler顾名思义就是执行计划,在cocos2d-x中用来处理计划任务,比如:定时任务,重复任务等,是一个很有有用的类。初次结识Scheduler,只知道是怎么使用,使用时不会阻塞主线程,还以为是启动了一个线程(好幼稚的想法),最近仔细看了一下代码,原来只是在计时上做了点文章而已。

初始化

同ActionManager一样,Scheduler类中虽然没有做单例限制,但是只能通过Director获取,Director是单例,所以Scheduler也算是”单例”。同样在Director的init函数中初始化。

1
2
3
4
5
6
7
8
9
10
/*初始化*/
bool Director::init(void)
{
/*...*/
/* scheduler*/
_scheduler = new (std::nothrow) Scheduler();
/*...*/
}
/*获取*/
Scheduler* getScheduler() const { return _scheduler; }

运行

那Scheduler是怎么处理定时任务呢?其实这个类和ActionManager的思想是一样的,同样是为维护一个uthash类型的数据,里面的存储的数据是自定义的数据结构:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _hashSelectorEntry
{
ccArray *timers;
void *target;
int timerIndex;
Timer *currentTimer;
bool currentTimerSalvaged;
bool paused;
/* uthash 中必须要定义 具体可以自行查阅*/
UT_hash_handle hh;
} tHashTimerEntry;

处理定时任务的核心的方法是:schedule(params),该方法只是定义并初始化了一个TimerTargetCallback,加入对应target的times数组中,具体的执行并不在此处处理。TimerTargetCallback负责计时、执行、取消任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void Scheduler::schedule(const ccSchedulerFunc& callback, void *target, float interval, unsigned int repeat, float delay, bool paused, const std::string& key)
{
/**target,key不能为空**/
CCASSERT(target, "Argument target must be non-nullptr");
CCASSERT(!key.empty(), "key should not be empty!");
/** 先去_hashForTimers中查找是否有对应target的tHashTimerEntry数据,把找出的数据放入element中 **/
tHashTimerEntry *element = nullptr;
HASH_FIND_PTR(_hashForTimers, &target, element);
/**如果没有,则新建一个tHashTimerEntry类型的数据,放入_hashForTimers中**/
if (! element)
{
element = (tHashTimerEntry *)calloc(sizeof(*element), 1);
element->target = target;

HASH_ADD_PTR(_hashForTimers, target, element);
element->paused = paused;
}
else
{
CCASSERT(element->paused == paused, "element's paused should be paused!");
}
/** 初始化数据中timers,里面存放的是 TimerTargetCallback 类的对象,也就是说一个target可以同时存在很多计时任务**/
if (element->timers == nullptr)
{
element->timers = ccArrayNew(10);
}
else
{
/** 如果已经有了该计时任务,则返回 **/
for (int i = 0; i < element->timers->num; ++i)
{
TimerTargetCallback *timer = dynamic_cast<TimerTargetCallback*>(element->timers->arr[i]);

if (timer && key == timer->getKey())
{
CCLOG("CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %.4f to %.4f", timer->getInterval(), interval);
timer->setInterval(interval);
return;
}
}
/**确保timers有足够的空间存放**/
ccArrayEnsureExtraCapacity(element->timers, 1);
}
/**新建并初始化计时任务,放入element->timers中**/
TimerTargetCallback *timer = new (std::nothrow) TimerTargetCallback();
timer->initWithCallback(this, callback, target, key, interval, repeat, delay);
ccArrayAppendObject(element->timers, timer);
timer->release();
}

在 schedule(params) 中并没有看到任务的执行,甚至没有看到计时的出现,因为这一操作都放在了 Scheduler::update(float dt) 函数中,update函数在 Director::drawScene() 被调用,而且 drawScene() 会在每一帧进行调用,也就是每帧都会调用 Scheduler::update(float dt) 函数。该函数的逻辑也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
void Scheduler::update(float dt)
{
_updateHashLocked = true;

if (_timeScale != 1.0f)
{
dt *= _timeScale;
}

tListEntry *entry, *tmp;

/** 遍历 _updatesNegList、_updates0List、_updatesPosList,里面存在的是帧任务,也就是每一帧都执行 **/
/** _updatesNegList存在的是 priority < 0 的任务,里面是按 priority 从大到小排的**/
DL_FOREACH_SAFE(_updatesNegList, entry, tmp)
{
/** 任务没有停止,且没有被标记删除 **/
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}
/** _updates0List存在的是 priority = 0 的任务 **/
DL_FOREACH_SAFE(_updates0List, entry, tmp)
{
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}

/** _updatesPosList存在的是 priority > 0 的任务, 里面是按 priority 从大到小排的 **/
DL_FOREACH_SAFE(_updatesPosList, entry, tmp)
{
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}

/**以下是处理计时任务**/
for (tHashTimerEntry *elt = _hashForTimers; elt != nullptr; )
{
_currentTarget = elt;
_currentTargetSalvaged = false;

if (! _currentTarget->paused)
{
/** 相当于遍历每个target的timers,每个target可以对应很多计时任务,timers中存放的就是这些计时任务 **/
for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex))
{
elt->currentTimer = (Timer*)(elt->timers->arr[elt->timerIndex]);
elt->currentTimerSalvaged = false;
/** 更新每个任务的时间,如果时间满足了计时任务中设定的interval,则执行callback或者selector **/
elt->currentTimer->update(dt);
/** 如果这个任务已经结束了,即调用了unschedule,则release **/
if (elt->currentTimerSalvaged)
{
elt->currentTimer->release();
}

elt->currentTimer = nullptr;
}
}

elt = (tHashTimerEntry *)elt->hh.next;

if (_currentTargetSalvaged && _currentTarget->timers->num == 0)
{
removeHashElement(_currentTarget);
}
}

/** 删除掉所有的被标记回收的帧任务 **/
DL_FOREACH_SAFE(_updatesNegList, entry, tmp)
{
if (entry->markedForDeletion)
{
this->removeUpdateFromHash(entry);
}
}

DL_FOREACH_SAFE(_updates0List, entry, tmp)
{
if (entry->markedForDeletion)
{
this->removeUpdateFromHash(entry);
}
}

DL_FOREACH_SAFE(_updatesPosList, entry, tmp)
{
if (entry->markedForDeletion)
{
this->removeUpdateFromHash(entry);
}
}

_updateHashLocked = false;
_currentTarget = nullptr;

#if CC_ENABLE_SCRIPT_BINDING
/** 上面是C++的计时任务处理,下面是脚本计时任务的处理**/
if (!_scriptHandlerEntries.empty())
{
for (auto i = _scriptHandlerEntries.size() - 1; i >= 0; i--)
{
SchedulerScriptHandlerEntry* eachEntry = _scriptHandlerEntries.at(i);
if (eachEntry->isMarkedForDeletion())
{
_scriptHandlerEntries.erase(i);
}
else if (!eachEntry->isPaused())
{
eachEntry->getTimer()->update(dt);
}
}
}
#endif
/** 执行线程函数 每帧都要执行 cocos里面的并没有使用太多的多线程,AudioCache,HttpClient用了多线程**/
if( !_functionsToPerform.empty() ) {
_performMutex.lock();
/** 加mutex线程锁,保证原子操作 **/
auto temp = _functionsToPerform;
_functionsToPerform.clear();
_performMutex.unlock();
for( const auto &function : temp ) {
function();
}

}
}

上面的帧函数中并没有看到计时和回调函数的处理,只是调用了 elt->currentTimer->update(dt),其实是放在了Timer中的update函数中执行,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void Timer::update(float dt)
{
if (_elapsed == -1)
{
_elapsed = 0;
_timesExecuted = 0;
return;
}

/** 计算出经过的时间 **/
_elapsed += dt;

/** 如果是延迟执行,即延迟一段时间后,才开始计时任务 **/
if (_useDelay)
{
if (_elapsed < _delay)
{
return;
}
/** 如果经过的时间已经满足了延迟时间,则执行回调 **/
trigger(_delay);
_elapsed = _elapsed - _delay;
_timesExecuted += 1;
_useDelay = false;
/** 如果是永久运行,且执行次数已经大于设定的重复次数,则停止计时任务 **/
if (!_runForever && _timesExecuted > _repeat)
{
cancel();
return;
}
}

/** if _interval == 0, should trigger once every frame **/
float interval = (_interval > 0) ? _interval : _elapsed;
while (_elapsed >= interval)
{
trigger(interval);
_elapsed -= interval;
_timesExecuted += 1;

if (!_runForever && _timesExecuted > _repeat)
{
cancel();
break;
}

if (_elapsed <= 0.f)
{
break;
}
}
}

Site by ZHJ using Hexo & Random

Hide