Optimizing Xamarin Apps & Libraries with the Linker
The Xamarin linker is one of the most important pieces of technology when building mobile apps, that no one knows exists. It does all of the hard work for you, stripping out all unused code from libraries that you include in your apps. It should only be used in release mode as it takes extra time to compile your app, but means that your app will be smaller:
The linker has specific implementations for both iOS and Android and is completely customizable.
Linking for App Developers
Before I get to how to optimize libraries, let me go through when the linker actually kicks in and how app developers use it.
For the large majority of developers, all you need to know is turn on Link SDK Assemblies Only when your app is in Release (it is the default). This will only look to link and remove libraries that ship with Xamarin.iOS and Xamarin.Android. The other option is Link All Assemblies, which will look at every assembly and library that your app uses and attempt to link them. I always recommend Link SDK Assemblies Only as it is the safest and easiest to understand. The difference between the two in terms of application size is usually minimal and often when linking everything you can get in trouble as the linker can be aggressive and will remove more than needed, especially if you use reflection.
The linker is pretty customizable and you can tell it to ignore specific libraries in the project settings. For instance if you are building an app with Xamarin.Forms you probably want to leave all of Xamarin.Forms in there so you could add some flags to iOS and Android:
// iOS
<MtouchExtraArgs>--linkskip=Xamarin.Forms.Platform.iOS --linkskip=Xamarin.Forms.Platform --linkskip=Xamarin.Forms.Core --linkskip=Xamarin.Forms.Xaml --linkskip=Samples</MtouchExtraArgs>
// Android
<AndroidLinkSkip>Xamarin.Forms.Platform.Android;Xamarin.Forms.Platform;Xamarin.Forms.Core;Xamarin.Forms.Xaml;Samples;FormsViewGroup;</AndroidLinkSkip>
You can learn more about this on the iOS and Android documentation.
Linking for Library Creators
For library creators it is super important to understand how the linker works, and how you can optimize your libraries for app developers. You never know when a developer may flip on Link All Assemblies or when the linker may get aggressive and link away some of your library.
Preserve Everything
There are instances where you are creating a library and always want to ensure that NOTHING is ever removed regardless of the linker setting. The [Preserve (AllMembers = true)]
can be added to any class in your iOS or Android library and then the linker will ignore that class. I like to always test my library in Link ALL Assemblies mode and if something goes wrong and gets removed then I slap this on the class.
Linker Safe
Sometimes it is really important that unused code in your library is properly removed. This is where the [assembly:LinkerSafe]
attribute comes in to inform the linker to include this library in ALL linking modes. This is cool because you are telling the linker that you have done the work to test your library and want it to be treated as if it was part of Xamarin.iOS or Xamarin.Android.
A great example of this is Xamarin.Essentials which is completely optimized with this property. It means that if you only use the Compass then that will be the ONLY chunk of code that is included in the app. This library was built and structured with the linker in mind with very little overlapping code, and each feature is implemented in it's own class.
When I started to look at my other cross-platform libraries I noticed that the PermissionsPlugin was starting to cause people some submission issues on iOS. This was because the library itself checks all sorts of different permissions and since I didn't optimize it for the linker, Apple was detecting usage of the APIs and started to require the info.plist to add permissions that weren't actually being used.
So, I turned on the [assembly:LinkerSafe]
and thought everything was going to be fine... until I packaged up an app, extracted the dll from the final package and put it into dotPeek on Windows and found out that NOTHING was getting linked out:
This fuzzy photo (sorry about that) shows that even though my code was only requesting one permission, all of the code was still there. Well, let's look at the code:
public async Task<Dictionary<Permission, PermissionStatus>> RequestPermissionsAsync(params Permission[] permissions)
{
var results = new Dictionary<Permission, PermissionStatus>();
foreach (var permission in permissions)
{
if (results.ContainsKey(permission))
continue;
switch (permission)
{
case Permission.Calendar:
results.Add(permission, await RequestEventPermission(EKEntityType.Event));
break;
case Permission.Camera:
results.Add(permission, await RequestAVPermissionStatusAsync(AVMediaType.Video));
break;
case Permission.Contacts:
results.Add(permission, await RequestContactsPermission());
break;
case Permission.LocationWhenInUse:
case Permission.LocationAlways:
case Permission.Location:
results.Add(permission, await RequestLocationPermission(permission));
break;
case Permission.MediaLibrary:
results.Add(permission, await RequestMediaLibraryPermission());
break;
case Permission.Microphone:
results.Add(permission, await RequestAVPermissionStatusAsync(AVMediaType.Audio));
break;
case Permission.Photos:
results.Add(permission, await RequestPhotosPermission());
break;
case Permission.Reminders:
results.Add(permission, await RequestEventPermission(EKEntityType.Reminder));
break;
case Permission.Sensors:
results.Add(permission, await RequestSensorsPermission());
break;
case Permission.Speech:
results.Add(permission, await RequestSpeechPermission());
break;
}
if (!results.ContainsKey(permission))
results.Add(permission, PermissionStatus.Granted);
}
return results;
}
At first, you are probably thinking like me that the linker obviously knows what Permission
my app is passing in, so it would remove any case
that wasn't being used. Nope, that linker is smart, but not that smart. If I call this method at all, the linker will include any method that is being used inside of it. This means that I would need to re-structure the entire library to make it linker safe. James Clancey came up with the amazing idea of breaking the mega method into smaller individual classes that compartmentalized functionality.
This is where the BasePermission
came in:
public class BasePermission
{
protected Permission permission;
public BasePermission(Permission permission)
{
this.permission = permission;
}
public virtual Task<PermissionStatus> CheckPermissionStatusAsync() =>
#if __IOS__
throw new NotImplementedException();
#else
CrossPermissions.Current.CheckPermissionStatusAsync(permission);
#endif
public virtual async Task<PermissionStatus> RequestPermissionAsync()
{
#if __IOS__
throw new NotImplementedException();
#else
var results = await CrossPermissions.Current.RequestPermissionsAsync(permission);
if (results.ContainsKey(permission))
return results[permission];
return PermissionStatus.Unknown;
#endif
}
}
The code looks a little funny, but it has two virtual methods that on iOS have no implementation at all and must be implemented in a parent class. This means that when the linker sees the BasePermission
on iOS it wont have any implementation and will get removed. Now, all I needed to do is create classes for each of the permissions such as this:
public class CameraPermission : BasePermission
{
public CameraPermission() : base(Permission.Camera)
{
}
#if __IOS__
public override Task<PermissionStatus> CheckPermissionStatusAsync() =>
Task.FromResult(PermissionsImplementation.GetAVPermissionStatus(AVFoundation.AVMediaType.Video));
public override Task<PermissionStatus> RequestPermissionAsync() =>
PermissionsImplementation.RequestAVPermissionStatusAsync(AVFoundation.AVMediaType.Video);
#endif
}
public class ContactsPermission : BasePermission
{
public ContactsPermission() : base(Permission.Contacts)
{
}
#if __IOS__
public override Task<PermissionStatus> CheckPermissionStatusAsync() =>
Task.FromResult(PermissionsImplementation.ContactsPermissionStatus);
public override Task<PermissionStatus> RequestPermissionAsync() =>
PermissionsImplementation.RequestContactsPermission();
#endif
}
On iOS you can see that I am calling into specific implementations for each permission check and request. This now means that even though the library contains 15 different permission classes if the calling app only uses 1 of them then it will be the only code that is included and there will be no more issues with app store submissions!
The linker is pretty complex, but pretty awesome when you figure out how to optimize for it. If you are a library creator I encourage you to flip on this flag for your iOS and Android projects and give it a test drive. This will ensure that you always know if your library will work regardless of any linker setting.
Be sure to read through all the documentation I linked to and also check out [Jon Douglas' blog]9https://www.jon-douglas.com/2017/09/22/linker-analyzer/) on using the linker analyzer for Android. You can also watch me struggle on my live stream from a month back where I figured out all of this.