Compressing the APK, trying to keep it working

Published on May 20, 2019

Compressing the APK, trying to keep it working


    / PxHere / PD


    Optimizing the weight of the APK is a non-trivial, but very relevant task in the days of the Instant App. Enabling proguard will save you from unnecessary code if your dependencies can be determined at the compilation stage, but there are several other types of files in the APK that can be excluded from the assembly.


    Under the cat on how to make dependencies - defined at the compilation stage, which files can be excluded from the assembly and how to do it, as well as, we will analyze how to exclude unused components from the assembly if you have several applications with a common code base.


    Before reading


    • Before applying the tips from the article, optimize the APK for Google ’s guide . This article is for those who do not have enough standard optimizations.
    • By "proguard" I mean an optimizing compiler with minification.
    • By component I mean a certain feature of the product from a business point of view. In our case, this is just a collection of files in a certain package. We have one gradle module for the entire application.

    The weight of our Google- optimized APK was 4.4 мб.


    Extra files


    Let's start with a simple one. If you do not use kotlin-reflect , you can exclude meta-information about kotlin classes from the assembly. You can do this as follows:
    Bbuild.gradle (Module: app)


    android {
        packagingOptions {
            exclude("META-INF/*.kotlin_module")
            exclude("**.kotlin_builtins")
            exclude("**.kotlin_metadata")
        }
    }

    Java reflection does not need files *.kotlin_module, *.kotlin_builtinsand *.kotlin_metadata. Determining which reflection you are using is very simple. If you write obj::class.<method>, then you use kotlin reflection, if obj::class.java.<method>, then java reflection.


    The result of optimization for us: -602.1 kb


    Dependencies


    Libraries sometimes add dependencies for cases that never happen in your application. For example, ktor-client pulls kotlin-reflect along with it (0.5 mb!).
    I struggled with such cases as follows: I collected the APK from minifyEnabled = true, threw it into the Android Studio analyzer, downloaded mapping.txtand looked for packages that, in theory, should not be present in the assembly. Eg kotlin.reflect. After running ./gradlew app:dependenciesin the project folder to search for dependencies (do not forget to increase the length of the history in the terminal. The dependency tree can be large!). From this tree it is easy to understand what refers to unnecessary dependencies and exclude them. In build.gradleyour module:


    dependencies {
        implementation("io.ktor:ktor-client-core:$ktorVersion") {
            exclude(group: "org.jetbrains.kotlin", module: "kotlin-reflect")
        }
        implementation("io.ktor:ktor-client-okhttp:$ktorVersion") {
            exclude(group: "org.jetbrains.kotlin", module: "kotlin-reflect")
        }
    }

    This code removes the ktor-client library dependency on kotlin-reflect . If you want to exclude something else, substitute your values.


    !!! Use this advice very carefully! Before eliminating dependencies, make sure you don't need them. If you do not, then the application may begin to fall in production !!!


    The result of optimization for us: -500.3 kb


    Validate your XML


    Unfortunately, proguard does not remove extra XML markup files from the layout folder. Unused XML can use "heavy" widgets and proguard will not be able to exclude them from the assembly too! To avoid this, delete unused resources withRefactor -> Remove unused resources...


    Check your di


    If you, like us, use runtime DI, then check if you have providers for those dependencies that you are not using. Proguard cannot exclude them from the assembly because they are not unused from the point of view of the compiler. You use them when building a dependency graph.


    Exclude Debug Dependencies from Release Builds


    Debugging tools can take a lot of space unexpectedly. For example, it stethoweighs about 0.2 мбafter compression! In any case, it is better to exclude the entire debugging infrastructure from the release build so that no one can learn too much about your application by simply downloading it from Google Play.


    You can make sure that for debug and for release different versions of the same files are used. To do this, in the folder srcnext to main, create folders debugand release. Now you can write a function initStethothat initializes Stetho in the file src/debug/java/your/pkg/Stetho.ktand a function initStethothat does nothing in the file src/release/java/your/pkg/Stetho.kt.


    Just in case, make sure that this dependency is included only in debug builds. This can be done by replacing implementationon debugImplementationin build.gradle. More often than not, proguard eliminates unnecessary files even without this step, but not always. The answer to the question "why?" below in the text of the article .


    Platforms


    Sometimes, on the same code base, several different versions of an application are issued. These can be different versions for different countries or regions, or, as in our case, for different clients. Below are tips on how to offload the platform.



    / PxHere / PD


    Our experience


    We are developing an E-SHOP mobile app designer . We have several dozen customers and each has its own individual set of components. Some components are used by all customers, some are only part. Our task is to include in the client assembly only those components that he needs.


    Flag Exception


    For each client we create a separate productFlavor . This is convenient because it is easy to make different resources for different clients, the IDE provides a graphical interface for switching between flavors, and caches work well. And you can also generate your own for each client BuildConfig.java. The field values ​​of this class are known at the compilation stage. That's what we need! Create a type field booleanfor each component.


    android {
        productFlavors {
            client1 {
                buildConfigField("boolean", "IS_CATALOG_ENABLED", "true")
            }
            client2 {
                buildConfigField("boolean", "IS_CATALOG_ENABLED", "false")
            }
        }
    }

    This is a simplified version of the configuration. The present is complex due to integration with our CI.


    It is now known whether the component is active at the compilation stage, and proguard can exclude it from the assembly!


    XML again


    Now the problem with unused XML layouts takes on a new dimension! You can’t just take and remove the markup of a component simply because some customers don’t need it.


    In our XML application of one of the rarely used components, we used a widget that referenced the image recognition library firebase.ml.vision. It weighs about 0.2 mb, which is a lot. It was decided to add this widget with code instead of declaring it in markup. After that, proguard was able to exclude visionfrom the assembly for customers who do not need it.


    The result of optimization for us: -222.3 kb for the average APK


    annotation @Keep


    There are 2 ways to tell proguard that your class cannot be minified: write a rule in a file proguard-rules.proor put an annotation @Keep. In the library play-services-visionon the root class is precisely this annotation. Therefore, 0.2 mb hung dead even in those client applications that do not need image recognition.


    I did not find a simple and safe way to remove this annotation. If you know how - please write in the comments.


    Fortunately, the library firebase.ml.vision, which is a newer version play-services-vision, does not use this annotation, and we solved the problem by going to it.


    And again DI


    Last but not least. DI for disconnected components. Everything is simple here: for each component we use our own container, and we connect the general dependencies through a separate module.


    The optimization result for us: -20.1 kb for the average APK


    findings


    • The weight of the average APK has decreased from 4.4 мбto 3.1 мб, and the minimum - to 2.5 мб!
    • The application code was not harmed, but improved. DI is now easier to work with

    All optimizations presented in the article are "low-hanging fruits." They are quite easy to implement and quickly get the result. Up to -43% for an already optimized APK in our case. I hope I saved your time by listing everything in one place.


    Thanks to all!