1

Android app shrinking beyond R8

 1 year ago
source link: https://proandroiddev.com/android-app-shrinking-beyond-r8-ad41934727dc
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Android app shrinking beyond R8

Hello Everyone!

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!

1*ywzFkFdKm9-OiG-8uMQgXQ.png

Intro

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.

Minimal App

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"/>
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.am);
WebView w = (WebView) findViewById(R.id.web);
w.setWebViewClient(new WebViewClient(){
//older Android still needs this deprecated function
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
});
w.loadUrl("https://debloat.app");
w.getSettings().setJavaScriptEnabled(true);
// This line enables notifications (for full version)
FirebaseMessaging.getInstance().subscribeToTopic("news");
}
}

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).

1*86xypIQS9oFM8msOcRVPJg.png
Full app content (apkanalyzer)
1*iwTrEjw83CCkVlCqXpKfEQ.png
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!

R8 shrinking

To shrink our app, we enable R8 at full mode and rebuild the app. Here is the comparison table.

1*gYmiby9qKJwXtuhOY34Xcw.png
Size comparison before vs. after R8 optimisations
1*eWvthGyAeUWie43c748VyA.png
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.

1*MpJuDmYOKslfWLOpNQAlnA.png
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 :)

1*oMtOvQ_qcR-bJsiQPbbHgw.png
APK’s resources folder.
1*cn6t7kHt_-pUiQXGdeGkYg.png
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.

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).

1*uo-DcrtkufTJQxzWCIWoxg.png
Full app (1.1 MB) coverage results
1*Ouc8Xq-uX3YTaXXCzVhwlA.png
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.

1*6SuWTA0J8LkDl7Ks9MgnyA.png
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.

1*4elSHV6w1cBCBLWjtA6S6Q.png
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.

ACV shrinking

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.

1*aZgf-7LIG_Lxve7Qa4NKsg.png
R8 & ACV-shrunk app (277KB), coverage results
1*CqkfgG3whQ7pw2Nv0nfncA.png
R8 & ACV-shrunk app
1*ZFjHOKPBoinZ0lB1V-bMLg.png
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.

1*kon9esPySuEh4AlJAFzl8g.png
Stub methods

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:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK