介绍:
热更新,就是在维持服务不间断的情况下,对软件代码逻辑或配置数据进行更新修复。随着游戏项目引入了脚本语言以后,热更新技术逐渐成为了标配,在我经历过的游戏项目中,无论是服务端还是客户端,版本的更新迭代都是围绕着静态patch和动态patch(热更新)来进行的。下面来谈一下客户端Python热更新的处理。
原理:
- 标准import
我们知道,import可以导入一个标准的python模块,将模块载入内存,并加到sys.modules中。多次import同一模块只是将名称导入到当前的Local名字空间,也就是一个模块不会重复载入。 - reload函数
reload()函数可以重新载入已经导入的模块,这样看起来就可以热更新python模块了。可惜的是,python原生的reload函数远不能满足游戏热更新的问题,原因如下:
reload重新加载的模块不会删除旧版本的模块,也就是已经引用的旧模块无法更新
同样因为不能旧对象的引用,使用from ... import ... 方式引用的模块同样不能更新
reloas(m)后,class及其派生class的实例对象,仍然使用旧的class定义。
加载模块失败时候,没有rollback机制,需要重新import该模块
因此,有必要结合游戏的情景,自定义适合的reload。新的自定义reload目的是为了达到在原程序不结束的情况下,让程序能动态加载改动后的代码。主要想达到下面两点:
- 提升开发期的开发效率
- 在游戏不重启的情况下修复紧急BUG
实现:
热更新当中实际的重点在于如何让已经创建的对象获得新代码的变化,以及在reload前后不产生类型上的不一致。刷新function,class内定义的method比较容易实现,但对于刷新module内定义的变量,class内定义的变量,还有新增加的成员变量,则需要有统一的约定。所以,在热更新过程中,我们只要考虑好代码更新和数据更新这两点,那么更新就是可行的。
下面罗列一下新的reload具备哪些特性:
- 更新代码定义(function/method/static_method/class_method)
- 不更新数据(除了代码定义外的类型都当作是数据)
- 在module中约定reload_module接口,class中约定reload_class接口,在这两个接口中手动处理数据的更新,还有更多的约定和接口待完成
替换函数对象的内容
# 用新的函数对象内容更新旧的函数对象中的内容,保持函数对象本身地址不变
def update_function(oldobj, newobj, depth=0):
setattr(oldobj, "func_code", newobj.func_code)
setattr(oldobj, "func_defaults", newobj.func_defaults)
setattr(oldobj, "func_doc", newobj.func_doc)
替换类的内容
# 用新类内容更新旧类内容,保持旧类本身地址不变
def _update_new_style_class(oldobj, newobj, depth):
handlers = get_valid_handlers()
for k, v in newobj.__dict__.iteritems():
# 如果新的key不在旧的class中,添加之
if k not in oldobj.__dict__:
setattr(oldobj, k, v)
_log("[A] %s : %s"%(k, _S(v)), depth)
continue
oldv = oldobj.__dict__[k]
# 如果key对象类型在新旧class间不同,那留用旧class的对象
if type(oldv) != type(v):
_log("[RD] %s : %s"%(k, _S(oldv)), depth)
continue
# 更新当前支持更新的对象
v_type = type(v)
handler = handlers.get(v_type)
if handler:
_log("[U] %s : %s"%(k, _S(v)), depth)
handler(oldv, v, depth + 1)
# 由于是直接改oldv的内容,所以不用再setattr了。
else:
_log("[RC] %s : %s : %s"%(k, type(oldv), _S(oldv)), depth)
# 调用约定的reload_class接口,处理类变量的替换逻辑
object_list = gc.get_referrers(oldobj)
for obj in object_list:
# 只有类型相同的才是类的实例对象
if obj.__class__.__name__ != oldobj.__name__:
continue
if hasattr(obj, "x_reload_class"):
obj.x_reload_class()
staticmethod
def _update_staticmethod(oldobj, newobj, depth):
# 一个staticmethod对象,它的 sm.__get__(object)便是那个function对象
oldfunc = oldobj.__get__(object)
newfunc = newobj.__get__(object)
update_function(oldfunc, newfunc, depth)
classmethod
def _update_classmethod(oldobj, newobj, depth):
oldfunc = oldobj.__get__(object).im_func
newfunc = newobj.__get__(object).im_func
update_function(oldfunc, newfunc, depth)
模块的更新也是相类似,就不一一粘贴了,只是在原来的reload基础上进行改良,对于模块热更新,还约定了一个reload_module接口,可以自定义数据的更新。
下面添加一些用例:
def x_reload_class(self):
""" 热更新后,每个重新对象的实例都会执行这个函数
由于新老对象的替换不会重新调用构造函数,因此有必要对热更新的类对象执行初始化逻辑
处理新老变量的修复,函数执行环境的修复
"""
self._new_var = 5000 # 新变量的初始化
self.init_widget() # 新修复的逻辑
目前的热更新模块已经在开发调试中使用,可以方便地完成一些更新任务,但是要更新到远程客户端,还需要更多的规范和接口来处理,如闭包,内部局部变量等,需要逐步地学习和完善。