The idea behind APEX by itself is rather common in everyday GNU/Linux distributions: package updates targeting specific sections of the Linux library set. But that’s something Google never tried to do given that Android has used a RO (read-only) partition where all the system libraries and frameworks are stored versus the usual RW (read-write) partitions used in most Linux distributions, rendering the standard upgrade process unsuitable.
Libraries are precompiled code that can be used in other programs. Commonly used methods can be made into libraries for Android apps to call, reducing the size of APKs as the same code won’t need to be re-implemented across multiple apps. You can find many pre-installed system libraries in the /system/lib and /system/lib64 directories. Android system libraries usually aren’t updated individually—rather, they get updated as part of Android platforms upgrades via an OTA update. On the other hand, libraries in Linux distros may be updated individually. With the introduction of APEX, system libraries in Android can be updated individually like Android apps. The main benefit of this is that app developers will be able to take advantage of updated libraries without waiting for an OEM to roll out a full system upgrade. Let’s dive into more technical detail about how APEX works.
How will APEX change the way libraries are updated?
APEX is an ecosystem that forced (or rather, is forcing) Google to reconsider the way Android loads all the libraries and files from a non-standard partition different from /system.
First of all, we have to specify the difference between a shared library and a static library. A shared library is a library (usually a file named libkind.so) that doesn’t include all the code needed to run in itself but is “linked” to other libraries actually providing the code, while a static library is, as you can guess, a library that doesn’t depend on any other libraries and has everything included statically within the file.
Android has historically configured the library path (known as LD_LIBRARY_PATH in the Linux world) with a single file called ld.config.txt  to configure the allowed search paths for the shared libraries needed by either binary or another library. These paths are hardcoded in the configuration and aren’t expandable. This layout, including the read-only system partition, leads to un-updateable libraries unless the user installs an OTA Android update. Google fixed this issue allowing to extend the search path by letting the single APEX packages provide their own ld.config.txt that included the extra (and updated) libraries paths contained in them.
While this move fixed one of the main issues, there were still a few serious issues to overcome. First of all: ABI (application binary interface) stability. Libraries should always export a stable set of interfaces to allow other apps and libraries to continue to work with the same protocol even with the upgraded library. Google is actively working on that by trying to create a stable C interface between APEXed libraries.
But an APEX isn’t limited to libraries and binaries alone. In fact, it can contain configuration files, timezone updates, and some Java frameworks (conscrypt at the time of writing).
Here are a few examples of the current APEX packages provided by AOSP:
- com.android.runtime: ART and bionic runtime (binaries and libraries)
- com.android.tzdata: TimeZone and ICU data (libraries and configuration data)
- com.android.resolv: Library used by Android to resolve network related requests (libraries)
- com.android.conscrypt: A Java Security Provider (Java framework)
How is an APEX package installed and structured?
An APEX package is a simple zip archive (like an APK) that can be installed by our handy ADB (at this point in development) and, later on, by the user themselves via a package manager (like Google Play or manually through the Android package installer).
The ZIP layout is as follows:
Let’s dive into that.
The apex_manifest.json specifies the package name and version.
The apex_payload.img is a micro-filesystem image (formatted as EXT4).
The other files are part of the verification process used to install the package. Let’s have a look.
The presence of AndroidManifest.xml, even if it’s used mainly in applications, helps us understand that most of the implementation used for a standard APK installation is reused even for these packages. In fact, only the extension is being checked to differentiate between them.
The META-INF/ directory has the package certificate and uses the same mechanism as normal APKs. So these packages are verified by a private/public key pair at runtime before the user is allowed to install an update. But that wasn’t enough for Google, so they added 2 more layers of security. They’re using dm-verity to check the integrity of the image and AVB (Android Verified Boot) verifications to make sure the image comes from a trusted source. In the worst case scenario, the APEX package will be rejected.
If all the verification steps are successful, the image will be marked as valid and will replace the system variant at the next reboot.
How is an image installed at boot?
Let’s start by taking a look at the APEXes currently installed on my device (an emulator)
As you can see, the pre-installed packages are stored in /system/apex/ and all of them are currently on version number 1. But what happens when an APEX is activated? We’ll again be using com.android.tzdata as an example.
Let’s reboot the device and analyze the logcat.
The first 2 lines provide enough information to understand the package’s origin and where it’s going to be installed: /apex/, a new directory introduced in Android Q that will be used to store the activated packages.
After the package has been successfully verified with AVB and the public key matches, the APEX is mounted using a loop device to /dev/block/loop0, making the EXT4 file system accessible to the system. A loop device is a pseudo-device that makes a file accessible as a block device, making the contents of that file accessible as a mount point.
At this point, the APEX is still not used because of the @1 suffix (that indicates the package version). To finally let the system know that our package has been successfully activated, it’ll be bind-mounted to /apex/com.android.tzdata where Android actively expects tzdata to live. A bind mount overlays an existing directory or file under a different point. 
The APEX implementation is entirely contained in a single repository under AOSP.  The apexd (APEX daemon) directory has the code running on Android. The apexer directory has the code used by the build system to create the APEX packages.
What’s the purpose?
At this point, all I can do is speculate. My best guess is that Google is trying to create a core set of APEX packages that can be updated by Google to possibly create a unified base core of Android shared between vendors, making “system” only updates possible, but using single package updates.
Are all devices going to support APEX?
No. For example, apexd requires /data/apex to be available right after boot to update all the Android modules. With FDE (Full-disk Encryption), /data/apex is encrypted until the device is unlocked by the user, rendering APEX basically useless as only the system APEX variants will be loaded at boot. Other than that, all devices should be supporting APEX, but they need a few kernel patches (many of which are fixes found by Google while playing with loop devices).