【Unity实战】利用Preserve特性解决代码裁剪导致的反射调用失效问题
1. 代码裁剪与反射调用的相爱相杀第一次遇到这个问题是在去年做手游项目的时候。那天测试同事急匆匆跑过来说哥安卓包加载存档直接闪退我心想编辑器里明明好好的怎么打包就出问题打开日志一看满屏都是MissingMethodException——找不到方法的报错。这种问题就像捉迷藏编辑器里藏得好好的一打包就现原形。后来发现是Unity的Managed Stripping在作怪。这个功能就像个勤快的保洁阿姨会把项目里没用的代码统统清理掉。在Player Settings的Optimization里你能看到Low/Medium/High三个档位级别越高删得越狠。普通情况下这功能很贴心能帮安装包瘦身但遇到反射调用时就容易误伤友军。反射调用就像用字符串当名片去拜访代码编译时保洁阿姨根本不知道这些动态关系。比如用JsonUtility反序列化时系统要靠反射机制找到类的无参构造函数。如果这个构造函数在项目里没有显式调用过阿姨就会认为它是垃圾代码直接扔掉。等运行时反射拿着名片找人时发现办公室早就搬空了。2. 如何诊断代码裁剪引发的问题遇到这种问题别急着抓瞎我总结了个诊断三步法首先看报错特征。典型的裁剪问题有两个标志1)只在打包后出现 2)错误信息里带着Reflection字样。就像我最近遇到的这个错误MissingMethodException: Default constructor not found... at System.Activator.CreateInstance(Type type) at Newtonsoft.Json.Serialization.DefaultContractResolver.CreateObjectContract(Type objectType)其次检查裁剪等级。打开Project Settings Player Other Settings找到Managed Stripping Level。如果是Medium或High嫌疑就更大了。有个取巧的办法临时改成Low打包测试如果问题消失基本就能锁定病因。最后用ILSpy反编译查看。把打包后的Assembly-CSharp.dll拖进ILSpy搜索报错缺失的类或方法。如果发现方法签名还在但方法体变成throw null那就是被裁剪的特征。就像下面这个例子// 反编译结果 public class SaveData { // 被裁剪的构造函数 public SaveData() { throw null; } }3. Preserve特性的花式用法[Preserve]特性就像是给代码贴上的重要文件请勿销毁标签。Unity官方文档说它能作用于类、方法、字段、属性甚至整个程序集。经过多个项目实战我整理了几个典型使用场景构造函数保留是最常见的特别是配合序列化库使用时using UnityEngine.Scripting; public class PlayerData { [Preserve] public PlayerData() {} // JSON反序列化需要的构造器 public PlayerData(int hp) {...} // 业务逻辑用的构造器 }泛型方法保留需要特别注意。有次我用Dictionarystring, Action实现事件系统打包后所有回调都失效了。后来发现泛型方法容易被误剪public class EventSystem { [Preserve] public static void AddListenerT(string key, ActionT callback) {...} }第三方库保留也有妙招。比如使用LitJSON时可以给整个命名空间加保护[assembly: Preserve] // 保护整个程序集 namespace LitJson { [Preserve] // 保护特定类 public class JsonMapper {...} }4. 高级防护技巧与替代方案除了[Preserve]还有几套组合拳可以打。去年我们项目用了HybridCLR热更新代码防护就得多管齐下。link.xml配置是核武器级别的保护。在Assets下创建link.xml文件可以声明保留整个命名空间linker assembly fullnameAssembly-CSharp namespace fullnameGameFramework.SaveSystem preserveall/ type fullnameAchievementData preserveall/ /assembly /linker反射方法白名单适合大规模项目。通过实现IPostProcessBuildWithReport接口可以在打包后自动扫描反射调用public class ReflectionWhitelist : IPostProcessBuildWithReport { public int callbackOrder 0; public void OnPostprocessBuild(BuildReport report) { // 自动检测所有通过反射调用的方法 MethodInfo[] methods typeof(ReflectionHelper) .GetMethods(BindingFlags.Static | BindingFlags.Public); // 生成对应的link.xml文件 } }条件保留技巧也很实用。比如要给E2E测试保留私有方法#if UNITY_INCLUDE_TESTS [Preserve] private void InternalDebugMethod() {...} #endif5. 实战中的血泪教训踩过最深的坑是用Unity自带的JSONUtility。当时我们有个配置表系统编辑器下运行完美打包后死活加载不了。后来发现是嵌套结构的私有字段被裁剪了。解决方案是给所有序列化字段加上[SerializeField][Serializable] public class WeaponConfig { [Preserve] public WeaponConfig() {} [SerializeField] // 必须加这个 private int baseDamage; }还有个记忆犹新的案例是Addressable资源系统。我们动态加载的ScriptableObject总是报错最后发现要在资源上打Preserve标记[CreateAssetMenu, Preserve] public class CharacterData : ScriptableObject {...}最近改用Mono.Cecil做编译时分析写了个自动检测反射调用的工具。原理是在IL层扫描所有ldtoken和Type.GetMethod调用自动生成preserve列表。这个方案特别适合大型项目能避免手动标记的遗漏。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2463149.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!