We all want our app packages to be as small as possible. Quite efficient way of decreasing the size of the app bundle is to use Linker. That's usually easier said than done. In my experience, the main problem with linker configuration is the identification of the problems caused by linker being too aggressive at stripping out the code that is actually necessary to run the app.
I've decided to write this blog post because I've recently ran into one of these issues where linker is doing "too good" job at removing code and therefore wanted to share with you my experiences.
Long story short
I've decided to update one of my Android apps. Unfortunately the process wasn't so smooth. At the first attempt of releasing it, after almost a year of no updates, the app decided to crash shortly after the splash screen showed up on the screen.
I can't say I was surprised that the app didn't launch successfully, after all I've updated most of the 3rd party libraries that the app relies on, added some new ones and I also decided to switch to the new and shiny d8 dexer & r8 shrinker. I actually feel that this is something quite usual to happen in such cases, especially considering that my Linking setting was set to the most aggressive Sdk and User Assemblies option:
Debugging
There's usually not much to be done in terms of debugging when the app crashes at the launch time, as it's probably not even reaching your code. You usually are only left with device's logs to look into and work out what's wrong from them. On Android you would normally turn to adb and it's logcat command, which would print out everything that's happening on the device.
adb logcat
Fortunately there's an easier and a bit more organised way to read through these logs, as they can get very messy when reading them from within the command line. There's a Device Log window in Visual Studio, that allows you to pick your connected device (or emulator) and show that device's logs. What's even more important it works for both Android and iOS.
Once I had the Device Log window in front of me, with the connected to the PC device selected from the left top corner drop-down, I was ready to start diagnosing.
Because there's so much logs coming in from the device, I always try isolate these that are important to me and my debugging session by making sure that the Device Log window is clear (hit the clear button in the toolbar) of any other logs just before I begin.
In my case the logs after launching the app and after the crash happened looked like so:
The important parts of these messages, when looked closer at are
java.lang.ClassNotFoundException: Didn't find class "android.support.v7.widget.FitWindowsLinearLayout"
and
Error inflating class android.support.v7.widget.FitWindowsLinearLayout
Now, that we've got our suspects, we go hunting.
NOTE: There are cases where the log messages are much clearer about the issue. You can read from them that there was a problem with a specific type or a method. Once I was done with fixing the above problem I ran into this one:
System.MissingMethodException: Default constructor not found for type XamEffects.Droid.TouchEffectPlatform
It clearly states in the the above, that for some reason the constructor or the entire type XamEffects.Droid.TouchEffectPlatform cannot be found (XamEffects plugin). Usually it means only one thing, that this code has been stripped out by the linker when it shouldn't, therefore the additional research is not really necessary.
Research
Whenever messages in the logs are not very clear or obvious, I look for some answers using the exact error message or a part of it as a search phrase. I do that with these three mediums:
I was lucky enough to find some hits with Google, which were not exactly solutions to my problem but at least they gave me an idea of the source - linker. Here are some of them:
- Xamarin Forums with ProGuard solution https://forums.xamarin.com/discussion/126072/binary-xml-file-line-14-error-inflating-class-android-support-v7-widget-fitwindowslinearlayout
- SO comment with ProGuard solution https://stackoverflow.com/a/46510941/510627
- VS Community Forum issue with ProGuard solution https://developercommunity.visualstudio.com/content/problem/365357/problem-with-android-linking-in-forms-app.html
From the contents of the above links it would have seemed that somehow linker is removing FitWindowsLinearLayout object's code. The easiest way to check if we're truly dealing with an issues of stripped out code, is to change currently set Linking option to Sdk Assemblies Only or straight to None and then build and run the app again to verify if the issue persists.
In my case, changing to Sdk and User Assemblies helped, however there was also a 7mb increase in the apk package size, which was not acceptable for me.
The solution
To fix Xamarin.Forms linker related issues one needs to instruct it to keep the code that shouldn't be stripped out. There are multiple ways of achieving this for Android and iOS. The best way (IMO) is the cross-platform option with the use of custom linker configuration file.
Linker configuration
The linker configuration file is an XML file that contains definition of which assemblies, types, methods or even fields shouldn't be linked out.
The most important thing for this file is to set the Build Action in its properties to LinkDescription. Naming of the file is irrelevant, however I would strongly suggest to name it linker.config, so there's no confusion about its purpose.
The empty linker.config file would look like this:
<?xml version="1.0" encoding="utf-8"?>
<linker>
<!-- Place for linker instructions of what shouldn't be stripped out -->
</linker>
The fix
Finally we've got to the point where we will update our existing (or newly created) linker configuration file with instructions that will hopefully fix our problem(s).
We add instructions to the linker.config file depending on what we've established from the device logs. If we're lucky and have clear understanding of what's the root cause of our app crash, like in the example above
Default constructor not found for type XamEffects.Droid.TouchEffectPlatform
we go ahead and update the instructions:
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="XamEffects.Droid">
<type fullname="*" />
</assembly>
</linker>
The assembly
tag and it's child type
tag will make sure that all of the types of the XamEffects.Droid library will be left intact when linking away code. You can always be more specific and instead of using the wildcard (*) in the type
tag, you could provide the full name (including namespace) of that particular type (i.e. <type fullname="XamEffects.Droid.TouchEffectPlatform" />
). Be aware though, that if there are any other types, methods or fields that will also need to be preserved from linking them out, they will require their own instructions to be added.
In case there's no understanding of which 3rd party library or part of it is being stripped out (i.e. no clear indication from the device logs), I tend to go through all of the app dependencies and try to work out which ones could be potentially the source of the problem. It usually comes down to expanding your head project References folder in the Solution Explorer and trying to narrow down the candidates based on their names.
From our earlier discoveries, we're looking for something related to android.support.v7.widget.FitWindowsLinearLayout. Unfortunately there's no exact match for android.support.v7.widget namespace in the names of the referenced libraries.
However, we can easily pick a bunch of potential candidates, that most likely hold the namespace of our interests.
At this stage of our investigations we are ready to start updating linker configuration. We could user a brute force approach and literally list all of these candidates as new instructions in the linker configuration files or we could be a bit more sophisticated with the process and have a "peek" at what's actually inside of these dll's to verify which would be a match.
Brute force approach
The good thing about this approach is that it will work even when you don't have any clue about what's the cause of the linking issues. You list all of your suspected assemblies (or all of the project dependencies if you have no candidates) as instructions in the linker.config file and then eliminate them one by one by removing them or commenting them out. If I go with this approach I tend to be a bit smarter about the elimination process and comment out half of them at a time (i.e. divide and conquer), instead of actually doing it one by one.
We have narrowed down our suspects to 5 libraries, therefore we add them in:
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="XamEffects.Droid">
<type fullname="*" />
</assembly>
<!-- Let's divide and concquer! -->
<assembly fullname="Xamarin.Android.Support.v7.AppCompat">
<type fullname="*" />
</assembly>
<assembly fullname="Xamarin.Android.Support.v7.CardView">
<type fullname="*" />
</assembly>
<assembly fullname="Xamarin.Android.Support.v7.MediaRouter">
<type fullname="*" />
</assembly>
<assembly fullname="Xamarin.Android.Support.v7.Palette">
<type fullname="*" />
</assembly>
<assembly fullname="Xamarin.Android.Support.v7.RecyclerView">
<type fullname="*" />
</assembly>
</linker>
Once you verify that having these in fixes the crashing (more about the process in the next blog post) then you proceed with dividing and conquering. We remove or comment out some of the entries, to narrow down the candidates, then we verify if the app crashes or not and we do so, until we are left with the one and only.
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="XamEffects.Droid">
<type fullname="*" />
</assembly>
<!-- Here's our winner! -->
<assembly fullname="Xamarin.Android.Support.v7.AppCompat">
<type fullname="*" />
</assembly>
</linker>
NOTE: For those of you who are here because of the android.support.v7.widget.FitWindowsLinearLayout problem, the Xamarin.Android.Support.v7.AppCompat assembly is the one that you need to preserve.
The disadvantage of this this approach is that it's time consuming, especially if you don't have any idea why your app is crashing. Going through the process of removing instructions, rebuilding the project etc. can be quite tiresome.
The peek'y approach
With this approach we're going to be a bit more clever about which assembly instructions we will add to the linker configuration. To determine this we will require a decompiling tool. I'm using for these purposes ILSpy (latest version) but you could use any tool of your choice/preference (e.g. dotPeek).
Having the ability to peek inside of the dll in this case is priceless. We proceed from the top to the bottom of the list of our candidates, starting with Xamarin.Android.Support.v7.AppCompat.dll. The file itself could be found in the obj\Debug\90\linksrc directory of your project, along the side of all the other dll's - the files will be there provided that you have built your app in the Debug mode beforehand. The result of having a look inside of it clearly indicates that we have our guy - there's namespace and the class name match.
This approach, as opposed to the brute force, is making the elimination process much less reliant on luck and guessing, which in turn makes it much more efficient, as it reduces the app crashing verification cycles.
In case we wouldn't find a match in the Xamarin.Android.Support.v7.AppCompat.dll library we would proceed further with our examinations with the next dependency on the list and continue the procedure until the match was found.
We could potentially optimise what we currently have in our linker.config by being more specific about which exact type we want to preserve. Instead of making sure all of the types (wildcard) from this assembly will stay untouched, we narrow it down just to the FitWindowsLinearLayout type.
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="XamEffects.Droid">
<type fullname="*" />
</assembly>
<!-- Here's our more specific winner! -->
<assembly fullname="Xamarin.Android.Support.v7.AppCompat">
<type fullname="Android.Support.V7.Widget.FitWindowsLinearLayout" />
</assembly>
</linker>
DISCLAIMER: I haven't checked if the above configuration actually works and fixes the crash, it's more to make a point that linker configuration can be more flexible.
Summary
Linker related problems are rarely easy to fix and are usually tricky to track down, so please don't be too hard on yourself if it takes some time for you to get it right or you won't succeed with your first few attempts.
Hopefully this little guide will be at least a road sign for you to understand better how to "bite" fixing linker related problems.