zenJect

Zenject框架(十五)

2019-04-03  本文已影响0人  虫小白

使用Zenject创建编辑器窗口

如果您需要添加自己的Unity插件,并且想要创建自己的EditorWindow派生类,那么您可以考虑使用Zenject来帮助管理此代码。 让我们来看一个如何做到这一点的例子:

  1. 在Project面板中右击Editor文件夹,选择Create -> Zenject -> Editor Window,命名为TimerWindow
  2. 在菜单栏中选择Window -> TimerWindow打开新建的窗口
  3. 窗口是空的,现在我们为窗口添加一些内容,打开TimerWindow脚本,输入以下内容:
public class TimerWindow : ZenjectEditorWindow
{
    TimerController.State _timerState = new TimerController.State();

    [MenuItem("Window/TimerWindow")]
    public static TimerWindow GetOrCreateWindow()
    {
        var window = EditorWindow.GetWindow<TimerWindow>();
        window.titleContent = new GUIContent("TimerWindow");
        return window;
    }

    public override void InstallBindings()
    {
        Container.BindInstance(_timerState);
        Container.BindInterfacesTo<TimerController>().AsSingle();
    }
}

class TimerController : IGuiRenderable, ITickable, IInitializable
{
    readonly State _state;

    public TimerController(State state)
    {
        _state = state;
    }

    public void Initialize()
    {
        Debug.Log("TimerController initialized");
    }

    public void GuiRender()
    {
        GUI.Label(new Rect(25, 25, 200, 200), "Tick Count: " + _state.TickCount);

        if (GUI.Button(new Rect(25, 50, 200, 50), "Restart"))
        {
            _state.TickCount = 0;
        }
    }

    public void Tick()
    {
        _state.TickCount++;
    }

    [Serializable]
    public class State
    {
        public int TickCount;
    }
}

在ZenjectEditorWindow的InstallBindings方法中,您可以像在场景中一样添加IInitializable,ITickable和IDisposable绑定。还有一个名为IGuiRenderable的新界面,您可以使用Unity的即时模式gui将内容绘制到窗口中。

请注意,每次在Unity重新编译代码时,都会重新加载编辑器窗口。再次调用InstallBindings,从头开始再次创建所有类。这意味着已经存储在成员变量中的任何状态信息都将被重置。但是,EditorWindow派生类本身的成员字段是可序列化的,因此您可以利用此功能使状态在重新编译时保持不变。在上面的示例中,我们可以将当前的计数关联到可序列化的类上并将其作为EditorWindow中的成员来保持当前的计数。

需要注意的是,ITickable.Tick方法被触发的速率可能会根据焦点的位置而改变。如果你运行计时器窗口,然后选择Unity外的另一个窗口,你就可以明白我的意思。(计数增加得慢得多)

优化建议

  1. 使用具有初始大小的内存池。 这应该将所有昂贵的实例化操作限制为场景启动,并允许您在游戏开始后避免任何性能峰值。 或者,如果您想要非常彻底,可以使用固定大小,这会在达到池大小限制时触发异常。

  2. 使用反射烘焙。 这通常只是启用它并忘记它的问题,并且可以消除在场景启动期间运行zenject代码所花费的时间的45%。

  3. 使用Unity的Profiler。 当打开Unity的Profiler时,Zenject会自动为所有常见的zenject接口操作添加分析样本,包括IInitializable.Initialize,ITickable.Tick,IDisposable.Dispose等,其方式类似于unity为所有MonoBehaviour方法自动执行此操作。 因此,如果您实现了ITickable,那么您应该在Profiler中看到Foo.Tick,其中Foo是您的一个类。、

当人们开始分析使用Zenject的项目时一个常见的问题是很难区分Zenject和他们自己的代码花费的时间。因此,很容易把问题归咎到Zenject上。尤其是看到和场景启动相关的花费,因为SceneContext.Awake方法似乎占用了很多帧。 SceneContext.Awake是触发安装器的的地方,也是构建整个对象图的地方,因此包含了zenject代码和用户代码的组合。

为了帮助确定zenject的初始与您自己的代码对性能影响的对比,,您可以在Player Setting中启用定义ZEN_INTERNAL_PROFILING。 执行此操作后,如果您运行场景,您应该看到调用SceneContext.Awake时产生的所有成本的更详细的细分。 启用此标志并运行场景后,配置文件输出应显示在控制台中,如下所示:

SceneContext.Awake detailed profiling: Total time tracked: 3104.19 ms.  Details:
  67.2% (02960x) (2086 ms) User Code
  19.4% (01243x) (0602 ms) Type Analysis - Direct Reflection
  06.1% (02928x) (0189 ms) DiContainer.Resolve
  02.3% (00003x) (0071 ms) Other
  02.3% (00259x) (0071 ms) DiContainer.Bind
  01.3% (01112x) (0041 ms) DiContainer.Instantiate
  00.7% (01032x) (0023 ms) Searching Hierarchy
  00.4% (01243x) (0011 ms) Type Analysis - Calling Baked Reflection Getter
  00.3% (01852x) (0010 ms) DiContainer.Inject

或者,如果启用反射烘焙,那么应该消除“直接反射”成本,它应该更像这样:

SceneContext.Awake detailed profiling: Total time tracked: 2357.43 ms.  Details:
  79.1% (02964x) (1865 ms) User Code
  06.4% (02928x) (0151 ms) DiContainer.Resolve
  06.2% (01243x) (0145 ms) Type Analysis - Calling Baked Reflection Getter
  02.8% (00003x) (0067 ms) Other
  02.4% (00259x) (0057 ms) DiContainer.Bind
  01.5% (01112x) (0034 ms) DiContainer.Instantiate
  00.8% (01852x) (0020 ms) DiContainer.Inject
  00.8% (01032x) (0018 ms) Searching Hierarchy

