cocos2dx-lua下热更新不重启的一种实现可能

cocos2dx-lua下热更新不重启的一种实现可能

上一次利用元表实现了变量改变追溯后,笔者对于元表的使用更加熟练了一些,今天突发奇想,想到能否利用元表来实现热更新不重启,便尝试了一番,结果是有可能,但是限制、麻烦可能更多,目前笔者觉得该方案只适用与代码编写环境,方便进行调试。

概要

既然要写文章记录,那就先简单提一下实现的方案吧。

cocos2dx-lua的面向对象是利用元表来实现的,当一个对象被创建,也就是被new出来的过程是这样的:创建一个空表(或者利用传入的function创建一个表)然后将class设为这个表的原表,如果class是继承自其他类的,那么将其他类设为class的元表。总结一下就是:__多个对象的元表都是同一个类,而类的父类也就是这个类的元表,父类的父类也是这样的一个关系__。
以下是cocos2dx-lua框架functions文件class函数的部分代码:

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
--...
cls.__index = cls
if not cls.__supers or #cls.__supers == 1 then
setmetatable(cls, {__index = cls.super})
else
setmetatable(cls, {__index = function(_, key)
local supers = cls.__supers
for i = 1, #supers do
local super = supers[i]
if super[key] then return super[key] end
end
end})
end
--...
cls.new = function(...)
local instance
if cls.__create then
instance = cls.__create(...)
else
instance = {}
end
setmetatableindex(instance, cls)
instance.class = cls
instance:ctor(...)
return instance
end
...

而元表的__index元方法又可以实现:当我们访问表中的一个变量时,如果有这个变量,直接返回,如果没有,从__index中获取这个变量。结合上述原理,cocos2dx-lua的面向对象就是:__一堆table互相设置元表,最开头的表就是对象,其他都是类,对象中存储运行时设置的变量,类中存储方法与静态变量__。

再来讨论热更新不重启的需求:我们需要在不丢失运行时的情况下,改变执行的代码(方法)。以上能知道,其实lua中运行时的环境是存储在原型链的最开始端,也就是对象 (这里没有考虑global环境与local环境),而我们需要改变的代码是原型链上的也就是说只需要改变对象的元表,就能实现执行方法、静态变量的改变。

实现

要实现上述的功能,我们需要考虑的是:当需要改变一个类时,我如何获取他的对象实现?因为元表的关系其实是一个单向链表,也是多对一的关系,通过对象我们能获取到他的类,但是通过类,我们并不能直接获取他的对象。为解决这个问题,我们需要修改functions.lua这个文件中的class方法,用来记录,保存对象实例。

下面是笔者修改后的代码:

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
-------------------
--@author #ccffee
-------------------

__objects = {} ---新建一个表用来存放对象

function class(classname, ...)
local cls = {__cname = classname}

local supers = {...}
for _, super in ipairs(supers) do
local superType = type(super)
assert(superType == "nil" or superType == "table" or superType == "function",
string.format("class() - create class \"%s\" with invalid super class type \"%s\"",
classname, superType))

if superType == "function" then
assert(cls.__create == nil,
string.format("class() - create class \"%s\" with more than one creating function",
classname));
cls.__create = super
elseif superType == "table" then
if super[".isclass"] then
assert(cls.__create == nil,
string.format("class() - create class \"%s\" with more than one creating function or native class",
classname));
cls.__create = function() return super:create() end
else
cls.__supers = cls.__supers or {}
cls.__supers[#cls.__supers + 1] = super
if not cls.super then
cls.super = super
end
end
else
error(string.format("class() - create class \"%s\" with invalid super type",
classname), 0)
end
end

cls.__index = cls
if not cls.__supers or #cls.__supers == 1 then
setmetatable(cls, {__index = cls.super})
else
setmetatable(cls, {__index = function(_, key)
local supers = cls.__supers
for i = 1, #supers do
local super = supers[i]
if super[key] then return super[key] end
end
end})
end

if not cls.ctor then
cls.ctor = function() end
end
---当创建一个类时,在__objects表中开辟一块空间存放对应的实例,此处需要注意的是,如果我们直接赋值为{},当我们用class方法创建一个名称相同的类时,会覆盖原来的表
__objects[cls.__cname] = __objects[cls.__cname] or {}
cls.new = function(...)
local instance
if cls.__create then
instance = cls.__create(...)
else
instance = {}
end
setmetatableindex(instance, cls)
instance.class = cls
instance:ctor(...)
local clsContent = __objects[cls.__cname] ---在__objects中获取对应类的空间
clsContent[#clsContent + 1] = instance ---存放对象
return instance
end
cls.create = function(_, ...)
return cls.new(...)
end

return cls
end

上述代码有注释的地方就是笔者做修改的地方。笔者创建了一个全局变量用于存放对应类的实例,是一些很简单的操作。

上面的步骤完成后,我们需要一个方法用来更新对应的类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
local function reloadClass(path)
package.loaded[path] = nil
local targetCls = require(path)
local objects = __objects[targetCls.__cname]
table.walk(objects or {}, function(object, index)
---移花接木
---清空原型链
setmetatable(object, {})
---重新设置原型链
setmetatableindex(object, targetCls)
print("代码:<" .. path .. "> 已经重载")
end)
end

这个函数需要传入一个path也就是要重载的文件类的路径。函数首先将package.loaded表中对应的值清空,防止后续require相同路径失败(笔者的用途是在单元测试中重载代码,代码的文件从始至终只有一个,所以会有这个操作),下面require了对应的path获取对应类,然后从__objects表中获取到了对应的对象实例表,后面遍历该表,将原先对象的元表替换为require获取到的类,这样就实现了新方法的转变。

缺点

本文的方法只是提供一个方向,因为还有很多不便的地方,以下是笔者已知的问题:

  1. 对象没有删除,笔者猜测(没有搜索过相关资料),lua虚拟机垃圾回收的方式是,一个变量没有被引用的话,就将这个变量作为垃圾处理,而上文我们将对象都存储在了__objects表中,所以垃圾回收不生效,运行久了可能就会卡了。

  2. 对象的构造函数不能被重载,构造函数一般会初始化一些变量,而上述重载的过程不涉及旧对象new的过程,所以如果构造函数有做修改,本文方案是不适用的。

  3. 事件注册会失效,cocos2dx-lua自带的addEventListenr方法存储的是函数变量的地址,所以当我们重载了一个对象,但是有其他对象利用addEventListenr注册了该对象的一个旧方法时,重载就会失效。但是该问题应该比较容易解决,利用getfenv方法或者其他的操作判断function对应的对象应该能够解决该问题,这里不过多阐述。


cocos2dx-lua下热更新不重启的一种实现可能
http://ccffee.fun/2022/07/22/cocos2dx-lua下热更新不重启的一种实现可能/
作者
ccffee
发布于
2022年7月22日
许可协议