Android’s multidex slows down app startup
The 65k method limit is a problem that has been addressed time and time again within the Android community. The current state of Android addresses the issue with multidexing. While multidexing is an excellent solution by Google, I’ve noticed a significant impact on app startup performance which has yet to raise any attention in the community. I wrote this article for developers who have never heard of this issue (but would like to implement multidexing) and to folks who already have multidexing but would like to observe the performance gains that I’ve seen with my solution.
For the uninitiated, Android apps are written in Java which is converted into a .class file. This class file (and any jar dependencies) are then compiled into a single classes.dex file. This dex file is then combined with any resources needed into a .apk file: which is ultimately what you download from the app store. Read more here.
One limitation with this compilation process is that the system only allows up to 65k methods within a single dex file. In the early days of Android, applications that hit the 65k method limit addressed the issue using Proguard to shrink any unused code. However, this approach is limited and only delayed the inevitable approach to the 65k method limit for production apps.
In light of this issue, Google has released a solution in the recent Android Support Libraries to address the 65k method limit: multidexing. This solution is handy and will allow your app to break well beyond the 65k method limit, but (as I’ve said before), there is a significant impact on performance which may slow down your app startup.
Setting up multidex
Multidexing is a fairly mature solution with great documentation. I highly recommend following the instructions on the Android Developer site to enable multidex in your project. You can also refer to the sample project in my Github.
While setting up multidexing for your project, you may notice a java.lang.NoClassDefFoundError when running your app. This means that the class for app startup is not located in the main dex file. The Android plugin for Gradle in Android SDK Build Tools 21.1 or higher has multidex support. This plugin uses Proguard to analyze your project and generate a list of classes for app startup in the file [buildDir]/intermediates/multi-dex/[buildType]/maindexlist.txt. However, this list is not 100% accurate and may miss some of the classes necessary for app startup.
To solve this, you should list those classes in the multidex.keep file to let the compiler know which classes to keep in the main dex file:
- Create a multidex.keep file in your project folder.
- List classes reported in the java.lang.NoClassDefFoundError into the multidex.keep file (note: don’t modify the maindexlist.txt in the build folder directly; this is generated every time the app compiles).
- Add the following scripts to the build.gradle in your app. This script will combine your multidex.keep and maindexlist.txt generated by Gradle while compiling your project.
Multidex app startup performance impact
If you use multidex, then you need to be aware that there will be an impact on your app startup performance. We’ve noticed this by keeping track of our app’s startup time — defined as the time between when a user clicks on the app icon up to when all images have been downloaded and displayed to users. Once multidex was enabled, our app startup time increased by about 15% on all devices running Kitkat (4.4) and below. Refer to Carlos Sessa’s Lazy Loading Dex files for more info.
The reason is because Android 5.0 and higher uses a runtime called ART which natively supports loading multiple dex files from application APK files. However, devices prior to 5.0 have extra overhead when loading classes from dex files outside of the main dex.
Addressing the multidex app startup performance impact
In the span between when the app starts and when all of the images are displayed, there exist many classes which are not be detected by Proguard and thus not stored in the main dex file. The question is, how can we know what classes have been loaded during app startup?
Fortunately, we have the findLoadedClass method in ClassLoader. Our solution is to do a runtime check at the end of the app startup. And if there is any class stored in the second dex file and that loads during app startup, then we move them into the main dex file by adding the class name into the multidex.keep file. My sample project will have the specific implementation details, but you can do this by:
- running getLoadedExternalDexClasses in the following util class at the end of what you consider to be your app startup
- adding the list returned by the method above to your multidex.keep file and then recompile
Here are the startup performance improvements that we’ve observed in our app across multiple devices. The first column (blue) is the base app startup time without multidexing. You can see the significant time increase in the second column (red), which is the app startup time when we enabled multidex without any extra work. The third column (green) is the app startup time with multidex turned on and the improvements discussed. As you can see from the graph, the app startup time drop back to the same level or even faster than before turning on multidex. Give it a try and you should notice performance improvements as well.
Just because you can doesn’t mean you should. You should treat multidexing as a last resort since it has a big impact on app startup and in order to address it you need to maintain extra pieces of code and solve strange errors (e.g. java.lang.NoClassDefFoundError). Once we hit the 65k method limit, we initially avoided multidexing in order to stave away the performance impact. We kept metrics and investigated the SDK’s that we were using and found a lot of useless code that could be removed and refactored. Only when there were no options left did we consider multidexing, and by then our code quality and standards had improved by leaps and bounds. Instead of jumping straight into multidexing, consider looking into keeping your code clean, reusing existing components, or refactoring your code in order to avoid the 65k method limit.