Investing Time in the Xamarin Linker for Smaller App Sizes
When developing an app, 99% of our time goes into writing the code, testing the functionality, and making small UI tweaks to please our end users. Most of this is done in debug mode which optimizes compilation and app size for speed, which makes sense. When we flip that flag to Release with the default settings, we often pay very little attention as to what is happening to our app. We get a nice APK or IPA and ship it up to the app store and we are done. However, if we just spent a little time fine tuning our settings, we could really reduce the overall app size. I recently sat down for 1 hour and reduced Hanselman.Forms by 4 MB on Android by optimizing the linker in Full Link mode. When I flipped on Startup Tracing for comparison the app size was reduced by 10MB!
I have written many times about using & understanding the linker, however today I want to talk about how to debug the linker.
My recommendation is to flip on Link "Sdk & User Assemblies" and run your application. See if everything works or if your application crashes. Most likely, it will crash and you will see an error such as:
Getting: System.InvalidOperationException: 'A suitable constructor for type 'Shiny.Jobs.JobManager' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.'
If you see this, it means it is time to debug through.
Debug the Linker
You should NEVER have the linker on when debugging an app.... unless you are debugging the linker settings! So, turn on that linker in debug and see what happens. Like I said above, you will probably see a crash with the linker stripping away too much. That is usually because of reflection or the main project not referencing items from your .NET Standard project. Hopefully library creators have optimized their libraries to handle the linker, however if they haven't you can easily setup assembly skipping in your csproj file. Here is what it looks like to completely skip over Xamarin.Forms:
// 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;FormsViewGroup;</AndroidLinkSkip>
We can easily add assemblies into this list until the app runs. Here is what Hanselman.Forms looks like:
<AndroidLinkSkip>Xamarin.Forms.Platform.Android;Xamarin.Forms.Platform;Xamarin.Forms.Core;Xamarin.Forms.Xaml;FormsViewGroup;Shiny.Core;MediaManager;MediaManager.Forms;</AndroidLinkSkip>
Don't Skip, Inform the Linker
Have you ever wondered why you have all this those Init calls in your app's startup?
Forms.SetFlags("CarouselView_Experimental");
Forms.Init(this, bundle);
FormsMaterial.Init(this, bundle);
Xamarin.Essentials.Platform.Init(this, bundle);
CrossMediaManager.Current.Init(this);
ImageCircleRenderer.Init();
Android.Glide.Forms.Init(this);
This is to help the linker become aware that you are using this library, so don't remove it. If a library doesn't have an Init, you can easily inform the linker you are using a class that is getting linked out like this:
var pancake = typeof(PancakeView);
var circle = new CircleImage();
Depending on how complex the dependency is then you may want to link skip assemblies out since we don't have access to the source code.
Custom Preserve Flags
If you are getting exceptions in classes that are in your own code then you can add a special attribute to the class to tell the linker to preserve it.
In your shared code (usually .NET Standard) add a new file called PreserveAttribute:
public sealed class PreserveAttribute : System.Attribute
{
public bool AllMembers;
public bool Conditional;
}
Now, on any class that is getting stripped out add the following attribute:
[Preserve(AllMembers = true)]
public partial class Tweet
{
}
Keep doing this until your code is linker safe and everything is working as expected. In no time at all you will have your app ready to be fully linker optimized and the app size will be reduced :)
Cleanup Before Committing Code
When debugging linker issues, I like to have the same settings mirrored acrossed debug and release. However, leaving these settings on in debug will slow down your builds and will turn off XAML Hot Reload. So, NEVER turn on linking in debug unless you are doing this.
Read More
Now that you have an idea of how to debug and customize yoru code for the linker be sure to read the in-depth documentation for even more goodies. I would also read the following blogs on shrinking your app size, optimizing Android builds, and faster startup times with startup tracing.