使用 Mono.Cecil 辅助 Unity3D 手游进行性能测试

Unity3D 引擎在  UnityEngine 名字空间下,提供了  Profiler 类(Unity 5.6 开始似乎改变了这个名字空间),用于辅助对项目性能进行测试。以 Android 平台为例,在构建之前,需要在 Unity 的 File/Build Settings 菜单项弹出的窗口中,勾选 Development Build 一项。后用  adb forward  的方式,将 Android 设备的 TCP 输出转发到电脑,实现和 Unity Profiler 的连接(网上很容易找到这个过程的具体描述,如这里)。但是 Unity Profiler 默认只提供部分方法/函数,尤其是 Unity 内置方法/函数的性能采样,如果想 Profile 自己项目的代码段,就必须在代码段入口和出口加上:

Profiler.BeginSample("ProfilerName");
// 代码段。
Profiler.EndSample();

对于一个已经进行了一段时间,有十几万行代码的项目,想逐个方法添加采样代码,甚至在加上预编译命令,是非常麻烦的,而且很容易出错。

幸好我遇到了 Mono.Cecil 这个库(github 链接)。

这个库所做的事情,并不难理解。我们知道 C# 通常编译为中间语言(IL),之后由 .NET 虚拟机对其进行执行。Cecil 的一部分能力就在于,可以任意修改生成的 IL。尽管 .NET 的 Mono 实现有很多缺陷,且 Unity 在 iOS 平台上早已推出 IL2CPP 机制,将 IL 转换为 C++ 代码,再在目标平台进行原生的 C++ 编译链接,但在 Android 设备上,大部分项目仍然使用 Mono 作为 Scripting backend。也就是说,安装入 Android 设备的 apk 包中是带有 C# 程序集的,而程序集中其实是 IL 代码。为了方便,在 Unity 的 File/Builde Settings 中,勾选 Google Android Project 一项,要求 Unity 不要直接生成 apk,而是生成 Android 工程。这样,在 Unity 的构建过程结束后,就可以使用 Cecil 在目标文件夹夹的 C# 程序集上注入代码。

如何注入呢?大体有几个要点:

  • 自定义 System.Attribute 子类,装饰必要的类或者方法。这样,之后用 Cecil 注入代码时,就可以根据这些装饰,实现白名单或者黑名单的功能,决定哪些地方要注入,哪些地方不要注入。
  • 用一个简单的静态类包装 Profiler.BeginSample 和 Profiler.EndSample 。由于这两个方法只允许在 Unity 的主线程调用,如果在别的线程调用,就会发生运行时错误。所以,包装类的作用,就是在真正调用这两个方法之前,检查当前线程是否为主线程。例如,对于 Unity 而言,所有 MonoBehaviour 类的构造函数和字段初始化式(Field initializer)都不是在主线程调用的。如何检查当前线程是否为主线程呢?只需要在项目第一个 Awake 方法中,将当前线程的引用( System.Threading.Thread.CurrentThread )记录下来,调用  Profiler.BeginSample  和 Profiler.EndSample  之前判断当前线程的引用是否和主线程引用相等即可。
  • 如果有自动化的打包流程,在调用 BuildPipeline.BuildPlayer 时,加入 BuildOptions.Development 和 BuildOptions.AcceptExternalModificationsToPlayer 来进行 Development Build,并且构建出 Android Studio 工程而非 apk 文件(参考这里)。构建结束时,利用 Unity 提供的后处理方法(官方文档)获取相应程序集的路径,用 Cecil 进行注入。注意 using 两个名字空间 Mono.Cecil 和 Mono.Cecil.Cil。

