• 记一次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不可懈怠!

  • 相关阅读:
    nodejs下载图片保存本地
    anaconda基本命令
    1.购买腾讯学生服务器遇到的坑
    git 命令
    JS深拷贝递归实现
    Object.prototype.toString()
    Spring核心知识点
    Spring核心知识点
    Spring核心知识点
    Spring基础知识点
  • 原文地址:https://www.cnblogs.com/wayland/p/9490404.html
Copyright © 2020-2023  润新知