懒人有各种各样的偷懒手段,主要是他想偷懒。
最近又扎入了阅读的深渊而不能自拔,一不小心意识到已是八月,索性就偷闲对最近开发中的Bug定位修改做个记录。
公司发展经年,已上线多个项目,有些项目也都上线了多个版本,伴随着跟破解玩家斗争的不断升级,终于在五年年前某个项目的某个版本中,开始引入了内存加密的手段,然后作为一只喜欢偷懒的程序狗,对内存加密的手段及方式一再进行升级和重构,为了达到尽可能的一劳永逸,终于在四年前进行了常用基本类型的加密封装:EncryptInt,EncrypyFloat,之后每每交付部门的各项目人员进行项目升级,于是开启了一段Bug的传奇。。。
在公司最新的项目之前(Unity4/5),项目中使用的序列化插件一直都是NewtonJson.Net,github上的开源项目,Unity商店中也有封装好的版本,当然为了偷懒的方便,前期未封装类型前(逐个重要信息加密时期),直接使用了别人上传的dll;为针对后期封装为加密类型后的偷懒,也对应的进行了插件的升级,这样插件升级后,我们即可以使用JsonConvert.DefaultSettings的方式进行加密类型数据的全局序列化设置:
1 JsonSerializerSettings tSetting = new JsonSerializerSettings(); 2 var tIntConverter = new JsonCustomIntConvert(); 3 var tFloatConverter = new JsonCustomFloatConvert(); 4 JsonConvert.DefaultSettings = new System.Func<JsonSerializerSettings>(() => 5 { 6 tSetting.Converters.Add(tIntConverter); 7 tSetting.Converters.Add(tFloatConverter); 8 return tSetting; 9 });
其中JsonCustomIntConvert/JsonCustomFloatConvert为我们针对加密类型EncryptInt/EncrypyFloat定义的序列化转换器:
1 public class JsonCustomIntConvert : JsonConverter 2 { 3 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 4 { 5 if(reader.TokenType == JsonToken.Null) 6 { 7 return 0; 8 } 9 else if(reader.TokenType == JsonToken.String) 10 { 11 return (EncryptInt)int.Parse(reader.Value.ToString()); 12 } 13 else if(reader.TokenType == JsonToken.Integer) 14 { 15 return (EncryptInt)Convert.ToInt32(reader.Value); 16 } 17 else 18 { 19 return 0; 20 } 21 } 22 23 public override bool CanConvert(Type objectType) 24 { 25 return objectType == typeof(EncryptInt); 26 } 27 28 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 29 { 30 int tValue = (EncryptInt)value; 31 writer.WriteValue(tValue); 32 } 33 }
于是问题就这么引入进来了。。
先来说下问题的表现及发现的历程:某一天测试在测试某个包时,突然反馈说游戏运行到半小时左右时会出现卡顿感 ,而且越往后越卡(中高档手机环境),而且卡顿的点应该是在进行数据存档。
因用户体验影响严重及项目上线紧急,于是迫不得已被拉来攻坚,svn拉个项目在开发环境下进行Bug的复现测试:
不断购买游戏道具,升级,升星。。。最终发现竟然真的有问题,初步怀疑存档过大或是数据异常,查看注册表发现并不是,而且退出游戏再重新运行之后卡顿的问题也同时消失。于是接着怀疑是存档的过程存在问题:于是在Update中每隔20帧进行一次存档来进行简化并进行打点测试。经过前期及后期的打点处(序列化存档处)时间消耗对比,发现不做内存数据做任何处理,仅不断进行序列化存储时间即在不断上涨,为了验证和玩家数据的无关性及Bug的固有性,于是将打点处的序列化改为序列化随机初始化的有50个固定数据的List,果然问题依旧,问题出在序列换的阶段。
因为JsonConvert.DefaultSettings的添加时间相比于测试出问题的时间比较久远,同时因项目多人协作开发,所以不好确认是当前版本的修改新导致的问题还是历史遗留问题,于是只好拿NewtonJson.Net项目源代码进行编译调试(最开始曾怀疑插件版本问题,于是安排人员同步进行插件版本升级测试,发现问题仍存在,甚至被吐槽设计有问题),具体调试过程因技术含量不高,主要是细致活,此处不再赘述——扩展Unity提供的Profiler及StopWatch打点调试接口,不断对源码的序列化逻辑进行二分打点测试(稍微复杂的分析点/误区在JsonSerializerInternalWriter的SerializeValue,因牵扯到递归问题,其实问题在递归发生之前/序列化之前),通过简单分析怀疑是泄露问题,所以针对可能的Add、Insert等进行了针对性的查找分析以及缓存修改测试,最终两方定位发现问题出在JsonSerializer.CreateDefault(settings)处,其底层实现为:
1 public static JsonSerializer CreateDefault() 2 { 3 // copy static to local variable to avoid concurrency issues 4 Func<JsonSerializerSettings> defaultSettingsCreator = JsonConvert.DefaultSettings; 5 JsonSerializerSettings defaultSettings = (defaultSettingsCreator != null) ? defaultSettingsCreator() : null; 6 7 JsonSerializer tSerializer = Create(defaultSettings); 8 return tSerializer; 9 }
可以看到每次序列化都会操作JsonConvert.DefaultSettings,即会不断的tSetting.Converters中添加新的序列化器,从而导致泄露。只需简单修改如下即可:
1 var tIntConverter = new JsonCustomIntConvert(); 2 var tFloatConverter = new JsonCustomFloatConvert(); 3 JsonConvert.DefaultSettings = new System.Func<JsonSerializerSettings>(() => 4 { 5 JsonSerializerSettings tSetting = new JsonSerializerSettings(); 6 tSetting.Converters.Add(tIntConverter); 7 tSetting.Converters.Add(tFloatConverter); 8 return tSetting; 9 });
所以综合整个分析过程,可以看到在具体实施层面,人员没有注意到底层的细节,以及匿名函数及闭包的影响才导致最终的Bug!
慎之慎之!修改后的及时测试及定期的code review不可懈怠!