基础需求描述
数据热更,是手游开发过程中常面临的一个需求。
在配置表字段不变的情况下, 数据的变动是无需程序关注的。但是当字段发生变动时,就是程序需要处理的数据热更的情形。
下面我们来设计一张UI表,并在后续的例子中也会用到它。
UI表的字段名 | ID | UI显示名称 | UI类型 | 开启音效 |
---|---|---|---|---|
原始字段 | * | * | * | |
新增字段 | * |
策划新需求,需要打开UI时播放一个音效,所以就有了新增的开启音效字段。
现在,让我们基于ILRuntime,来看看几种数据热更方案。这些也是我这些年所经理的项目中常见的方案。基于这些方案以及我现在的开发需求,我也设计了全新的数据热更方案,用于解决之前方案所面临的问题。
原有项目方案
数据全放热更工程
这种方案最为简单粗暴。
基于项目全放热更的设计或对应模块全在热更工程中的设计,将数据结构以及序列化后的数据实例全都在ILRuntime的热更工程里。
比如UI模块全放在了热更工程中,那么它用到的UI表也就放在了热更工程里。这样,当UI表发生结构变动时,也仅仅相当于代码热更,确实开发起来更简单粗暴有效。
(那么,葛二蛋,代价是什么?)
这样的实现终究有其需要付出的代价。ILRuntime中实现的类在实例化时,会比C#天然实现的类占用更多的内存(可以简单理解为ILRuntime中每个字段都有一些额外的数据存储)。因为这样的设计将热更数据也完全放在了热更工程里,所以导致了内存占用居高不下。
我之前所经历的一个项目在采用这个方案后,优化热更工程的内存占用一直是我们的重点任务。尤其当大部分数据都放在了热更里时,这个问题会愈发严重,毕竟动辄几十张数据表,成百万计的数据量,内存消耗还是很恐怖的。
数据表有更新就全放热更工程
这种方案其实是针对全热更方案的问题而产生的一个变种。
项目开发阶段,所有数据都在主工程内,使用原生的C#进行实例的创建、管理。一旦上线后有热更需求,就在热更工程内存放一份新的数据表结构,使用命名空间与原有表结构进行区分。数据序列化时除了主工程内的那一份,热更里额外序列化一份数据存储在热更工程内。
以UI表举例,主工程里的结构是这样的:
namespace Project
{
public class TableUI
{
public int Id;
public string Name;
public int Type;
}
}
一旦出现热更需求,就会在热更工程里放一份全新的结构:
namespace Project.Hotfix
{
public class TableUI
{
public int Id;
public string Name;
public int Type;
public string Audio;
}
}
理论上讲,这个方案在改表结构的热更需求比较少的情况下还是不错的。可终究同一个数据同时存在两份是对内存极大地不尊重。
此外,我们程序在开发项目时,常规情况下会用到Project.TableUI
,一旦这个表出现热更需求就需要变更为使用Project.Hotfix.TableUI
。可是我们终究还得开发下一个大版本,为了性能我们终究会把上一个版本的新增字段放回到主工程内,这时候就不得不面临维护两套代码了:一套使用Project.TableUI
,一套使用Project.Hotfix.TableUI
,这么做对开发人员很不友好。
所以,这套方案虽然比第一套在内存上有些许改善,但我仍认为这套方案极不合理。
增量更新方案
这套方案是我对现在的项目新设计的方案,主要就是吸取前两个方案的经验教训,既能实现数据热更,还需要内存友好,更重要的是对开发人员要尽量无感。
设计思路
下面以UI表的热更需求,给出我现在方案所涉及的类图与代码:
增量热更方案下TableUI相关类图主工程内的代码:
namespace Project
{
public class TableUI
{
public int Id;
public string Name;
public int Type;
public IHotfixData HotfixData;
public void Deserialize(Stream stream)
{
...
HotfixData?.Deserialize(stream);
}
}
public interface IHotfixData
{
void Deserialize(Stream stream);
}
}
热更工程内的代码:
namespace Project.Hotfix
{
public class TableUIHotfixData : IHotfixData
{
public string Audio;
public void Deserialize(Stream stream) { ... }
}
}
这套方案的核心设计思路,就是数据本身还是以存储在主工程为主。
面临热更需求,则在热更工程内实现该类对应的热更数据类,继承IHotfixData
接口,将新增的字段放到里面。之后将该类实例化后绑定到TableUI
表的HotfixData
字段。当对实例进行数据的序列化时,通过调用各自的Deserialize
实现。
这样做的好处,对比之前的方案,就是只让热更的数据放在热更工程内,尽可能减少其内存占用。
开发友好
下一个问题,如何做到开发友好?
对于逻辑开发程序员,我当然希望在获取到一个TableUI
的实例后,直接通过.
就能拿到我所需要的的数据。而现在的矛盾点,则集中在命名空间以及数据来源这两部分,所以我们需要利用设计和C#提供的语法来解决这个困难。
首先我们明确一个新的设计:所有的字段都配有一个Get方法。
namespace Project
{
public class TableUI
{
public int Id;
public string Name;
public int Type;
public int GetId() => Id;
public string GetName() => Name;
public int GetType() => Type;
public IHotfixData HotfixData;
}
}
这样的设计,无论TableUI.Id
亦或是TableUI.GetId()
都可以获得我想要的数据。
然后,在此基础上,我们在热更工程里再利用一下C#的语言特性:
namespace Project.Hotfix
{
public static class TableUIHotfixDataUtil
{
public static string GetAudio(this TableUI table)
{
return ((TableUIHotfixData)table.HotfixData).Audio;
}
}
}
这样,在热更代码里,我可以使用TableUI.GetAudio()
来获取我希望得到的新增的数据字段的内容。
最后,就是见证开发友好的时刻了。
当开发下一版的主工程时,之前的热更数据也已经整合进了主工程。那么主工程里的数据代码就变成了以下这样:
namespace Project
{
public class TableUI
{
public int Id;
public string Name;
public int Type;
public string Audio;
public int GetId() => Id;
public string GetName() => Name;
public int GetType() => Type;
public string GetAudio() => Audio;
public IHotfixData HotfixData;
}
}
而之前调用TableUI.GetAudio()
的代码无需任何修改。
对于逻辑开发程序员来说,底层不是他们关注的重点,他们只需要知道有一个Get方法可以获得我所需要的数据就可以了。
面临的问题
其实这套方案实现之后,也许是限于我们现有的数据系列化与反序列化的方案,也是有一些问题存在的。
现有问题
我们现在的数据是按照二进制进行序列化与反序列化的,这就导致数据文件可以做到比较小、序列化反序列化速度比较快,但是数据的反序列化必须按照序列化时的顺序进行,容错率其实很低。
也正是这一设计,导致我们现在需要对策划配表进行管控,在开发热更版本时:
- 主工程内的字段不可删除
- 主工程内的字段顺序不可变更
- 新增字段只能放在现有字段之后
- 主工程内的字段的字段名、字段类型不可变更
这些都是项目开发时的隐藏成本,即便我们可以通过工具实现自动化生成管理,也需要面对策划的各种行为进行跟踪监视,以避免他们犯错。
其实还有一些别的方法可以尽量减少这种掣肘,只是需要多付出一些性能的代价。
可以尝试的改进
当我们开发热更工程时,主工程内的字段当然是应该加以限制的,起码不应该删除或变更类型。所以我们着重解决的就应当是字段顺序问题。
针对这个问题,其实可以考虑使用json、xml作为数据序列化与反序列化的格式。这样,每个字段在加载时根据一个字符串名去加载所需的数据,就可以解决字段顺序所导致的潜藏的问题。
但是我刚刚说过了,这么做的代价就是性能。无论解析json、xml的计算性能,抑或是将他们加载进内存的内存压力,整体性能相较现有的二进制方案都是远远不及的。
以上就是我总结的ILRuntime下的数据热更方案。其实很多细节我没有给出,因为每个项目对待ILRuntime的加载、数据的管理和加载会有区别,所以热更数据如何注册绑定如何序列化不是这篇文章要讲的内容,所以我选择了省略不讲。
既要利用ILRuntime带给我们的热更的便利性,又要避免它潜在的性能大坑,我认为我花了两天去设计实现的这个新方案是能满足我对它的期望的。
网友评论