记一次JsonConvert.DefaultSettings误用导致的Bug调试

懒人有各种各样的偷懒手段,主要是他想偷懒。

最近又扎入了阅读的深渊而不能自拔,一不小心意识到已是八月,索性就偷闲对最近开发中的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不可懈怠!

原文地址:https://www.cnblogs.com/wayland/p/9490404.html