Cocos2d-x 3.2:定时器的使用和原理探究(2)
本文转载至深入了解Cocos2d-x 3.x:定时器的使用和原理探究(2)
上一篇说到定时器的使用方法,这篇主要分析它的实现原理。
1.哈希链表
Cocos2d-x封装了一个结构体,叫做UT_hash_handle,只要在自定义的结构体中声明这个结构体变量,就实现了哈希链表,并且能使用一系列的哈希链表专用的宏。这个结构体的具体实现如下:
1
2
3
4
5
6
7
8
9
10
|
typedef
struct
UT_hash_handle {
struct
UT_hash_table *tbl;
void
*prev;
/* prev element in app order */
void
*next;
/* next element in app order */
struct
UT_hash_handle *hh_prev;
/* previous hh in bucket order */
struct
UT_hash_handle *hh_next;
/* next hh in bucket order */
void
*key;
/* ptr to enclosing struct's key */
unsigned keylen;
/* enclosing struct's key len */
unsigned hashv;
/* result of hash-fcn(key) */
} UT_hash_handle;
|
这个结构体主要实现的是一个双向链表,具体实现哈希验证的还要看UT_hash_table 结构体:
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
|
typedef
struct
UT_hash_table {
UT_hash_bucket *buckets;
unsigned num_buckets, log2_num_buckets;
unsigned num_items;
struct
UT_hash_handle *tail;
/* tail hh in app order, for fast append */
ptrdiff_t
hho;
/* hash handle offset (byte pos of hash handle in element */
/* in an ideal situation (all buckets used equally), no bucket would have
* more than ceil(#items/#buckets) items. that's the ideal chain length. */
unsigned ideal_chain_maxlen;
/* nonideal_items is the number of items in the hash whose chain position
* exceeds the ideal chain maxlen. these items pay the penalty for an uneven
* hash distribution; reaching them in a chain traversal takes >ideal steps */
unsigned nonideal_items;
/* ineffective expands occur when a bucket doubling was performed, but
* afterward, more than half the items in the hash had nonideal chain
* positions. If this happens on two consecutive expansions we inhibit any
* further expansion, as it's not helping; this happens when the hash
* function isn't a good fit for the key domain. When expansion is inhibited
* the hash will still work, albeit no longer in constant time. */
unsigned ineff_expands, noexpand;
uint32_t signature;
/* used only to find hash tables in external analysis */
#ifdef HASH_BLOOM
uint32_t bloom_sig;
/* used only to test bloom exists in external analysis */
uint8_t *bloom_bv;
char
bloom_nbits;
#endif
} UT_hash_table;
|
然后看看与哈希链表相关的宏定义,使用这些宏能很方便的插入链表,删除链表,查找链表。
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
|
/**
* 查找元素
* head:哈希链表的头指针
* findptr:要查找的元素指针
* out:查找结果
*/
HASH_FIND_PTR(head,findptr,out)
/**
* 添加元素
* head:哈希链表的头指针
* ptrfield:要添加的元素指针
* add:要添加的哈希链表元素
*/
HASH_ADD_PTR(head,ptrfield,add)
/**
* 替换元素
* head:哈希链表的头指针
* ptrfield:要替换的元素指针
* add:要替换的哈希链表元素
*/
HASH_REPLACE_PTR(head,ptrfield,add)
/**
* 删除
* head:哈希链表的头指针
* delptr:要删除的元素指针
*/
HASH_DEL(head,delptr)
|
以上是引擎中实现的哈希链表的相关知识,接下来再看看与定时器相关的哈希链表。定时器的实现中,将一个定时器存储在哈希链表中,那么在scheduler是如何实现以后哈希链表的结构体的呢?如下:
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
|
// 不同优先级的update定时器的双向链表
typedef
struct
_listEntry
{
struct
_listEntry *prev, *next;
ccSchedulerFunc callback;
void
*target;
int
priority;
bool
paused;
bool
markedForDeletion;
// selector will no longer be called and entry will be removed at end of the next tick
} tListEntry;
//内置的update定时器
typedef
struct
_hashUpdateEntry
{
tListEntry **list;
// Which list does it belong to ?
tListEntry *entry;
// entry in the list
void
*target;
ccSchedulerFunc callback;
UT_hash_handle hh;
} tHashUpdateEntry;
// 自定义定时器
typedef
struct
_hashSelectorEntry
{
ccArray *timers;
void
*target;
int
timerIndex;
Timer *currentTimer;
bool
currentTimerSalvaged;
bool
paused;
UT_hash_handle hh;
} tHashTimerEntry;
|
以上就是相关的哈希链表的知识,接下来从定义定时器的函数Node::schedule中一步一步的分析定时器是如何加入到哈希链表中的。
2.如何定义自定义定时器
首先,上一篇文章中说到了很多个自定义定时器的函数,但是最终会调用的函数只有两个,分别是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/**
* 定义一个自定义的定时器
* selector:回调函数
* interval:重复间隔时间,重复执行间隔的时间,如果传入0,则表示每帧调用
* repeat:重复运行次数,如果传入CC_REPEAT_FOREVER则表示无限循环
* delay:延时秒数,延迟delay秒开始执行第一次回调
*/
void
schedule(SEL_SCHEDULE selector,
float
interval, unsigned
int
repeat,
float
delay);
/**
* 使用lambda函数定义一个自定义定时器
* callback:lambda函数
* interval:重复间隔时间,重复执行间隔的时间,如果传入0,则表示每帧调用
* repeat:重复运行次数,如果传入CC_REPEAT_FOREVER则表示无限循环
* delay:延时秒数,延迟delay秒开始执行第一次回调
* key:lambda函数的Key,用于取消定时器
* @lua NA
*/
void
schedule(
const
std::function<
void
(
float
)>& callback,
float
interval, unsigned
int
repeat,
float
delay,
const
std::string &key);
|
本文从传统的定义定时器的方法入手,也就是第一个方法。接下来看看这个方法的实现:
1
2
3
4
5
6
7
|
void
Node::schedule(SEL_SCHEDULE selector,
float
interval, unsigned
int
repeat,
float
delay)
{
CCASSERT( selector,
"Argument must be non-nil"
);
CCASSERT( interval >=0,
"Argument must be positive"
);
_scheduler->schedule(selector,
this
, interval , repeat, delay, !_running);
}
|
看到其实还是调用_scheduler的schedule方法,那么_scheduler又是个什么鬼?
1
|
Scheduler *_scheduler;
///< scheduler used to schedule timers and updates
|
查看定义可以知道是一个Scheduler 的指针,但是这个指针从哪里来?在构造函数中有真相
1
2
3
4
5
6
7
|
Node::Node(
void
)
{
// set default scheduler and actionManager
_director = Director::getInstance();
_scheduler = _director->getScheduler();
_scheduler->retain();
}
|
是从导演类中引用的。这一块暂时我们不管,接下来深入到_scheduler->schedule函数中分析,如下是函数的具体实现
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
|
void
Scheduler::schedule(SEL_SCHEDULE selector, Ref *target,
float
interval, unsigned
int
repeat,
float
delay,
bool
paused)
{
CCASSERT(target,
"Argument target must be non-nullptr"
);
//定义并且查找链表元素
tHashTimerEntry *element = nullptr;
HASH_FIND_PTR(_hashForTimers, &target, element);
//没找到
if
(! element)
{
//创建一个链表元素
element = (tHashTimerEntry *)
calloc
(
sizeof
(*element), 1);
element->target = target;
//添加到哈希链表中
HASH_ADD_PTR(_hashForTimers, target, element);
// Is this the 1st element ? Then set the pause level to all the selectors of this target
element->paused = paused;
}
else
{
CCASSERT(element->paused == paused,
""
);
}
//检查这个元素的定时器数组,如果数组为空 则new 10个数组出来备用
if
(element->timers == nullptr)
{
element->timers = ccArrayNew(10);
}
else
{
//循环查找定时器数组,看看是不是曾经定义过相同的定时器,如果定义过,则只需要修改定时器的间隔时间
for
(
int
i = 0; i < element->timers->num; ++i)
{
TimerTargetSelector *timer =
dynamic_cast
<TimerTargetSelector*>(element->timers->arr[i]);
if
(timer && selector == timer->getSelector())
{
CCLOG(
"CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %.4f to %.4f"
, timer->getInterval(), interval);
timer->setInterval(interval);
return
;
}
}
//扩展1个定时器数组
ccArrayEnsureExtraCapacity(element->timers, 1);
}
//创建一个定时器,并且将定时器加入到当前链表指针的定时器数组中
TimerTargetSelector *timer =
new
(std::
nothrow
) TimerTargetSelector();
timer->initWithSelector(
this
, selector, target, interval, repeat, delay);
ccArrayAppendObject(element->timers, timer);
timer->release();
}
|
这一段代码具体分析了如何将自定义定时器加入到链表中,并且在链表中的存储结构是怎么样的,接下来看看内置的Update定时器。
3.如何定义Update定时器
Update定时器的开启方法有两个,分别是:
1
2
3
4
5
6
7
8
9
|
/**
* 开启自带的update方法,这个方法会每帧执行一次,默认优先级为0,并且在所有自定义方法执行之前执行
*/
void
scheduleUpdate(
void
);
/**
* 开启自带的update方法,这个方法会每帧执行一次,设定的优先级越小,越优先执行
*/
void
scheduleUpdateWithPriority(
int
priority);
|
第一个方法实际上是直接调用第二个方法,并且把优先级设置为0,我们直接看第二个方法就可以了。
1
2
3
4
|
void
Node::scheduleUpdateWithPriority(
int
priority)
{
_scheduler->scheduleUpdate(
this
, priority, !_running);
}
|
具体调用还是要进入到_scheduler->scheduleUpdate。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/** Schedules the 'update' selector for a given target with a given priority.
The 'update' selector will be called every frame.
The lower the priority, the earlier it is called.
@since v3.0
@lua NA
*/
template
<
class
T>
void
scheduleUpdate(T *target,
int
priority,
bool
paused)
{
this
->schedulePerFrame([target](
float
dt){
target->update(dt);
}, target, priority, paused);
}
|
可以看到这里主要还是调用了一个schedulePerFrame函数,并且传入了一个lambda函数。这个函数实际上调用的是target->update,接下来走进schedulePerFrame看看它的实现:
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
|
void
Scheduler::schedulePerFrame(
const
ccSchedulerFunc& callback,
void
*target,
int
priority,
bool
paused)
{
//定义并且查找链表元素
tHashUpdateEntry *hashElement = nullptr;
HASH_FIND_PTR(_hashForUpdates, &target, hashElement);
//如果找到,就直接改优先级
if
(hashElement)
{
// 检查优先级是否改变
if
((*hashElement->list)->priority != priority)
{
//检查是否被锁定
if
(_updateHashLocked)
{
CCLOG(
"warning: you CANNOT change update priority in scheduled function"
);
hashElement->entry->markedForDeletion =
false
;
hashElement->entry->paused = paused;
return
;
}
else
{
// 在这里先停止到update,后面会加回来
unscheduleUpdate(target);
}
}
else
{
hashElement->entry->markedForDeletion =
false
;
hashElement->entry->paused = paused;
return
;
}
}
// 优先级为0,加入到_updates0List链表中,并且加入到_hashForUpdates表中
if
(priority == 0)
{
appendIn(&_updates0List, callback, target, paused);
}
// 优先级小于0,加入到_updatesNegList链表中,并且加入到_hashForUpdates表中
else
if
(priority < 0)
{
priorityIn(&_updatesNegList, callback, target, priority, paused);
}
// 优先级大于0,加入到_updatesPosList链表中,并且加入到_hashForUpdates表中
else
{
// priority > 0
priorityIn(&_updatesPosList, callback, target, priority, paused);
}
}
|
在这里看上去逻辑还是很清晰的,有两个函数要重点分析一下,分别是
1
2
|
void
Scheduler::appendIn(_listEntry **list,
const
ccSchedulerFunc& callback,
void
*target,
bool
paused)
void
Scheduler::priorityIn(tListEntry **list,
const
ccSchedulerFunc& callback,
void
*target,
int
priority,
bool
paused)
|
第一个用于添加默认优先级,第二个函数用于添加指定优先级的。首先看添加默认优先级的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
void
Scheduler::appendIn(_listEntry **list,
const
ccSchedulerFunc& callback,
void
*target,
bool
paused)
{
//创建一个链表元素
tListEntry *listElement =
new
tListEntry();
listElement->callback = callback;
listElement->target = target;
listElement->paused = paused;
listElement->priority = 0;
listElement->markedForDeletion =
false
;
//添加到双向链表中
DL_APPEND(*list, listElement);
//创建一个哈希链表元素
tHashUpdateEntry *hashElement = (tHashUpdateEntry *)
calloc
(
sizeof
(*hashElement), 1);
hashElement->target = target;
hashElement->list = list;
hashElement->entry = listElement;
//添加到哈希链表中
HASH_ADD_PTR(_hashForUpdates, target, hashElement);
}
|
接下来看另一个函数
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
|
void
Scheduler::priorityIn(tListEntry **list,
const
ccSchedulerFunc& callback,
void
*target,
int
priority,
bool
paused)
{
//同上一个函数
tListEntry *listElement =
new
tListEntry();
listElement->callback = callback;
listElement->target = target;
listElement->priority = priority;
listElement->paused = paused;
listElement->next = listElement->prev = nullptr;
listElement->markedForDeletion =
false
;
//如果链表为空
if
(! *list)
{
DL_APPEND(*list, listElement);
}
else
{
bool
added =
false
;
//保证链表有序
for
(tListEntry *element = *list; element; element = element->next)
{
// 如果优先级小于当前元素的优先级,就在这个元素前面插入
if
(priority < element->priority)
{
if
(element == *list)
{
DL_PREPEND(*list, listElement);
}
else
{
listElement->next = element;
listElement->prev = element->prev;
element->prev->next = listElement;
element->prev = listElement;
}
added =
true
;
break
;
}
}
//如果新加入的优先级最低,则加入到链表的最后
if
(! added)
{
DL_APPEND(*list, listElement);
}
}
//同上一个函数
tHashUpdateEntry *hashElement = (tHashUpdateEntry *)
calloc
(
sizeof
(*hashElement), 1);
hashElement->target = target;
hashElement->list = list;
hashElement->entry = listElement;
HASH_ADD_PTR(_hashForUpdates, target, hashElement);
}
|
本文简单的分析了哈希链表以及定时器的存储和添加,下一篇文章将分析定时器是如何运转起来的。