It is quite common knowledge that Android apps are developed in Java. This is often loosely and incorrectly interpreted to mean that Android apps execute on a standard Java Virtual Machine using standard Java bytecode.
So how much does Android rely on Java for building and running apps?
To answer this we must first clearly define what we mean by “Java”. Usually when we say this we are talking about the programming language and in the typical case, the runtime environment is correctly inferred to be the Java Virtual Machine. Java, when referred to at this level is actually just a language spec. Any compiler that has a frontend for the spec can take that source code and produce output. That output can be anything the compiler supports via one of its backends, it doesn’t have to be JVM compliant bytecode. That is just one option. As an example, there was a compiler released as part of the GNU compiler collection called GCC-GCJ. This can compile Java source code directly to machine code, and it therefore doesn’t require any JVM to execute. The project has since been discontinued.
Android Build System
We already know that Android apps are written in Java, but what happens after that?
The following diagram documents the entire build process at a high level. The source code is compiled and packaged in an Android Application Package (APK) along with resources, assets, and dependencies. The compilers component is most pertinent to our discussion. We will explore the two implementations in the following sections.
Fig 1. Overview of the Android Build System
javac and dx
This was the original toolchain released as part of the Android SDK. Java source code is taken and converted to Java bytecode (.class files) via the Java compiler javac. This includes source generated by a Java annotation processor (think dagger, lombok etc.). Bytecode transformers (see the Transformers API) then have an opportunity to manipulate the java bytecode to do things like obfuscation and minification (proguard, retrolamba, etc.) before finally being transcoded to dex bytecode by dx. This is then packaged up with the various other resources into the apk container.
The decoupling of both compilers has some implications, namely that anything that touts compatibility with Java (read, generates JVM compliant bytecode) can be used transparently with respect to dx. It does however mean the build process takes longer than it would if we were to compile straight to dex bytecode.
JACK and JILL
The new toolchain attempts to simplify this process by providing a compiler that can compile the Java source code directly to dex bytecode. This compiler is referred to as the Java Android Compiler Kit or JACK for short.
An outline of the new toolchain is as follows.
Fig 2. Overview of the new JACK and JILL toolchain
JACK no longer has any concept of java bytecode and as many libraries used in Android development are distributed in the form of Java Archives (jar) and Android Archives (aar) containing Java bytecode our toolchain still needs to support it. This support is provided by the JACK Intermediate Library Linker (JILL). JILL takes the java bytecode and associated files and changes it into a new intermediary format called JAYCE (packaged in the .jack container format in the diagram below). All discrete components (these could be modules or jars etc) added to the .jack library file are also pre-dexed. This means their dex bytecode is included in the .jack library file and JACK does not need to compile it again.
Because JILL is where support for java bytecode is encapsulated it is also where support for bytecode processors as well as other JVM compatible languages like Kotlin/ Scala etc needs to reside.
NOTE: Instant run is currently not supported but JetBrains and Google are working on it.
The runtime provides an implementation of the standard java library and some mechanism for taking the dex bytecode that apps are deployed to the device in and converting it to the machine code that actually runs on the hardware. The standard lib implementation has changed from Apache Harmony to openJDK as of Android Nougat. The runtimes prior reliance on Apache Harmony was the reason why we Android developers were deprived of newer language features like the streams API, lamdas, method references etc.
Both the Dalvik VM and Android Runtime operate on Dalvik bytecode. They have some fundamental differences that have implications on performance and storage among other things.
Fig 3. Full Android software stack
The Dalvik VM is the older of the two runtimes. It is actually quite similar to a standard java virtual machine however it has some architectural differences that make it more suitable for running on a resource constrained mobile device, with multiple instances. The specifics are outside the scope of this post.
It uses a just-in-time compiler to convert dex to machine code as it is run. This might seem a suboptimal time to be compiling bytecode to machine code however a just-in-time compiler has the advantage of using information of specific runtime characteristics to provide further optimisations for subsequent uses. This is referred to as profile guided just-in-time compilation. It is for this reason that ART also contains a just-in-time compiler despite doing most of the compilation work ahead of time.
Android Runtime (ART)
The Android Runtime (referred to as ART) has been available since KitKat and is the only runtime provided with Android as of Lollipop. One of the reasons for storing the apps in bytecode was because bytecode is more expressive, and therefore takes up less space on disk than machine code does. Mobile devices are no longer as constrained by storage space and so storing machine code is acceptable. Doing so improves startup time of apps and removes overheads of compilation at invocation time.
ART takes dex bytecode and compiles it to machine code in the form of an executable and linkable format (ELF) shared object file. As of Android Nougat ART has been shipped with a JIT compiler for the purposes of optimising runtime performance via profiling as mentioned above.
It also has improved garbage collection and debugger support.
Jumping back to our original question:
how much does Android rely on Java for building and running apps?
We can say with some confidence that Android relies quite heavily on Java for building apps. If memory serves, this was actually done deliberately. I recall reading somewhere that Java was chosen to take advantage of the existing tooling/ developer base which makes sense strategically speaking.
Where it starts to diverge is in the toolchain when bytecode is transcoded to Android specific flavours, while Java-esque, is still different. There is currently more than one toolchain for doing this, however the end result is more or less the same with respect to the Java source/ bytecode. It is replaced with dex bytecode and that is what ends up being deployed to the device. The runtime then determines when and how that dex becomes machine code.
We’ve tried to keep this at a level that is both informative and useful but glossed over some of the finer details. Hopefully this has given you a decent overview of the Android build systems and the role Java plays in them.