Lazy Loading Dex files
Avoiding MultiDex at startup
If you are working as an Android developer, you might be aware of the existence of the infamous dex method limit. If you don’t know what I am talking about, check different posts in the internet. Sebastiano Gottardo’s [DEX] Sky’s the limit? No, 65K methods is or Matthias Käppler’s Congratulations, you have a lot of code!” Remedying Android’s method limit are good examples. Most of the basics are covered there so I will dive directly in what I found out.
Understanding your situation
The first thing to do is understanding your codebase. What is contributing to your method count?
An easy way to check what libraries are contributing to the method count is using mihaip’s dex-method-count. Warning: This library counts per package. Be careful with libraries having other dependencies or different packages.
Second question to ask yourself: How big is the code you maintain?
The first thing we noticed is that dependencies were taking all of our methods. Support libraries and play services are huge. After some investigation, we found out that our method count was 30k and the rest was third parties.
Does it make sense to go above 65k?
Last Google IO, I had the opportunity to discuss about this subject with Fernando Cejas who works at SoundCloud. In their case, they have a native component, which with the JNI interface, adds lots of methods. Nothing to do, their main use case have a lot of methods, they had to use MultiDex.
Our case is different, our app isn’t huge, it’s just full of third parties.
How bad is MultiDex?
For users with Android versions older than Lollipop, MultiDex has a delay in startup time. We decided to do a small A/B test in production adding an artificial delay to the app startup.
After testing with different values, we reached to the conclusion that, as many can imagine, a delay in the startup has a significant negative impact on conversion. I can’t share the numbers, but trust me. Measure your startup performance during your night builds and make sure you keep it healthy.
Lazy loading Dex files
If you check MultiDex library source code, it’s just modifying the classloader to add an additional dex on app startup. The question is, why on startup? What happens if we load the dex files after startup?
Go through your dependencies. Are all those dependencies part of the most common use cases? In our case, the answer is no. Just to give you some examples. Payment libraries: PayPal, Google Wallet, Bitcoin, etc. Would your users use all of them? Form helpers: Date pickers, credit card scanners. Used only once when the user signs up.
Does it make sense to get the startup hit to have everything single library available at startup?
Show me the code
Recently I had some free time to write a sample app. The code is here.
The app lazy loads the Picasso library. The app starts and when the user wants to show an image, it loads Picasso first. We will never lazy load Picasso since it’s part of our most common use case, but since Picasso is open source, it was easier to create a sample out of it.
How does this all work?
The first thing to do is pre-dex the jar file you want to lazy load. This is done with the dx tool. In my case, located at /adt-bundle-mac-x86_64/sdk/build-tools/22.0.1/dx:
dx --dex --output=/path/to/the/library.dex /path/to/the/library.jar
After you get the dex file, place it in the assets folder.
To use the dependency from the code, you will need to mark it as provided.
dependencies {
provided 'com.squareup.picasso:picasso:2.5.2'
}
Having the dependency as provided removes the library from the final dex. You can test that by checking the method count with compile (1024 methods) and with provided (214 methods).
Using a dependency with provided is a bit tricky. You need to make sure you don’t use the dependency until you finish loading the module. In the code, I have wrapped all Picasso calls in a separate class. I honestly haven’t put a lot of thoughts into finding a more elegant way. Depending in the type of library you are using, you might want to add an intermediate activity while the module is loading.
What happens in the code is very straight forward. When we want to load the predexed file we need to move it from the assets folder to the application specific cache directory. Once is moved, it calls a modified version of the MultiDex library’s method installSecondaryDexes(). I added the MultiDex library sources to the project. You can make a diff, I just made that method public. Once the predexed lib is loaded, you can use Picasso.
I don’t remember doing anything else. The code is far from perfect and please pay attention to the different TODO around the code. I have added some comments to explain my shortcuts.
Things to investigate/fix/test/etc
Test in older devices
I have tried this in very few devices. It would be nice to know if it works in all API levels.
Find a solution for aars
Aars might contain resources, so this hack is not applicable.
Test adding more than one dependency
I have never tried to load more than one. AFAIK, MultiDex supports more than one, so it should work.
Testing in production
We haven’t test this in production yet :(
Proguard
As bubbleguuum mentioned on this reddit post:
You cannot proguard any class or interface that inherits from a class or interface in the additional dex (in your example, inheriting a Picasso class or interface)
Conclusion
Using MultiDex is not a decision to take lightly. Lazy Loading Dexes is just a hack to extend the time below 65k until lollipop+ gets more users.