本文严禁转载。
之前的方法及其局限
问题背景和最初的尝试见这里。最开始的想法比较简单,只想着利用 PostprocessBuild 这个事件,来对已经准备好的本地工程文件(iOS 或 Android)中的 .NET 程序集进行注入。但是,这样做限制很多。
首先,无法对 IL2CPP 作为 Scripting Backend 的情况进行注入。因为触发这个事件时,本地工程文件中没有 .NET 程序集,只有 C++ 代码,无法用 Cecil 进行注入。
第二,Android 平台,用 Mono2x 作为 Scripting Backend 的情况下,也需要打包为 Android Studio Project 才能使用。对于直接打包成 apk 的情况,无法简单的进行注入(除非使用解包、注入、重新签名打包的方法,比较麻烦)。
第三,iOS 平台,即使用 Mono2x 作为 Scripting Backend,也无法成功。这是因为在 iOS 平台打包需要先进行一个叫 AOT Cross Compiling 的步骤,对所有的程序集生成对应的 .dll.s 文件。这些文件包含的信息会在运行时被校验,如果我篡改了程序集,而没有理会 .dll.s 文件,在运行时会报错。错误信息类似
A script behaviour (probably XXX?) has a different seralization layout when loading. (Read ** bytes but expected ** bytes)
Did you #ifdef UNITY_EDITOR a section of your serialized properties in any of your scripts?
其中 XXX 是 .NET 脚本名称,两组星号表示两个不同值。这错误最终导致脚本加载失败,无法运行游戏。与错误信息描述不同,我并没有在出问题的脚本上写任何条件编译的代码。要想解决这个问题,估计需要篡改 .dll.s 文件才可以,仍然是很不经济的。
篡改编译器的方法
接下来一个办法,就是对 Unity 的 C# 编译器 mcs.exe 进行篡改。我没有深入实验,因为几个简单的实验就耗费了一天多的时间。我主要尝试了两种方法,当然,都没成功。
方法一,将原 mcs.exe 重命名(如 mcs1.exe),而后自己写一个 .NET 控制台应用程序,占据原来 mcs.exe 的位置,在其中用 System.Diagnostic.Process 类来启动 mcs1.exe。这个过程中,我对 Process 对象的一些配置,如环境变量(EnvironmentVariables 属性)、输入输出重定向(RedirectStandardXXX 属性)进行了多种排列组合,仍无法正确调用 mcs1.exe,就更不要说调用之后的事情了。
方法二,直接在 mcs.exe 中注入代码。因为 mcs.exe 也是一个 .NET 应用程序,并且看上去未经混淆,所以直接注入是可行的。即,「把向游戏程序集中注入代码的代码,注入到编译器中。」这样做主要的问题,是 mcs.exe 的输出目录是临时文件夹,无法保证其中有我们依赖的(如注入后写入程序集时,需要用 Mono.Cecil 的 DefaultAssemblyResolver 进行解析的)程序集。
通过PostprocessScene回调事件来进行注入
Unity 虽然没有在执行 mcs.exe 和后续步骤(IL2CPP、Android 打包 apk、iOS 上的 AOT 交叉编译等)之间提供回调,但是回调事件 OnPostprocessScene 目前是确保在它们之间至少触发一次的。多亏 https://github.com/rayosu/UnityDllInjector 提醒了我。在这个事件回调中处理 DLL,理论上在任何平台、任何 Scripting Backend 上都可以有效注入。实现过程中有几个要点需要注意:
- 事件 OnPostprocessScene 对应 Build Settings 中指定打包的场景个数,所以它可能执行多次,故而需要防止重复。除了上述 UnityDllInjector 中提供的方法,还可以直接把注入标记写入你的目标程序集。但值得注意的是,新增一个用于标记的空类在 iOS + Mono2x 下又是不好用的,猜测还和 AOT 交叉编译有关。保险的做法之一,是在游戏代码中保留几个 bool 常量,值为 false,注入前检查相应的值,如果为 true 则跳过,否则注入。注入完成后,将相应的 bool 常量篡改为 true 即可。
- 游戏脚本对应的程序集,在注入时一定处于和 Assets 同级的 Library 下的 ScriptAssemblies 文件夹下,但要注意你依赖的 Unity 程序集。我使用 UnityDllInjector 提供的方法,依然不能保证获取到需要的程序集。最终我采用的方法是,使用 EditorApplication.applicationContentsPath 获取 Unity 安装目录,在其中 Data/Managed 目录里寻找必要的程序集。
目前我测试了 Android + Mono/IL2CPP 和 iOS + IL2CPP,都没有问题。iOS + Mono2x 可能由于我们项目本身的一些问题,在 Xcode 链接阶段有一些问题。