使用 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(, 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 手游进行性能测试的更多相关文章
- Unity3D手游开发实践
<腾讯桌球:客户端总结> 本次分享总结,起源于腾讯桌球项目,但是不仅仅限于项目本身.虽然基于Unity3D,很多东西同样适用于Cocos.本文从以下10大点进行阐述: 架构设计 原生插件/ ...
- (转)Unity3D手游开发实践
作者:吴秦出处:http://www.cnblogs.com/skynet/本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含链接). (转)& ...
- [unity3d]手游资源热更新策略探讨
原地址:http://blog.csdn.net/dingxiaowei2013/article/details/20079683 我们学习了如何将资源进行打包.这次就可以用上场了,我们来探讨一下手游 ...
- Unity3D手游开发日记(6) - 适合移动平台的水深处理
市面上大部分的手机游戏,水面都比较粗糙,也基本没发现谁做过水深的处理. 水深的处理在PC平台比较容易,因为很容易获得每个像素的深度,比如G-Buffer,有了像素的深度,就能计算出每个像素到水面的距离 ...
- Unity3D手游开发日记(9) - 互动草的效果
所谓互动草,就是角色跑动或者释放技能,能影响草的摆动方向和幅度. 前面的文章早已经实现了风吹草动的效果,迟迟没有在Unity上面做互动草,是因为以前我在端游项目做过一套太过于牛逼的方案.在CE3的互动 ...
- Unity3D手游-横版ACT游戏完整源代码下载
说明: 这不是武林.这不是江湖,没有道不完的恩怨,没有斩不断的情仇,更没有理不清的烦恼,这是剑的世界,一代剑魁闯入未知世界,将会为这个世界展开什么样的蓝图.让你来创造它的未来,剑魁道天下,一剑斗烛龙! ...
- Unity3D 手游开发中所有特殊的文件夹
这里列举出手游开发中用到了所有特殊文件夹. 1.Editor Editor文件夹可以在根目录下,也可以在子目录里,只要名子叫Editor就可以.比如目录:/xxx/xxx/Editor 和 /Edi ...
- Unity3D手游开发日记(8) - 运动残影效果
2D游戏的残影很简单,美术做序列帧图片就行了,那么3D游戏的残影美术做不了,得靠程序员动态创建模型来处理. 实现原理也很简单: 1.间隔一定时间创建一个残影模型 GameObject go = Gam ...
- Unity3D手游开发日记(7) - 适合移动平台的天气效果
腾讯的天涯明月刀的天气很棒,以前我也在CE3的基础上做了一个效果差不多的,但是在手机上,还是斜下固定视角的游戏,是否还需要一个天气系统? 而且没有G-Buffer的支持,很多牛逼效果实现不了,比如角色 ...
随机推荐
- iOS开发之Run Loop
1.概述 (1) Run Loop提供了一种异步执行代码的机制,不能并行执行任务. (2) 在主队列中,Main Run Loop直接配合任务的执行,负责处理UI事件.计时器,以及其它内核相关事件. ...
- css的存在方式和选择器
css的存在方式 元素内联 页面嵌入 外部引入 元素内联 直接在html的标签中定义样式,类似于: <div style="属性1;属性2;属性3"><div&g ...
- Python快速入门(4)
输入输出: open() read() readine() readlines() write() pickle模块可以做序列化操作,持久保持对象的信息. 我们可以很容易的读写文件中的字符串.数值就要 ...
- ASP.NET Core:使用Dapper和SwaggerUI来丰富你的系统框架
一.概述 1.用VS2017创建如下图的几个.NET Standard类库,默认版本为1.4,你可以通过项目属性进行修改,最高支持到1.6,大概五月份左右会更新至2.0,API会翻倍,很期待! 排名分 ...
- 使用register_shutdown_function触发写日志,使用fastcgi_finish_request提高响应速度
公司内部的市场管理系统,一直是我一个人维护,最近老是有开发埋怨,内网的账号被人改了密码,账号被解绑了...哈的,错在这还不是一个完整的系统,既没有严格的权限也没有做操作日志呀... 权限现在是准备做在 ...
- PyQt通过resize改变窗体大小时ListWidget显示异常
前几天开始的pygame音乐播放器Doco,做的差不多了,上午做到了歌词显示和搜索页面.遇到bug,即通过resize改变ui大小时ListWidget显示异常 #目的: 增加一部分窗口用来显示歌词和 ...
- POJ1185炮兵阵地【动态规划】
炮兵阵地 Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 26892 Accepted: 10396 Descriptio ...
- 【转】一个小工具类,利用shareObject把数据缓存
原文链接:http://bbs.9ria.com/thread-284082-1-2.html 之前做一个数据缓存,就顺便把写入缓存,清除缓存,获取缓存都整理了一下,其中也做了些参考,个人水平有限,有 ...
- 3.Redis常用命令:String
字符串类型是Redis中最为基础的数据存储类型,它在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等.在Redis中字符串类型的Value ...
- user-modify属性,让html标签可以编辑
其实这只是一个很小的需求,但是写着写着发现干货越来越多,所以特意给大家分享一下. 项目需要做一个类似QQ聊天输入的效果 有的同学说,这不是很简单吗?一开始我也这么感觉 :) 观察需求 1.整体固定在底 ...