Xamarin.Mac 的工作原理

大多数时候,开发人员永远都不需要担心 Xamarin.Mac 的内部“魔法”,但是大致了解其后台工作原理有助于利用 C# 可重用功能区,诠释现有文档以及调试可能出现问题。

在 Xamarin.Mac 中,应用程序可以连接起两个世界:基于 Objective-C 的运行时,包含本机类实例(NSStringNSApplication 等),以及 C# 运行时,包含托管类实例(System.StringHttpClient 等)。 Xamarin.Mac 在这两个世界之间构筑起一座双向桥梁,以便应用可以调用 Objective-C 中的方法(选择器),例如 NSApplication.Init,而 Objective-C 可以回调该应用的 C# 方法(例如用于应用委托的方法)。 一般而言,Objective-C 中的调用通过 P/Invokes 以及 Xamarin 提供的某些运行时代码,以透明方式处理。

向 Objective-C 公开 C# 类/方法

不过,要想 Objective-C 回调到应用的 C# 对象,就需要以 Objective-C 可以理解的方式公开。 这要借助 RegisterExport 属性完成。 请参见以下示例:

[Register ("MyClass")]
public class MyClass : NSObject
{
   [Export ("init")]
   public MyClass ()
   {
   }

   [Export ("run")]
   public void Run ()
   {
   }
}

在本例中,Objective-C 运行时现在已经知道通过选择器 initrun,调用了类 MyClass

大多数情况下,开发人员可以忽略这些详细的实施信息,因为应用接收到的大多数回调要么通过对 base 类重写操作(例如AppDelegateDelegatesDataSources),要么通过动作传输到 API。 不论是其中哪种情况, C# 代码中不需要 Export 属性。

构造函数浏览

很多情况下,开发人员需要向 Objective-C 运行时公开应用的 C# 类构造 API,以便后者可以从 Storyboard 或 XIB 文件等位置进行调用以实现本地的实例化。 以下是 Xamarin.Mac 应用中使用的五个常见构造函数:

// Called when created from unmanaged code
public CustomView (IntPtr handle) : base (handle)
{
   Initialize ();
}

// Called when created directly from a XIB file
[Export ("initWithCoder:")]
public CustomView (NSCoder coder) : base (coder)
{
   Initialize ();
}

// Called from C# to instance NSView with a Frame (initWithFrame)
public CustomView (CGRect frame) : base (frame)
{
}

// Called from C# to instance NSView without setting the frame (init)
public CustomView () : base ()
{
}

// This is a special case constructor that you call on a derived class when the derived called has an [Export] constructor.
// For example, if you call init on NSString then you don’t want to call init on NSObject.
public CustomView () : base (NSObjectFlag.Empty)
{
}

通常,开发人员应保留创建某些类型(如自定义 NSViews)时生成的构造函数 IntPtrNSCoder。 如果 Xamarin.Mac 需要调用其中一个构造函数以响应 Objective-C 运行时请求,而你已经删除该构造函数,那么该应用会在本机代码中崩溃,而且可能很难准确找出这个问题。

内存管理和周期

Xamarin.Mac 中的内存管理方式在许多方面与 Xamarin.iOS 非常相似。 这同样是一个复杂的主题,超出了本文的讨论范围。 请阅读内存和性能最佳做法

提前编译

通常,.NET 应用程序在构建时不会编译为计算机代码,而是编译为名为 IL 代码的中间层,后者可以在应用启动时,将实时 (JIT) 编译为计算机代码。

mono 运行时以 JIT 方式编译此计算机代码所需的时间可能会使 Xamarin.Mac 应用的启动速度下降高达 20%,因为生成必要的计算机代码需要时间。

由于 Apple 对 iOS 施加的限制,IL 代码的 JIT 编译不适用于 Xamarin.iOS。 因此,所有 Xamarin.iOS 应用在生成周期内均会对计算机代码进行完整的提前 (AOT) 编译。

Xamarin.Mac 的一项新增功能是能够在应用生成周期内对 IL 代码进行 AOT,就像 Xamarin.iOS 一样。 Xamarin.Mac 使用混合 AOT 方式编译所需的大部分计算机代码,但允许运行时编译所需的 trampolines,还能灵活地继续支持 Reflection.Emit(以及当前可以在 Xamarin.Mac 上运行的其他用例)。

AOT 可以在两个主要领域帮助 Xamarin.Mac 应用:

  • 更好的“本机”故障日志 - 如果 Xamarin.Mac 应用程序在本机代码中崩溃,通常在对 Cocoa API 进行无效调用时(例如将 null 发送到不接受它的方法中)经常会出现这种情况,则具有 JIT 帧的本机故障日志难以分析。 由于 JIT 帧没有调试信息,因此会有多行代码存在十六进制偏移,因此根本无法知道出了什么问题。 AOT 会生成名为“real”的帧,因此更容易读取追踪。 这也意味着 Xamarin.Mac 应用能够更好地与本机工具(如 lldbInstruments)交互。
  • 更好的启动时间性能 - 对于需要较长启动时间的大型 Xamarin.Mac 应用程序,使用 JIT 编译所有代码可能需要大量时间。 AOT 会提前完成此项工作。

