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(, loadStr);
var callBegin = Instruction.Create(OpCodes.Call, targetMethod.Module.Import(m_BeginSampleMethod));
instructions.Insert(, callBegin);

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

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

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

  1. Unity3D手游开发实践

    <腾讯桌球:客户端总结> 本次分享总结,起源于腾讯桌球项目,但是不仅仅限于项目本身.虽然基于Unity3D,很多东西同样适用于Cocos.本文从以下10大点进行阐述: 架构设计 原生插件/ ...

  2. (转)Unity3D手游开发实践

    作者:吴秦出处:http://www.cnblogs.com/skynet/本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含链接). (转)& ...

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

    原地址:http://blog.csdn.net/dingxiaowei2013/article/details/20079683 我们学习了如何将资源进行打包.这次就可以用上场了,我们来探讨一下手游 ...

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

    市面上大部分的手机游戏,水面都比较粗糙,也基本没发现谁做过水深的处理. 水深的处理在PC平台比较容易,因为很容易获得每个像素的深度,比如G-Buffer,有了像素的深度,就能计算出每个像素到水面的距离 ...

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

    所谓互动草,就是角色跑动或者释放技能,能影响草的摆动方向和幅度. 前面的文章早已经实现了风吹草动的效果,迟迟没有在Unity上面做互动草,是因为以前我在端游项目做过一套太过于牛逼的方案.在CE3的互动 ...

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

    说明: 这不是武林.这不是江湖,没有道不完的恩怨,没有斩不断的情仇,更没有理不清的烦恼,这是剑的世界,一代剑魁闯入未知世界,将会为这个世界展开什么样的蓝图.让你来创造它的未来,剑魁道天下,一剑斗烛龙! ...

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

    这里列举出手游开发中用到了所有特殊文件夹. 1.Editor Editor文件夹可以在根目录下,也可以在子目录里,只要名子叫Editor就可以.比如目录:/xxx/xxx/Editor  和 /Edi ...

  8. Unity3D手游开发日记(8) - 运动残影效果

    2D游戏的残影很简单,美术做序列帧图片就行了,那么3D游戏的残影美术做不了,得靠程序员动态创建模型来处理. 实现原理也很简单: 1.间隔一定时间创建一个残影模型 GameObject go = Gam ...

  9. Unity3D手游开发日记(7) - 适合移动平台的天气效果

    腾讯的天涯明月刀的天气很棒,以前我也在CE3的基础上做了一个效果差不多的,但是在手机上,还是斜下固定视角的游戏,是否还需要一个天气系统? 而且没有G-Buffer的支持,很多牛逼效果实现不了,比如角色 ...

随机推荐

  1. Python学习笔记之基本语法学习1

    ★学习目标: 用Python做HTTP接口测试 ★学习的大纲: ●Python语言基础(安装,第一个案例,基本语法等) ●Request模块使用 ●编写一个简单功能的接口测试案例 ●HTTP协议基础 ...

  2. Django发送带图片和附件的邮件

    最近需要做集团的SRC系统.暂无安全研发,所以只能找我这个小菜兼职开发.系统使用Django框架,在整个过程中,有许多奇特的需求.在某项需求中,需要给厂商用户发送富文本邮件,漏洞详情,这个折腾了一下, ...

  3. Appium 解决锁屏截屏问题(java篇)

    今天有个小伙伴问我,怎么把锁屏进行解锁操作?   A.思路在初始化driver后,加入等待判断是否有锁屏(元素)(记得要加入等待) B.如果有就进行解锁,就一般的输入数字密码然后进行解锁(当然了你要知 ...

  4. 无线接收信号强度(RSSI)那些事儿

    本文由嵌入式企鹅圈原创团队成员黄鑫供稿. 本文所述的原理适用于所有无线传输技术,只是用蓝牙来举例.应该说,嵌入式企鹅圈更加偏重于嵌入式和物联网.安卓技术原理方面的知识分享和传播,其次才是实践,尽管很多 ...

  5. MongoDB基础教程系列--第五篇 MongoDB 映射与限制记录

    上一篇提到的 find() 的方法,细心的伙伴会发现查询的结果都是显示了集合中全部的字段,实际应用中,显然是不够用的.那么有没有办法指定特定的字段显示出文档呢?答案是肯定的,MongoDB 中用映射实 ...

  6. Visual Studio 2017无法加载Visual Studio 2015创建的SharePoint解决方案

    前几天安装了最新的Visual Studio 2017企业版,发现无法打开之前使用Visual Studio 2015创建的SharePoint 2016解决方案,提示"需要更新" ...

  7. Influxdb1.2.2安装_Windows

    一.文件准备 1.1 文件名称 influxdb-1.2.2_windows_amd64.zip 1.2 下载地址 https://portal.influxdata.com/downloads [注 ...

  8. mac的终端为什么会显示git:(master),如何取消掉?

    今天在终端误操作,在主目录下执行git init命令,结果杯具了, 总是出现这个提示. 各种搜索解决方案,终于退出了. 方法如下: 删掉.git目录: rm -rf ~/.git

  9. 关于constraint的用法

    1.主键约束:要对一个列加主键约束的话,这列就必须要满足的条件就是非空因为主键约束:就是对一个列进行了约束,约束为(非空.不重复)以下是代码  要对一个列加主键,列名为id,表名为emp格式为:alt ...

  10. css系列:input的placeholder在safari下设置行高失效

    在项目中遇到input的placeholder在safari下设置行高失效的问题,问度娘后未得治原因,倒是有解决办法: 方法一:使用padding使提示文字居中,如果font-size:14px,UI ...