使用 Cecil 的要点主要是:

  • 用 ModuleDefinition.ReadModule(string) 来读取一个 C# 程序集,到一个 ModuleDefinition 对象中。
  • 对于需要注入代码的程序集,在读取时,要输入一个 resolver 对象,以便能解析来自该程序集之外的方法。
    var resolver = new DefaultAssemblyResolver();
    // 搜索目标文件夹中的程序集来解析程序集外部的方法。
    resolver.AddSearchDirectory(directory);
    var moduleDef = ModuleDefinition.ReadModule(assemblyPath, new ReaderParameters { AssemblyResolver = resolver });
  • 在模块定义( ModuleDefinition  )对象中,使用其 Types 和  TypeDefinition  的 Methods 属性,辅以 Linq 中的扩展方法,找到需要的方法定义。
  • 对于一个方法定义( MethodDefinition )对象,其 HasBody 属性说明是否真的有方法体。比如抽象方法,就是没有方法体的。对于有方法体的方法定义对象 targetMethod ,我们需要调用 targetMethod.Body.Instructions 来获取该方法中的全部 IL 指令。
  • 本文的场景下,只需要在方法的入口和出口注入代码。入口注入代码比较简单,只要构造一条 IL 指令,将其插入到指令容器开头( Insert  方法)即可。由于 Profiler.BeginSample 方法(随即其包装方法)带有一个字符串类型的参数,所以需要两条 IL 指令。

var loadStr = Instruction.Create(OpCodes.Ldstr, myString);
instructions.Insert(0, loadStr);
var callBegin = Instruction.Create(OpCodes.Call, targetMethod.Module.Import(m_BeginSampleMethod));
instructions.Insert(1, callBegin);

  • 方法出口注入代码稍微有些麻烦。尽管 IL 级别的函数都是以一个返回指令结束的,但直接在返回指令之前插入新的指令是不够的。因为很多时候,返回指令是由跳转指令直接跳转过去的。而对于我们在 C# 中获取的指令容器,跳转指令保存了其跳转目标的引用。因此,我们不仅需要在返回指令前插入我们需要的指令(对 Profiler.EndSample 包装方法的调用),还要将跳转目标为该返回指令的跳转指令的目标,修改为我们新增的指令。这里有详尽的关于 IL 指令的列表。对应 Cecil 中  OpCodes 类中的常量,我们可以过滤出跳转指令,并用 Operand 属性获取或修改其跳转目标。
  • 修改完成后,需要对当前的模块对象 moduleDef 调用 moduleDef.Write(assemblyPath, new WriterParameters { WriteSymbols = true }) 来写回程序集文件。这个调用中,第二个参数的含义,是把新增的符号也写入程序集(比如我们调用的该程序集之外的方法)。

在注入完成后,继续 Android 平台的原生构建生成 apk 包,安装进设备,将设备连接电脑,即可在 Unity 的 Profiler 窗口中看到新增的性能采样信息。

时间: 2024-08-06 18:21:23

使用 Mono.Cecil 辅助 Unity3D 手游进行性能测试的相关文章

使用 Mono.Cecil 辅助 Unity3D 手游进行性能测试(续)

本文严禁转载. 之前的方法及其局限 问题背景和最初的尝试见这里.最开始的想法比较简单,只想着利用 PostprocessBuild 这个事件,来对已经准备好的本地工程文件(iOS 或 Android)中的 .NET 程序集进行注入.但是,这样做限制很多. 首先,无法对 IL2CPP 作为 Scripting Backend 的情况进行注入.因为触发这个事件时,本地工程文件中没有 .NET 程序集,只有 C++ 代码,无法用 Cecil 进行注入. 第二,Android 平台,用 Mono2x 作

Unity3D手游开发实践

<腾讯桌球:客户端总结> 本次分享总结,起源于腾讯桌球项目,但是不仅仅限于项目本身.虽然基于Unity3D,很多东西同样适用于Cocos.本文从以下10大点进行阐述: 架构设计 原生插件/平台交互 版本与补丁 用脚本,还是不用?这是一个问题 资源管理 性能优化 异常与Crash 适配与兼容 调试及开发工具 项目运营 ? 1.架构设计 好的架构利用大规模项目的多人团队开发和代码管理,也利用查找错误和后期维护. 框架的选择:需要根据团队.项目来进行选择,没有最好的框架,只有最合适的框架. 框架的使

[unity3d]手游资源热更新策略探讨

原地址:http://blog.csdn.net/dingxiaowei2013/article/details/20079683 我们学习了如何将资源进行打包.这次就可以用上场了,我们来探讨一下手游资源的增量更新策略.注意哦,只是资源哦.关于代码的更新,我们稍后再来研究.理论上这个方案可以使用各种静态资源的更新,不仅仅是assetbundle打包的. (转载请注明原文地址http://blog.csdn.net/janeky/article/details/17666409) 原理 现在的手游

Unity3D手游开发日记(6) - 适合移动平台的水深处理

市面上大部分的手机游戏,水面都比较粗糙,也基本没发现谁做过水深的处理. 水深的处理在PC平台比较容易,因为很容易获得每个像素的深度,比如G-Buffer,有了像素的深度,就能计算出每个像素到水面的距离,实现水深alpha渐变. 但是在移动平台,又是万恶的浮点纹理...导致此方案不行. 但是方案都是人想出来的,我想了两种适合移动平台的方案 方案1:用水面顶点颜色保存alpha值来做水深渐变. 这种方案,要求水面的模型面片是格子的,就像地形网格一样,格子越密,alpha的精度才越高. 方案2:用贴图

unity3D手游地图设计的四大类型 哪种适合你?

据报道/最近,小编在外媒看到一篇有关手游地图设计方面的博客,英文作者Junxue Li(李俊学)是游戏美术外包公司Novtilus Art的CEO,他在博客中讨论了四种不同类别的手游地图以及各自适合的手游类型,并且谈到了这些地图类型的优势与不足,这里分享给业内的童鞋们,希望可以有所帮助,以下是狗刨学习网编译的博客内容: 我们的团队为一些休闲游戏做过地图,包括三消.泡泡龙.老虎机游戏等等内容,其实,这些不同类型的手游的地图其实是比较类似的,都有线性升级进度,而且可以通过地图形式展现出升级的过程.

Unity3D手游开发日记(9) - 互动草的效果

所谓互动草,就是角色跑动或者释放技能,能影响草的摆动方向和幅度. 前面的文章早已经实现了风吹草动的效果,迟迟没有在Unity上面做互动草,是因为以前我在端游项目做过一套太过于牛逼的方案.在CE3的互动草的基础上扩展,效果好,但技术太复杂,效率开销也特别高. 如果在手机上,就得做一套简单高效的. 实现效果:从任意方向碰一下草,草就应该来回晃动,晃动幅度逐渐减小.多次触碰,效果应该叠加.这样的话就比较真实. 实现原理:用正玄波实现草来回摆动的简谐运动,用指数衰减来模拟阻力 实现步骤: 1.每个草挂一

Unity3D手游-横版ACT游戏完整源码下载

说明: 这不是武林,这不是江湖,没有道不完的恩怨,没有斩不断的情仇,更没有理不清的烦恼,这是剑的世界,一代剑魁闯入未知世界,将会为这个世界展开什么样的蓝图,让你来创造它的未来,剑魁道天下,一剑斗烛龙!!! 游戏开发了三个月,非常值得收藏,至于做什么用途,就看你自己啦!来来来,放大招了!请看附件:) 测试环境: Unity3D 4.3.4 运行效果: 完整源码下载http://www.yxkfw.com/thread-5035-1-1.html

Unity3D 手游开发中所有特殊的文件夹

这里列举出手游开发中用到了所有特殊文件夹. 1.Editor Editor文件夹可以在根目录下,也可以在子目录里,只要名子叫Editor就可以.比如目录:/xxx/xxx/Editor  和 /Editor 是一样的,无论多少个叫Editor的文件夹都可以.Editor下面放的所有资源文件或者脚本文件都不会被打进发布包中,并且脚本也只能在编辑时使用.一般呢会把一些工具类的脚本放在这里,或者是一些编辑时用的DLL. 比如我们现在要做类似技能编辑器,那么编辑器的代码放在这里是再好不过了,因为实际运行

Unity3D手游开发日记(4) - 适合移动平台的热浪扭曲

热浪扭曲效果的实现,分两部分,一是抓图,二是扭曲扰动.其中难点在于抓图的处理,网上的解决方案有两种,在移动平台都有很多问题,只好自己实现了一种新的方案,效果还不错. 网上方案1. 用GrabPass抓图 GrabPass在有的手机上是不支持的...效率也是问题,所以... 代码可以看看: [csharp] view plain copy Shader "Luoyinan/Distortion/HeatDistortion" { Properties { _NoiseTex ("