启用 AOT 编译

在 Xamarin.Mac 中,如需启用 AOT,只需双击解决方案资源管理器中的项目名称,导航到 Mac Build 并将 --aot:[options] 添加到其他 mmp参数:字段([options] 是用于控制 AOT 类型的一个或多个选项,请参阅下文)。 例如:

Adding AOT to additional mmp arguments

重要

启用 AOT 编译会显著增加生成时间,有时可能多达几分钟,但它可以令应用启动时间平均缩短 20%。 因此,建议仅在发行版的 Xamarin.Mac 应用上启用 AOT 编译。

AOT 编译选项

在 Xamarin.Mac 应用上启用 AOT 编译时,有多个不同选项可供选择:

  • none - 不进行 AOT 编译。 这是默认设置。
  • all - AOT 编译 MonoBundle 中的每个程序集。
  • core - AOT 编译 Xamarin.MacSystemmscorlib 程序集。
  • sdk - AOT 编译 Xamarin.Mac 和基类库 (BCL) 程序集。
  • |hybrid - 将此添加到上述任一选项,就会启用支持 IL 剥离 的混合 AOT,但会导致编译时间延长。
  • + - 将单个文件纳入 AOT 编译。
  • - - 从 AOT 编译中删除单个文件。

例如,--aot:all,-MyAssembly.dll 会对 MonoBundle 中的所有程序集启用 AOT 编译,但不包括MyAssembly.dll,而 --aot:core|hybrid,+MyOtherAssembly.dll,-mscorlib.dll 会启用混合代码 AOT,包括 MyOtherAssembly.dll,但不包括 mscorlib.dll

Partial Static registrar

开发 Xamarin.Mac 应用时,尽量缩短完成更改和进行测试的间隔时间,这对于满足开发截止时间非常重要。 代码库和单元测试的模块化等策略有助于缩短编译时间,因为它们可以减少重新生成应用所需的昂贵成本和大量时间。

此外,Partial Static Registrar,作为 Xamarin.Mac 的新增功能(在 Xamarin.iOS 上率先推出),可以大幅减少 Xamarin.Mac 应用在调试配置时的启动时间。 要想了解为何使用 Partial Static Registrar 可以在启动调试中获得接近 5 倍的提升,需要首先了解一些背景知识,例如,什么是 registrar,静态和动态的区别,以及此“Partial Static”版本的作用。

关于 registrar

揭开任何 Xamarin.Mac 应用程序的外表,你会发现它们都基于 Apple 和 Objective-C 运行时的 Cocoa 框架。 Xamarin.Mac 的主要作用是在这个“本机世界”与 C# 的“托管世界”之间建起桥梁。 此项任务的部分工作由 registrar 完成,并在 NSApplication.Init () 方法内部执行。 正因此,在 Xamarin.Mac 内部使用 Cocoa API 需要首先调用 NSApplication.Init

registrar 的任务是通知 Objective-C 运行时该应用是否存在派生自类 NSApplicationDelegateNSViewNSWindowNSObject 的 C# 类。 这需要扫描应用中的所有类型,以确定需要注册的内容以及需要报告每个类型的哪些元素。

这种扫描可以在应用程序启动时,通过反射方式动态进行,也可以作为生成时间步骤静态进行。 选择注册类型时,开发人员应注意以下事项:

  • 静态注册可以大幅缩短启动时间,但可能会显著降延长编译时间(通常是调试生成时间的两倍以上)。 这是发行版配置版本的默认设置.
  • 动态注册会延迟这项工作,直至应用程序启动并跳过代码生成,但这项额外工作会导致应用程序在启动时出现明显的暂停(至少持续两秒钟)。 这在调试配置版本中尤其明显,后者默认采取动态注册,因此反射速度较慢。

Partial Static 注册,最早出现在 Xamarin.iOS 8.13 中,允许开发人员在两种方案中选择更好的一个。 通过提前计算每个元素在 Xamarin.Mac.dll 中的注册信息,并通过静态库(仅需在编译时关联)将此信息连同 Xamarin.Mac 一起寄送,Microsoft 已经消除动态 registrar 的大部分反射时间,且不会影响生成时间。

启用 Partial Static registrar

在 Xamarin.Mac 中,如需启用 Partial Static Registrar,只需双击解决方案资源管理器中的项目名称,导航到 Mac Build 并将 --registrar:static 添加到其他 mmp 参数:字段。 例如:

Adding the partial static registrar to additional mmp arguments

其他资源

以下是其内部工作原理的进一步解释说明: