As mobile application developers, we’re all aware that the app bundle size (APK/IPA) is always a matter of concern. This is especially true when it comes to developing with Xamarin—large bundle sizes is a well-known drawback with Xamarin apps. They can be really heavy! But then the question is, how do you deal with this? Let’s find out!
My inspiration for writing this post came from this awesome blog by Adam Pedley on Reducing app file size in Xamarin Forms, where he has added a lot of important pointers (I’ve also picked up some of the content from his blog).
There are a few things you can do to make your app lighter when developing with Xamarin.
Xamarin applications use a linker to reduce the size of the application. The linker employs a static analysis of your application to determine which assemblies are actually used, which types are actually used, and which members are actually used.
The linker then behaves like a garbage collector, continually looking for the assemblies, types, and members that are referenced until the entire closure of referenced assemblies, types, and members is found. Then everything outside of this closure is discarded.
The primary mechanism for controlling the linker is the Linker Behavior (Linking in Visual Studio) drop-down within the Project Options dialog box. There are three options:
- Don’t Link (None in Visual Studio)
- Link SDK Assemblies (SDK Assemblies Only)
- Link All Assemblies (SDK and User Assemblies)
The Don’t Link option turns off the linker; this is useful for troubleshooting runtime failures in order to see whether or not the linker is responsible. This setting is not usually recommended for production builds.
The Link SDK Assemblies option only links assemblies that come with Xamarin. All other assemblies (such as your code) are not linked. As such, any of your third-party libraries will not be affected. This is the easiest and safest type of linking and must be your default. As the default packages that you’ll need to get started with Xamarin Forms on iOS and Android, all have linking support, so you should not have a problem.
Now you might have noticed that a lot of times when you try to use this, certain packages start to throw random errors, oftentimes about not finding the constructor of certain classes. You’ll notice that your project crashes regularly, saying it can’t find the ctor() for a specific class from a libraries namespace. This is because linking has removed that classes constructor that your code is looking for.
There are several libraries out there that aren’t built with linking support. It’s likely that these libraries must be using Reflection as part of their internal working, and so the compiler will think some of their methods and properties aren’t in use. Hence, it will decide to remove them.
There are a few ways to work around this problem. I prefer compiler link skip, which skips linking for a certain library. This has to be done in each platform in which you’re using the dll.
How to Skip Assemblies
Go to your iOS project’s properties and under the iOS build tab do the following. In the Additional mtouch arguments field, you’ll need to add the names of the framework’s DLL’s that you want to ignore, alongside a –linkskip=LibraryName. Make sure you don’t add the .dll extension and separate them by a space.
Go to your Android project’s properties and under the Android build tab do the following. In the Ignore assemblies field, type the name of the assemblies you want to ignore, separated by ; and without the .dll extension.
The Link All Assemblies option links all assemblies, which means your code may also be removed if there are no static references, as it will investigate everything and bring down your app size. When set to this type, the linking process is quite aggressive, and the chances are that the app will remove something that you actually need.
If you manage to get all your library frameworks linking successfully, then you’re 80% of the way there. For a full linking, you need to link your own code as well. Go back into the project settings and select Link all.
This setting is pretty much the same, except it opens up your own code to be linked. In order to prevent your classes from being linked, you can use the [Preserve] attribute form the Xamarin.Internals namespace. This will tell the compiler to ignore a method, property, or class.
Firstly, for any properties on your Binding Context that you’re only referencing from XAML and never in the code behind, you’ll need to add a Preserve attribute to them.
Another thing to watch out for is your dependency services. These will be linked automatically, as there is no direct reference to the code from the PCL or the native project. DependencyService.Get<IPlatformSpecificOperations>()doesn’t actually reference that class.
Finally, custom renderers—just like dependency services—aren’t directly referenced in the codebase, so you’ll have to do the same for them as well.
James Montemagno has done extensive experimentation on this, and in his blog here you can see how he’s shown his bike apps example:
I wouldn’t suggest you turn the linking on during development, as it has a substantial impact on build time. Only do it when you plan on releasing your application and always have buffer time in your bucket as this usually causes issues, and it can take a little time to understand what’s causing it in the first place. And always do a 100% test after you release with these settings.
Removing unwanted packages
NuGet packages or .dll’s, in general, are known to substantially increase the build size. Sometimes it’s easy to be neglectful of this (at least I am guilty here), and then at a certain point in your project, it becomes confusing whether or not a particular package is even in use or not.
Recently I saw a question on StackOverflow, where the questioner had the following already done:
- Proguard enabled
- Configuration: Release
- Platform: Active (Any CPU).
- Enable Multi-Dex: true
- Enable developer instrumentation (debugging and profiling): false
- Linking: SDK and User Assemblies (Tried SDK assemblies only also)
- Supported architectures: Selected all
So the only possible thing that came to my mind at that point was that the person was neglecting the unused packages, and in the end, removing the unwanted images and assemblies is what solved his issue.
Doing this brought down his APK size from 80MB’s to 21MB’s—that’s almost a 300% decrease in bundle size.
If you’re wondering how to find unused packages in Visual Studio IDE, you can check out this StackOverflow question.
If you have a lot of high-quality images in your app, you can easily compress them without losing any quality. Go to any image compression website (I use TinyPng, as everyone suggests it), to compress PNG or JPEG files.
I’ve seen a drastic image file size reduction where sometimes the size 50% or more without any quality loss. It won’t help with memory usage but could cut off MB’s on your app package file size. Using SVG instead, is, of course, another option, if you’re just starting to build your app. But SVG’s aren’t always a good option—for example when your images are very vivid and have tiny and important details in them.
App Thinning in iOS
If you enable Bitcode, this option is on by default—you enable iOS to apply its app thinning process when you submit it to the App Store. As such, only the relevant code for each architecture will be sent to the user’s device when they download your app.
AOT in Android
If you use AOT in your app to speed up the start times and performance of your app, you’ll notice that the file size increases quite a bit.
Android AOT Additional Arguments
Thanks to the Adam Pedley post that I linked to above, I actually found these interesting arguments you can pass to the AOT compiler to reduce your binary size further.
While I haven’t seen what happens to a stack trace, in any crash report, I can at least confirm they do reduce your file size. As a precaution, I would ensure your analytics and logging are well written to capture as much data as possible—in other words, don’t rely on the stack trace. For your (quick) reference, you can add both of these to your Android csproj with the following:
In the resulting AOT, normally full method names are included in the resulting *.so files. You can stop this by passing the no-write-symbols attribute.
Debug information may also be coming out in the final AOT binary—this option turns it off.
ProGuard is normally known as a code obfuscator, meaning that it will try to change the method names and code structure, if possible, to make it very hard for people to reverse engineer your code.
I personally don’t believe in obfuscation, as it only slows reverse engineering down—it never prevents it from happening. But in addition to this, ProGuard also optimizes and minimizes your code, resulting in additional file size reductions. I am always a sucker for a reduced file size, you see 😀
On Android, there are ABIs (Application Binary Interfaces) that you can support when you release your application. The most used will be the armeabi-v7a; however, there are still tons of devices that support and run the old armeabi ABI and even x86 devices, as well.
But in my research, most of the x86 supporting devices are either obsolete or emulators, which, to be very honest, I don’t care to support. But to ensure your app is reaching the most users, you most likely have to go into the project settings and select every single ABI.
However, for every ABI that you select, you’re actually bundling a separate libmonodroid and sgen with your app. If you don’t believe me, then rename your .apk to .zip and take a look in the lib folder:
This, of course, makes sense as you would need a different version of monodroid and sgen that supports that ABI. The issue is that you now have all of these libraries bundle into a single APK, and your users will be downloading all of them! The solution for any Android developer (even Java devs) is to simply split up your APKs and upload all of them to the Google Play Store.
You may need to restart VS after selecting the checkbox and ensure this flag is set in your csproj:
Additionally, your new APKs will be in your /bin/Release folder and will be marked with Signed in their file name.
Xamarin apps can be heavy and the above methods can help you reduce the application size to the minimum. But always remember you won’t able to make your app the same size as a native app because Xamarin.Forms and Mono add quite a significant amount of overhead to the file size. AOT increases that even further, but if you perform everything mentioned above correctly, you’ll be able to come up with an acceptable file size for your IPA/APK.
If I’ve missed something, go ahead and add it in the comments. I’ll make sure I add them in the post. Also, if you find something incorrect in the blog, please go ahead and correct me in the comments.
Editor’s Note: Heartbeat is a contributor-driven online publication and community dedicated to exploring the emerging intersection of mobile app development and machine learning. We’re committed to supporting and inspiring developers and engineers from all walks of life.
Editorially independent, Heartbeat is sponsored and published by Fritz AI, the machine learning platform that helps developers teach devices to see, hear, sense, and think. We pay our contributors, and we don’t sell ads.
If you’d like to contribute, head on over to our call for contributors. You can also sign up to receive our weekly newsletters (Deep Learning Weekly and the Fritz AI Newsletter), join us on Slack, and follow Fritz AI on Twitter for all the latest in mobile machine learning.