Xamarin.Mac 的工作原理

大多数情况下,开发人员不必担心 Xamarin.Mac 的内部“神奇”,但是,大致了解后台工作原理将有助于使用 C# 镜头解释现有文档,并在问题出现时进行调试。

在 Xamarin.Mac 中,应用程序会桥接两个世界:有一个基于的 Objective-C 运行时,其中包含本机类 (NSStringNSApplication等) 的实例,而 C# 运行时包含托管类 (System.StringHttpClient、 等) 实例。 在这两个世界之间,Xamarin.Mac 会创建一个双向桥,以便应用可以调用) ((如 NSApplication.Init) )中Objective-C的方法 (选择器,并且可以Objective-C (应用委托) 上的方法一样调用应用的 C# 方法。 通常,通过 P/Invoke 以透明方式处理对 的Objective-C调用,Xamarin 提供的一些运行时代码。

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

但是,若要 Objective-C 回调应用的 C# 对象,需要以可以理解的方式 Objective-C 公开这些对象。 这通过 RegisterExport 属性完成。 请参见以下示例:

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

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

在此示例中,Objective-C运行时现在将知道名为 MyClass 的类,该类具有名为 和 runinit选择器。

在大多数情况下,这是开发人员可以忽略的实现细节,因为应用接收的大多数回调将通过类 ((如 AppDelegate、、 DelegatesDataSources) 或传递到 API 的操作上)重写的方法base。 在所有这些情况下, Export C# 代码中不需要属性。

构造函数运行

在许多情况下,开发人员需要向运行时公开应用的 C# 类构造 API Objective-C ,以便可以从诸如在 Storyboard 或 XIB 文件中调用时等位置实例化该 API。 下面是 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)
{
}

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

内存管理和周期

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

提前编译

通常,.NET 应用程序在生成时不会编译为计算机代码,而是编译到称为 IL 代码的中间层,该层在启动应用时获取 实时 (JIT) 编译为计算机代码。

单声道运行时 JIT 编译此计算机代码所需的时间可能会使 Xamarin.Mac 应用的启动速度减慢多达 20%,因为生成必要的计算机代码需要时间。

由于 Apple 对 iOS 施加的限制,Xamarin.iOS 无法使用 IL 代码的 JIT 编译。 因此,所有 Xamarin.iOS 应用都是完整的 提前 (AOT) 在生成周期内编译为计算机代码。

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

AOT 可在两个主要方面帮助 Xamarin.Mac 应用:

  • 更好的“本机”崩溃日志 - 如果 Xamarin.Mac 应用程序在本机代码中崩溃,这在对 Cocoa API 进行无效调用 ((如将 发送到 null 不接受它的方法) )时很常见,则很难分析具有 JIT 帧的本机崩溃日志。 由于 JIT 帧没有调试信息,因此将有多个具有十六进制偏移量的行,并且不知道发生了什么情况。 AOT 生成“真实”命名帧,并且跟踪更易于阅读。 这也意味着 Xamarin.Mac 应用将与 lldbInstruments 等本机工具更好地交互。
  • 更好的启动时间性能 - 对于具有多个第二次启动时间的大型 Xamarin.Mac 应用程序,JIT 编译所有代码可能需要很长时间。 AOT 会提前完成此操作。

启用 AOT 编译

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

将 AOT 添加到其他 mmp 参数

重要

启用 AOT 编译可显著增加生成时间,有时长达数分钟,但应用启动时间平均可缩短 20%。 因此,应仅在 Xamarin.Mac 应用的 发布 版本上启用 AOT 编译。

Aot 编译选项

在 Xamarin.Mac 应用上启用 AOT 编译时,可以调整几个不同的选项:

  • none - 无 AOT 编译。 这是默认设置。
  • all - AOT 编译 MonoBundle 中的每个程序集。
  • core - AOT 编译 Xamarin.MacSystemmscorlib 程序集。
  • sdk - AOT (BCL) 程序集编译 Xamarin.Mac 和 基类库。
  • |hybrid - 将此添加到上述选项之一可启用混合 AOT,这允许 IL 去除,但会导致编译时间更长。
  • + - 包含用于 AOT 编译的单个文件。
  • - - 从 AOT 编译中删除单个文件。

例如, --aot:all,-MyAssembly.dll 将在 MonoBundle 中的所有程序集上启用 AOT 编译,MyAssembly.dll--aot:core|hybrid,+MyOtherAssembly.dll,-mscorlib.dll启用混合,代码 AOT 包括 MyOtherAssembly.dll 并排除 。mscorlib.dll

部分静态 registrar

开发 Xamarin.Mac 应用时,尽量减少完成更改和测试之间的时间,满足开发截止时间可能很重要。 代码库模块化和单元测试等策略有助于减少编译时间,因为它们减少了应用需要花费大量成本的完全重新生成次数。

此外,Xamarin.iOS) 率先推出的 部分静态 Registrar (是 Xamarin.Mac 的新手,可以显著缩短 调试 配置中 Xamarin.Mac 应用的启动时间。 了解使用部分静态 Registrar 如何挤压调试启动中几乎 5 倍的改进,需要一些背景知识,了解 什么是 registrar 静态和动态之间的区别,以及此“部分静态”版本的作用。

关于 registrar

在任何 Xamarin.Mac 应用程序的幕后,Apple 的 Cocoa 框架和运行时都 Objective-C 位于底层。 在 C# 的“本机世界”和“托管世界”之间建立桥梁是 Xamarin.Mac 的主要责任。 此任务的一部分由 registrar(在 方法内 NSApplication.Init () 执行)处理。 这是在 Xamarin.Mac 中使用 Cocoa API 需要 NSApplication.Init 先调用的一个原因。

registrar的工作是通知Objective-C运行时是否存在派生自 、、 NSWindowNSObjectNSApplicationDelegateNSView类的应用 C# 类。 这需要扫描应用中的所有类型,以确定需要注册的内容以及每种类型上要报告的元素。

此扫描可以在应用程序启动时使用反射 动态完成,也可以 静态地作为生成时间步骤完成。 选择注册类型时,开发人员应注意以下事项:

  • 静态注册可以大幅缩短启动时间,但会显著缩短生成时间, (调试生成时间通常) 两倍以上。 这是 发布 配置版本的默认值。
  • 动态注册会延迟此工作,直到应用程序启动并跳过代码生成,但此附加工作可能会创建明显的暂停 (至少两秒钟) 应用程序启动。 这在调试配置版本中尤其明显,后者默认为动态注册,其反射速度较慢。

Xamarin.iOS 8.13 中首次引入的部分静态注册为开发人员提供了这两个选项的最佳功能。 通过预先计算中 Xamarin.Mac.dll 每个元素的注册信息,并将此信息与 Xamarin.Mac 一起传送到静态库 ((只需在生成时) 链接到),Microsoft 删除了动态 registrar 的大部分反射时间,同时不影响生成时间。

启用部分静态 registrar

通过在 Xamarin.Mac 中双击“项目名称”,导航到 解决方案资源管理器“Mac 生成”并添加到--registrar:static其他 mmp 参数:”字段,即可在 Xamarin.Mac 中启用部分静态Registrar。 例如:

将部分静态 registrar 添加到其他 mmp 参数

其他资源

下面是一些有关内部工作原理的更详细说明: