ServiceLoader works unexpectedly in the on-demand dynamic feature module
Dynamic Feature Module is one of the most exciting news of Google I/O 2018. Since Firefox Lite is a browser targeting small APK size, tailored made for data plan sensitive users, Dynamic Feature suits our needs: Adding features without increasing the initial APK size, but also having a feature-rich browser, is our ultimate goal.
This blog post describes an issue I encountered when I tried to integrate with Dynamic Feature. I want to write it down for myself.
TLDR; Service Loader in Dynamic Feature works unexpectedly. See the “Workaround” session if that’s the only thing that interests you.
# Pre-reading
Basic understanding of Dynamic Feature and how it works.
ServiceLoader
ClassLoader
BundleTool
# Background
We want to add a feature module called history, which will use the FirefoxAccount History Sync library to get your browsing history under your FxAccount. It has some Rust code, and use Kotlin Coroutine as a wrapper.
After I made it a dynamic feature module, I started an Activity in the dynamic feature module, it crashed. Said: FileNotFound Exception, can’t find
I looked at the source code, the exception is thrown by Dispatcher, it wants to find at least implementation of MainDispatcherFactory, which is AndroidDispatcherFactory. I’m not familiar with CoroutineContext. So I dug into the Coroutine source code. I found when you want to launch a Coroutine, you need to specify how to handle the threading, this is where Dispatcher comes to play. AndroidDispatcherFactory will provide a HandlerDispatcher that work with the Handler in Java. And kotlin-coroutine-android has defined its own implementation of DispatcherFactory implementation: AndroidDispatcherFactory
So the flow diagram is like this:
Activity in Feature module — implements → CoroutineContext — uses → Dispatcher.Main — uses → MainDispatcherFactory(interface) — uses → ServiceLoader — find Implementation → AndroidDispatcherFactory(implementation)
# ServiceLoader
To me, it’s a wrapper around ClassLoader. It lookup the class (resource) in ClassLoader’s DexPathList, where all your dex files reside (in installed APKs). ClassLoader only loads classes when classes are used. When we call ServiceLoader.load(), it’ll look up the class in the dex files and return the iterator with the implementations are found.
Normally the dex path is under /data/app/your_package_name-<some number>.apk
# How dynamic feature modules are installed?
First, the Play Store downloads your base.apk + configuration.apk + any APKs that’s “must-needed” for a user.
For feature modules, when you (in your code) call SplitInstallManager.requestInstall(), it’ll talk to Play Core Library and download the feature APKs for you, then install them.
How do we simulate this process?
We can use the “install-multiple” command. So we can have a debuggable version and see this error. ( Google might have a better solution to this)
If you go through Play Store, since it’s signed with release key, it’s not debuggable. And you also don’t have the root access, you can’t see what’s under /data/app.
# Reproduce the crash
I feel CoroutineContext and the Dispatcher loading process is too complicated for a sample project, I’ve written a demo app using a package named: com.example.myapplication that uses a simple ServiceLoader directly.
Please use the “serviceloader” branch to test the simple scenario
To reproduce the error and see what works what not, you”ll need :
1. An emulator without Play Store (so you can run adb root)
2. clone the sample project
3. Go throw the README or just find the errors there.
# Workaround
1. Call ServiceLoader.load() after installing the dynamic module and before using it in the feature module. or
2. Only use the ServiceLoader.load() in the feature module. See the other branch: https://github.com/cnevinc/DynamicDeliverySample/tree/serviceloader-workaround
# Extend exploration
After play around with my sample projects, I have something I would like to test. And below are my findings:
- When using ServiceLoader, if we’ve already found the implementation in the base module, can we update the implementation in the feature module?
No, if we have two resources with different content in META-INF, it won't compile - If we put only META-INF configuration and implementation class in the feature module, does that work?
Yes, that’ll work, maybe a Google solution to inject implementation besides using Dagger2.
There are some basic design principles (in my opinion):
- If the dynamic module and base module using the same dependency: put the dependency in the base module.
- If the dynamic module and normal module used the same dependency: put the dependency in the normal module
- If only the dynamic feature module will use the dependency, put it in the dynamic feature module
I wrote this blog only to keep my personal note so it’s not written in the way to help others understand the problem better. But if you want to learn more about it, please let me know so I’ll take some effort to refine it. Thanks!