My name is Aleksandr, and I am working on ACV shrinking — more effective way to shrink Android apps. Through testing!
In short, ACV shrinking helps to remove bytecode bloat from Android apps by removing unused code. Compared to existing static analysis-based tools it requires thorough testing. But then, only executed code makes it into the APK.
From this article you will get insights on app internals, how the app changes under R8 shrinking, and how to employ instruction coverage to outperform R8. Feel free to click through our coverage reports in the end of this article!
App size was always important to app producers since it affects install rates. On the other hand, a rich UI experience is a competitive advantage that increases size.
Among tools R8 shrinker and ProGuard are most known for size reduction. Additionally, Facebook released its own optimiser called ReDex. They statically identify and remove not reachable code and apply other clever optimisations. These tools help a lot. However, APKs still keep plenty of reachable but never executed code.
For this demo we produce the smallest WebView app to begin with. Later, we add background notifications to incrementally observe changes. Single activity and a few lines of code to open your webpage, and here we go!
<?xml version="1.0" encoding="utf-8"?> <WebView android:layout_width="fill_parent" xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/web" android:layout_height="fill_parent"/>
Turned out, Android Studio generates 1–5 MB APK by default for simple apps. Thus, we first remove default dependencies and resources, and clean the AndroidManifest. Another magic trick is to put a single vector XML for an icon.
Following this receipe, our APK weighs just 10KB (861B for DEX). This doubles to 20KB when published at Google Play. Check it out!
Adding background notifications
Now, we add the firebase-messaging dependency. Turned out, the firebase dependency requires the supportive AndroidX library. Thus, full app size bounces back to 1.1MB (882KB for DEX).
Full app content (apkanalyzer)
Full app internals (apktool)
Apkanalyzer reveals app’s internals, but we also use Apktool to see more. Full app have got additional high level packages (besides the main “app.debloat” package) and a lot of classes in them. Let’s shrink it!
To shrink our app, we enable R8 at full mode and rebuild the app. Here is the comparison table.
Size comparison before vs. after R8 optimisations
R8 minified class/package names
The app size improved by 67%. Moreover, code structure changed dramatically too. Visually, instead of readable package/class/method/variable names, we got short names. Yet, the number of code entities has been greatly reduced too.
Code entities and resources comparison
One may believe we need all of that. But we will surely find here the redundant code and resources. For example, it was unexpected to find additional 122 XML and 41 PNG files. Turned out, most of XML files keep translations of Google Play services unavailability errors. This functionality goes to almost every app! You may decide yourself on its usefulness 🙂
APK’s resources folder.
Example of ./res/values/strings.xml
Worth to note, resources are referenced and used somewhere in the code. There is too much code, but version we could actually recognise packages, classes, methods and fields in the full app. R8 reduces the amount of code, though it’s also harder to get a clue on the minimised code. We can actually see what executes with instruction coverage.
ACVTool is an instruction coverage measurement tool that highlights actually executed code in a JaCoCo-like report, but for the whole app and in smali representation.
We will skip all the technical details and immediately dive into instruction coverage generated from end-to-end tests. We’ve got one report per each app version — for the initial 1.1 MB app and for the R8-optimised (366KB).
Full app (1.1 MB) coverage results
R8-optimised app (366KB) coverage results
The actually executed code appears to be just 7.6% for the not optimised app. Yet, it grows to 27.5% for the R8-optimised app. Let’s see how the main package changed.
Main package classes before and after R8 shrinking
Turned out, R8 only left the MainActivity class in the main package. Naturally, this class is referenced in the manifest file. However, the class has changed, too.
MainActivity.onCreate method before and after R8 shrinking.
The MainActivity class actually has got more instructions in the onCreate method. Apparently, R8 inlined code from other methods. We can even find here a try-catch structure! Yet, the major part of the app (~73% of instructions) was not executed, despite our testing efforts.
With instruction coverage information we now know exactly what was executing. That code is to stay. However, we carefully cut the not executed instructions. This way the ACV-shrunk version produces close to 100% instruction coverage and weighs 277KB now.
R8 & ACV-shrunk app (277KB), coverage results
R8 & ACV-shrunk app
Total reduction results
In the end, we got a 277KB app, which is 24% smaller than R8-optimised version. In total, this is 75% less of initial APK (and 91% less for DEX)
However, bytecode manipulations are quite a challenging task. Our automated approach still keeps some empty classes and definitions of methods where instructions have been removed — stub methods. Stubs are never invoked, but some of them may maintain the code structure through inheritance.
Though in this report we focused on cutting not executed instructions, eventually, stubs get removed. There is quite some room for other optimisations too. This work is in progress.
Please feel free to share your thoughts, learn more at Debloat.App project, and check the genuine instruction coverage reports from this article:
This article was originally published on proandroiddev.com on December 26, 2022