如您所见,在该例中,通过调用SceneContext.Awake方法产生的79%的花费与我们的游戏代码(此处标记为User Code)而不是zenject相关。

请注意,当不使用反射烘焙时,与“Direct Reflection”相关的花费应该主要仅在启动时发生。 这是因为在第一次产生这些花费之后,结果将被缓存。

通过在构建设置中定义ZEN_STRIP_ASSERTS_IN_BUILDS,您还可以在速度和内存分配的轻微减少方面获得微小的收益。 这将导致所有断言从构建中剥离。 但请注意,通过执行此操作,调试构建中任何与zenject相关的错误将变得更加困难。

有关Zenject与其他DI框架的一些基准测试,请参见后续Ioc Performance章节(特别参见底部的图表)。

反射烘焙(Reflection Baking)

从Zenject中挤出额外性能的一种简单方法是启用一个名为Reflection Baking的功能。该功能可以将一些在代码库中分析类型(也称为反射)的花费从运行时移动到构建时。在我们的一个产品Modest Tree中,打开Reflection Baking会使zenject启动时间缩短了45%(节省了大约424毫秒)。根据使用的类型和目标平台,结果会因项目而异,但通常会很明显。

反射烘焙还将减少实例化新对象所需的时间。 在IL2CPP平台上尤其如此,因为在该平台上受到限制,通过反射实例化通常较慢。

要在项目中启用该功能,只需在Project面板中右击,然后选择Create -> Zenject -> Reflection Baking Settings。 现在,如果再次构建项目,Zenject内部的反射成本应该基本消除。

默认情况下,反射烘焙将修改项目中生成的所有程序集。 包括Unity生成并放置在Library / ScriptAssemblies文件夹中的所有程序集,但不包括放置在Assets目录下的程序集(但是您也可以在该处使用反射烘焙作为单独的步骤)

在许多情况下,您想要限制在哪些区域应用反射烘焙。您可以通过选中反射烘焙设置对象,取消选中All Generated Assemblies标志,然后将要使用的程序及显式添加到Include Assemblies属性来执行此操作。或者,可以将All Generated Assemblies标志设置保留为“true”,然后通过向“Namespace Patterns”字段添加一个或多个正则表达式以及更改Exclude Assemblies属性来限制更改哪些程序集。例如,如果您的所有游戏代码都位于ModestTree.SpaceFighter命名空间下,那么您可以通过添加^ ModestTree.SpaceFighter作为命名空间模式来确保反射烘焙仅应用于该名称空间。请注意,zenject将自动为其自身添加命名空间模式,因此您无需执行此操作(但是,如果未选中All Generated Assemblies,则必须将zenject程序集添加到Include Assemblies中)

默认情况下,反射烘焙仅适用于构建,在Unity编辑器内部进行测试时不会生效。如果要暂时禁用烘焙,可以在Inspectior面板中取消选中Is Enabled In Builds。您还可以通过选中Is Enabled In Builds强制在Unity编辑器中应用烘焙。但请注意,这会增长编译时间,因此可能得不偿失,但在分析查看烘焙效果时可能很有用。另请注意,如果您使用的是Unity 2017 LTS,则由于Unity API限制,反射烘焙只能在构建时使用。

烘焙外部库(Baking External DLLs)

当通过添加反射烘焙设置对象使用反射烘焙时,仅会将反射烘焙应用于直接放入Unity项目的C#文件。 如果您正在使用外部dll,并且想要在其中应用反射烘焙,那么您可以通过添加构建后步骤来实现。 您可以通过打开zenject \ NonUnityBuild \ Zenject.sln文件并构建“Zenject-ReflectionBakingCommandLine”项目,在github仓库中找到命令行工具。

原理(Under the hood)

Reflection Baking使用名为Cecil的库在生成的程序集上进行IL编织。 这意味着在Unity为项目中的源文件生成DLL之后,Zenject将直接编辑这些DLL以将zenject操作直接嵌入到您的类中。 通常,zenject必须迭代类的每个字段,属性,方法和构造函数,以找到需要注入的内容,这就是产生反射成本的地方。 但是,一旦类应用了反射烘焙,那么zenject只需要在类上调用静态方法来检索它需要的所有信息,这会快得多。

Coverage设置(Coverage Settings)

在工程上下文中有两个和烘焙反射有关的设置我们需要了解:Editor Reflection Baking Coverage模式和Builds Reflection Baking Coverage模式。当遇到没有烘焙反射信息的类型时,这些值将决定zenject应该使用什么模式(当在Unity编辑器中时使用“Editor”模式,在生成的程序中使用“Builds”模式)。可以是以下值:

  1. Fallback To Direct Reflection - 使zenject在找不到烘焙信息时产生必要的反射操作。这是默认值。
  2. No Check Assume Full Coverage - 设置此值后,如果找不到给定类型的反射烘焙信息,则Zenject将假定该类型不包含任何反射信息。如果有很多第三方代码没有应用反射烘焙,也不以任何方式使用Zenject,那么这种方法非常有用。当Coverage模式设置为(1)时,这可能会很耗能,因为Zenject仍将使用反射操作分析第三方代码。请注意,设置此选项后,您将需要确保反射烘焙始终应用于使用Zenject的所有地方。
  3. Fallback To Direct Reflection With Warning - 设置此值后,当Zenject遇到未应用反射烘焙的类型时,它将使用耗能的反射操作,但也会发出警告。 如果您的目的是通过反射烘焙获得完全覆盖,但是您不希望使用模式(2)并且在烘焙过程中遗漏某些类型时导致事情完全中断,这将非常有用

Zenject 6升级指南(Upgrade Guide for Zenject 6)

上一篇 下一篇

猜你喜欢

热点阅读