从一个Bug说起:
在内部试玩时发现有个任务的玩家跟随Npc逻辑挂了.
telnet连接到出问题的设备上, 开始搞事情
这个跟随的逻辑是一个Timer驱动的. 这个Timer在主角创建时就会启动. 一开始不认为是Timer本身的问题. 怀疑是流程上各个地方有return的情况
动态改掉几个函数都没有执行到. 说明Timer的回调没有调用到. 怀疑到Timer本身来
输出了下Timer的状态, 发现是running = true. 感觉很诧异. 又看了下Timer里的其他字段发现有个handle. 输出来看了下, 发现有个removed = true.
这里就奇怪了, 说明被异常removed了. 于是开始看Timer的实现:
Timer.lua 创建一个Timer的流程如下:
1. Timer.New()
创建元表, 基础字段初始化. running = false...
2. Timer.Start()
1). 从UpdateBeat创建一个Handle
通过调用UpdateBeat:CreateListener, 传入Timer.Update函数和Timer本身得到返回值Handle, 其作用是一个链表的Node.
代码: self.handle = UpdateBeat:CreateListener(self.Update, self)
2). 注册到UpdateBeat里:
即把handler作为链表的node放到UpdateBeat的列表里.
代码: UpdateBeat:AddListener(self.handle)
3). 此时Timer类的初始化过程完成了. 后续就等着Timer.Update被调用了. 可以看到主要的内容其实是在UpdateBeat里.
4). 要了解Handle的管理和Timer.Update的调用方式就要看UpdateBeat代码了
5). UpdateBeat是在event.lua里定义的
可以看到是一个_event的元表
6) 关注下_event的内容:
#1 _event.CreateListener(func, obj)
即上面 1)里调用的函数, 这里的func就是Timer.Update, obj就是Timer自身.
这里判断了self.keepSafe, 即5)里 定义UpdateBeat的第二个参数true. 所以这里的self.keepSafe = true.
可以看到keepSafe的情况下, func和obj被包到了一个xfunctor里, 这个xfunctor看了下, 是一个xpcall的封装. 也就是keepSafe的event实际调用func时会用xpcall的方式执行. 出错的话会返回抛错信息.
然后是其他的几个字段. 包括缓存了封装了Timer.Update的xpcall函数的value字段. 实现双向链表需要的两个指针(_prev, _next), 和我们要查bug需要关注的removed = true . 可以看到在Timer.Start申请Handle之初, 这个removed就是true状态的.
#2_event.AddListener
创建了Handle之后, 通过AddListener接口注册到链表里. 可以看到, 这里判断了self.lock 这个字段的作用是锁self.list. 即避免在操作list的时候, 有其他逻辑来改这个list.
这个lock字段是在执行一轮Update时置为true. 执行完成一轮后改成false. 在lock为true的过程中, 如果有其他AddListener的行为, 都会把Add行为缓存到一个临时列表: self.opList里. 而不是self.list
这个列表缓存了Add操作. 即function() self.list:pushnode(handle) end. 然后等执行一轮Update之后, 把缓存的Add的操作都执行一遍. (RemoveListener同理)
#3 _event.__call
这个函数是在对_event执行调用操作时执行的. 如下图 UpdateBeat()
__call函数首先上锁 lock = true. 然后用迭代器生成器 ilist 遍历 链表list. 这个链表就是我们AddListener操作存放Timer的双向链表. (ilist详细见下方解释)
每次迭代的 i 即为一个handle, f是xpcall包装的Timer.Update函数. 然后调用f函数, 返回xpcall的结果 成功标识: flag, 错误信息 msg. 如果出错, 则用error抛出LuaException. 并且调用_list.remove,从链表移除该Node, 并且解锁: lock = false
(这里有个问题, 如果其中一个Timer出错了, 然后解锁了, 后面有Timer真的塞了Timer进来, 岂不是要炸?? 求解释. )
如果一切正常情况下, 会在最后把cache的Add/RemoveListener的操作执行一遍.
ilist迭代器可以在list.lua中看到, 根据Lua的for语句的性质. 迭代器生成器返回三个值: 迭代函数(list.next), 恒定状态(即_list), 控制变量(没用). 然后以恒定状态和控制变量为参数去调用迭代函数, 得到的返回值再赋值给 for循环的key, value (<var-list>)
所以迭代函数为list.next. 这个函数只需要一个恒定状态(第一次是list, 其实就是头节点, 后面是每个node)即可了, 因为直接可以通过next访问到下一个. 当下一个就是头结点时, 循环结束.
for循环定义:
for <var-list> in <exp-list> do
list.lua: