【敏捷开发】使用静态分析优化Unity代码

发表于2018-01-24
评论3 4.3k浏览

本文首发于知乎专栏:MACK的游戏开发笔记,欢迎各位关注。


在Unity开发者大会上听了一个介绍对Unity脚本做静态分析的演讲,感觉非常有用,于是做了一些尝试。大概就是提供了一个基于微软Roslyn插件UnityEngineAnalyzer可以对Unity的代码做静态分析,找到一些代码隐患和一些可能存在的性能问题。插件可以单独运行,也可以导入VS做为一个插件,它取代部分人工的ProjectReview,并可集成到CI系统(持续集成)做自动化检测。


什么是静态代码分析

简而言之就是指在不运行代码的情况下,通过词法分析,语法分析等技术对代码做扫描和解析,验证代码是否满足规范,是否有隐患,是否可靠可维护等。例如比较著名的Lint可以检测出可能的空指针,拼写错误,除0等等。VS,还有VS的插件Resharper也带有静态代码分析的功能。和一般的插件不同,UnityEngineAnalyzer可以分析一些Unity特有的一些性能和问题隐患,也支持自定义添加规则。


什么是Roslyn

Roslyn 是微软公司开源的 .NET 编译器。编译器支持 C# 和 Visual Basic 代码编译,并提供丰富的代码分析 API。这是一个更为开放式的编译器,与以往不透明的编译过程不同,开发者可以在编译过程中访问和分析编译数据。根据提供的API可以进行大量用户自定义的扩展。通过使用Roslyn提供的API,在您键入代码时,甚至在您完成一行之前,它们就能生成警告,不需要等到生成代码时才找出您所犯的错误。分析器还可以通过新的 Visual Studio 灯泡图标提示来显现自动代码修复方案,让您立即清理代码。


为什么需要静态代码分析

  • 提早发现代码中的BUG,避免将BUG带到生产环境
  • 极大的提高软件质量,以及可维护性
  • 统一代码规范、提高可读性,减少新加入成员的熟悉时间
  • 加速个人和团队的成长,知识和经验的积累
  • 节约有限的开发时间
  • 避免人工检查的遗漏,不可能记住所有优化点也不能保证不遗漏
  • 可以内建到CI系统(持续集成)

规范的项目一般会有代码审核,而代码审核分为人工审查和工具审查。人工审查方面目前每天会有代码Review,每个版本会做ProjectReview。但是人都会有疏漏,使用自动化的工具进行补充可以避免人工检查的漏洞和节省大量时间。


UnityEngineAnalyzer的优点

  • 可以发现Unity特有的一些潜在问题,例如代码规范,性能问题
  • 可以作为VS插件使用,也可以作为独立的程序单独执行
  • 可以通过ComandLine执行
  • 可以生成HTML报告
  • 多平台支持

以下是他们官网的介绍:UnityEngineAnalyzer分析器是一套基于Roslyn分析程序,旨在检测Unity3D C#代码中的常见问题。Unity3D让我们很容易做跨平台游戏,但是有一些隐藏的规则例如性能和AOT等,会影响到程序的体验和测试等。我们希望这些问题能在编译之前被发现。


UnityEngineAnalyzer的检查项

  • 不允许直接Tag的比较,使用CompareTag代替直接比较减少GC;
  • 不允许在Update中调用Find;
  • 不允许有空的MonoBehaviour方法,例如Update,FixedUpdate,LateUpdate等;
  • 不允许使用OnGUI,OnGUI会导致GC;
  • 不允许使用Coroutine避免产生GC,这个目前使用的5.5.1版本已经修复不需要了;
  • 不允许使用Foreach避免产生GC,这个目前使用的5.5.1版本已经修复不需要了;
  • string方法
  • Remoting(AOT),EmitCalls(AOT),GetType(AOT)
  • 对IL2CPP,使用去虚拟化可以提升部分虚函数调用的开销 gad.qq.com/article/deta
  • …….

例子如下:



UnityEngineAnalyzer的使用

  • 命令行模式

1. 到 github.com/vad710/Unity 下载最新版本,然后解压缩,编译。UnityEngineAnalyzer.CLI\bin目录下得到可执行文件UnityEngineAnalyzer.CLI.exe。

2. 打开命令提示符或Powershell窗口

3. 运行UnityEngineAnalyzer.CLI.exe <project path>。例如:> UnityEngineAnalyzer.CLI.exe C:\Code\MyGame.CSharp.csproj

4. 观察分析结果

5. 在项目文件相同位置,会生成report.json和UnityReport.html报告

上图是我们项目的分析报告。需要注意的是html需要用FireFox浏览器打开,json可以直接浏览。原始工程的exe在执行的时候会报一些异常需要修改一下代码,主力工程不需要。命令行模式设置可以通过选择项目-〉右键-〉属性;调试-〉命令行参数。然后在命令行参数里面输入命令行参数调试运行。


  • NuGet打包

有两种方式可以制作插件集成到VS工程中,一个就是以NuGet package的方式。因为在网上已经有编译好的NuGet包,我只尝试了直接使用并没有编译本地的的NuGet。安装步骤如下:

1. 使用VS2015打开目标工程

2. 选择Tools->NuGet Package Manager->Manage NuGet Package for solution,如下图:

3. 在Browse页面搜索UntiyEngineAnalyzer并选中

4. 在右侧属性面板中勾选你希望分析的项目点击Install。(注意源的版本比较老只能添加一个工程)


  • VSIX扩展包

另一种方式是编译成VSIX插件,安装到VS中对IDE下所有项目进行分析。只要编译UnityEngineAnalyzer.Vsix工程在bin中双击UnityEngineAnalyzer.Vsix.vsix安装即可。安装完毕可以在Tools/Extensions and Updates里看到,如下图:

重启VS之后在Error List就可以看到用户自定义的Unity特有的Wanrings了,如下图:

因为NuGet源上的版本非常老了而且使用没有VSIX方便,所以最终使用了VSIX的方式,给其他人提供了VSIX的插件。


UnityEngineAnalyzer的扩展

我们当然可以对分析器进行扩展和修改。例如新版本的Unity不需要对Foreach等做限制,另外可以添加项目组自己的代码规范,代码风格,特定设置等。

解决方案中的工程如下:

  • UnityEngineAnalyzer:这是主项目,构建包含诊断和代码修补程序的分析器 DLL
  • UnityEngineAnalyzer.CLI:这是可执行的exe
  • UnityEngineAnalyzer.Test:这是单元测试项目,有一些测试用例便于调试
  • UnityEngineAnalyzer.Vsix:这是制作用于VS的插件的项目,也是把dll绑定到VS中

安装完SDK之后VS有相应的模版可以创建工程,这里没有这个需求只是在源工程上修改。

如果通过模版创建会默认生成DiagnosticAnalyzer代码,这里每条分析都一个派生类继承自DiagnosticAnalyzer。例如DoNotUseOnGUIAnalyzer。

在DiagnosticIDs这个文件里可以自定义一些用户自己希望使用的警告标签,例如DoNotUseOnGUI = "UEA0001"

AnalyzerTestFixture是测试用例的基类。

通过设置UnityEngineAnalyzer.Vsix启动启动,按F5启动可以启动一个VS副本用来调试。这和安装完VS第一次启动一样,还需要设置一些参数风格等。如下图右边的VS通过调试模式启动左边的VS,这是在打断点调试。

如下图可以看到副本的VS中警告部分会有绿色下划线,下方会有warning的标签,原因提示等,双击也可以调到有问题的代码的地方。

然后我们看一下如何进行扩展。首先第一步继承Initialize函数,这是分析器的入口函数,通过注册一个回调来进行诊断。例如context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);其中AnalyzeSymbol是要实现的检查的回调函数,SymbolKind.Method是检测类型,后面再可视化视图中可以看到节点类型。每次启动的时候会调用一次Initialize。

当每次在VS中键入代码的时候,都会调用注册的回调函数AnalyzeSymbol(用户可以自己定义)。当检测发现问题的时候则调用context.ReportDiagnostic(diagnostic)方法,抛出一个警告。参数diagnostic可以设置警告的标签,警告的内容,行号,类名等等。用户可以通过context获取节点的一些信息,例如函数名是否是OnGUI,如果不是则返回。如果是继续判断该函数的类是否是Unity的MonoBehaviour类的派生类。代码如下

DiagnosticDescriptors是一个静态类,它的作用就是创建一些静态的DiagnosticDescriptor,用来设置标签和内容等,字符串等放在ResourceManager中,然后添加到ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics。

选择View->Other Windows->SyntaxTree,可以打开语法分析窗口,将鼠标放在代码的某一行上可以得到语法树的可视化信息。如下图:

通过在编辑器中选择代码,您可以看到树中的相关节点,反之亦然。还可以在语法树中右键单击“View Directed Syntax Graphic”生成一个图,可视化所选节点的树结构,可以看到语法令牌、各个词、数字和符号等。例如Initialize函数,如图 4 中所示。

如果右键选择“View Symbol”,可以在下面的属性网格将显示所调用方法的方法符号信息,可以看到Initialize的调用是UnityEngineAnalyzer.AOT.TypeGetTypeAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext),如下图:

打开DoNotUseOnGUIResources.resx文件,可以设置在错误列表中向用户显示标题,描述等。使用者还可以使用 #pragma 指令禁止该诊断的某个测试。,如下图:

还可以通过设置defaultSeverity:DiagnosticSeverity.Info设置将要生成的诊断的严重程度,例如Info,Warning,Error等。如果 改为Error在VS中则会中断编译。另外也可以定义是否在默认情况下关闭或打开,可以选择加入部分或者全部规则。例如:

SupportedDiagnostics 属性可以添加单个或多个诊断。一般情况一个分析器应该只生成一种诊断,但有的时候一个分析会区分在不同情况下抛出不同的诊断,例如DoNotUseFindMethodsInUpdate和DoNotUseFindMethodsInUpdateRecursive。

除了可以发现编码中的错误,另外还可以编写代码来提供修改代码的方案。这个暂时还没有尝试。

附上其他的一些Register的方法。

UnityEngineAnalyzer的总结

  • 静态代码分析不能代替性能分析和Review等人工检查,但可以节省大量时间和将部分过程自动化流程化
  • 可以从性能分析中得到优化点不断添加到新的分析器中
  • 只能优化代码
  • 可根据需求或者Unity版本升级,随时修改Analyzer
  • 配合项目的0Error和0warning计划极大程度提高代码稳定性
  • Mac下还不支持,还不能集成到Unity编辑器中
  • 不同平台制定不同的优化策略。例如IL2cpp的反虚拟化优化,减少虚函数的调用开销


如何获得UnityEngineAnalyzer

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引