How Xamarin.Mac works
Most of the time the developer will never have to worry about the internal "magic" of Xamarin.Mac, however, having a rough understanding of how things works under the hood will help in both interpreting existing documentation with a C# lens and debugging issues when they arise.
In Xamarin.Mac, an application bridges two worlds: There is the Objective-C based runtime containing instances of native classes (NSString
, NSApplication
, etc) and there is the C# runtime containing instances of managed classes (System.String
, HttpClient
, etc). In between these two worlds, Xamarin.Mac creates a two way bridge so an app can call methods (selectors) in Objective-C (such as NSApplication.Init
) and Objective-C can call the app's C# methods back (like methods on an app delegate). In general, calls into Objective-C are handled transparently via P/Invokes and some runtime code Xamarin provides.
However, for Objective-C to call back into an app's C# objects, they need to be exposed in a way that Objective-C can understand. This is done via the Register
and Export
attributes. Take the following example:
[Register ("MyClass")]
public class MyClass : NSObject
{
[Export ("init")]
public MyClass ()
{
}
[Export ("run")]
public void Run ()
{
}
}
In this example, the Objective-C runtime will now know about a class called MyClass
with selectors called init
and run
.
In most cases, this is an implementation detail that the developer can ignore, as most callbacks an app receives will be either via overridden methods on base
classes (such as AppDelegate
, Delegates
, DataSources
) or on Actions passed into APIs. In all of those cases, Export
attributes are not necessary in the C# code.
In many cases, the developer will need to expose the app's C# classes construction API to the Objective-C runtime so it can be instantiated from places such as when called in Storyboard or XIB files. Here are the five most common constructors used in Xamarin.Mac apps:
// 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)
{
}
In general, the developer should leave the IntPtr
and NSCoder
constructors that are generated when creating some types such as custom NSViews
alone. If Xamarin.Mac needs to call one of these constructors in response to an Objective-C runtime request and you’ve removed it, the app will crash inside native code and it may be difficult to figure out exactly the issue.
Memory management in Xamarin.Mac is in many ways very similar to Xamarin.iOS. It also is a complex topic, one beyond the scope of this document. Please read the Memory and Performance Best Practices.
Typically, .NET applications do not compile down to machine code when they are built, instead they compile to an intermediate layer called IL code that gets Just-In-Time (JIT) compiled to machine code when the app is launched.
The time that it takes the mono runtime to JIT compile this machine code can slow the launch of a Xamarin.Mac app by up to 20%, as it takes time for the necessary machine code to be generated.
Because of limitations imposed by Apple on iOS, JIT compilation of the IL code is not available to Xamarin.iOS. As a result, all Xamarin.iOS app are full Ahead-Of-Time (AOT) compiled to machine code during the build cycle.
New to Xamarin.Mac is the ability to AOT the IL code during the app build cycle, just like Xamarin.iOS can. Xamarin.Mac uses a Hybrid AOT approach that compiles a majority of the needed machine code, but allows the runtime to compile needed trampolines and the flexibility to continue to support Reflection.Emit (and other use cases that currently work on Xamarin.Mac).
There are two major areas where AOT can help a Xamarin.Mac app:
- Better "native" crash logs - If a Xamarin.Mac application crashes in native code, which is common occurrence when making invalid calls into Cocoa APIs (such as sending a
null
into a method that doesn't accept it), native crash logs with JIT frames are difficult to analyze. Since the JIT frames do not have debug information, there will be multiple lines with hex offsets and no clue what was going on. AOT generates "real" named frames and the traces are much easier to read. This also means Xamarin.Mac app will interact better with native tools such as lldb and Instruments. - Better launch time performance - For large Xamarin.Mac applications, with a multiple second startup time, JIT compiling all of the code can take a significant amount of time. AOT does this work up front.
AOT is enabled in Xamarin.Mac by double-clicking the Project Name in the Solution Explorer, navigating to Mac Build and adding --aot:[options]
to the Additional mmp arguments: field (where [options]
is one or more options to control the AOT type, see below). For example:
Important
Enabling AOT compilation dramatically increases build time, sometimes up to several minutes, but it can improve app launch times by an average of 20%. As a result, AOT compilation should only be enabled on Release builds of a Xamarin.Mac app.
There are several different options that can be adjusted when enabling AOT compilation on a Xamarin.Mac app:
none
- No AOT compilation. This is the default setting.all
- AOT compiles every assembly in the MonoBundle.core
- AOT compiles theXamarin.Mac
,System
andmscorlib
assemblies.sdk
- AOT compiles theXamarin.Mac
and Base Class Libraries (BCL) assemblies.|hybrid
- Adding this to one of the above options enables hybrid AOT which allows for IL stripping, but will result in longer compile times.+
- Includes a single file for AOT compilation.-
- Removes a single file from AOT compilation.
For example, --aot:all,-MyAssembly.dll
would enable AOT compilation on all of the assemblies in the MonoBundle except MyAssembly.dll
and --aot:core|hybrid,+MyOtherAssembly.dll,-mscorlib.dll
would enable hybrid, code AOT include the MyOtherAssembly.dll
and excluding the mscorlib.dll
.
When developing a Xamarin.Mac app, minimizing the time between completing a change and testing it can become important to meeting development deadlines. Strategies such as modularization of codebases and unit tests can help to decrease compile times, as they reduce the number of times that an app will require an expensive full rebuild.
Additionally, and new to Xamarin.Mac, Partial Static Registrar (as pioneered by Xamarin.iOS) can dramatically reduce the launch times of a Xamarin.Mac app in the Debug configuration. Understanding how using the Partial Static Registrar can squeezed out an almost a 5x improvement in debug launch will take a bit of background on what the registrar is, what the difference is between static and dynamic, and what this “partial static” version does.
Under the hood of any Xamarin.Mac application lies the Cocoa framework from Apple and the Objective-C runtime. Building a bridge between this "native world” and the "managed world” of C# is the primary responsibility of Xamarin.Mac. Part of this task is handled by the registrar, which is executed inside NSApplication.Init ()
method. This is one reason that any use of Cocoa APIs in Xamarin.Mac requires NSApplication.Init
to be called first.
The registrar’s job is to inform the Objective-C runtime of the existence of the app's C# classes that derive from classes such as NSApplicationDelegate
, NSView
, NSWindow
, and NSObject
. This requires a scan of all types in the app to determine what needs registering and what elements on each type to report.
This scan can be done either dynamically, at startup of the application with reflection, or statically, as a build time step. When picking a registration type, the developer should be aware of the following:
- Static registration can drastically reduce launch times, but can slow down builds times significantly (typically more than double debug build time). This will be the default for Release configuration builds.
- Dynamic registration delays this work until application launch and skips code generation, but this additional work can create a noticeable pause (at least two seconds) in application launch . This is especially noticeable in debug configuration builds, which defaults to dynamic registration and whose reflection is slower.
Partial Static Registration, first introduced in Xamarin.iOS 8.13, gives the developer the best of both options. By pre-computing the registration information of every element in Xamarin.Mac.dll
and shipping this information with Xamarin.Mac in a static library (that only needs to be linked to at build time), Microsoft has removed most of the reflection time of the dynamic registrar while not impacting build time.
The Partial Static Registrar is enabled in Xamarin.Mac by double-clicking the Project Name in the Solution Explorer, navigating to Mac Build and adding --registrar:static
to the Additional mmp arguments: field. For example:
Here are some more detailed explanations of how things work internally: