Weight Optimization APK is a non-trivial, but very relevant in the Instant App, task. The inclusion of proguard will save you from unnecessary code, if your dependencies can be determined at the compilation stage, but the APK has several other types of files that can be excluded from the assembly.
Under the cut on how to make the dependencies - defined at compile time, which files can be excluded from the assembly and how to do it, and also, let us analyze how to exclude unused components from the assembly if you have several applications with a common code base.
The weight of our Google- optimized APK has been 4.4
.
Let's start with the simple. If you are not using kotlin-reflect , you can exclude meta-information about kotlin-classes from the assembly. You can do this as follows:
In build.gradle (Module: app)
android { packagingOptions { exclude("META-INF/*.kotlin_module") exclude("**.kotlin_builtins") exclude("**.kotlin_metadata") } }
For Java reflection, no *.kotlin_module
, *.kotlin_builtins
and *.kotlin_metadata
files are needed. Determining which reflection you use is very simple. If you write obj::class.<method>
, then you use kotlin-reflection, if obj::class.java.<method>
, then java-reflection.
Total optimization for us: -602.1 kb
Sometimes libraries pull 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 an APK with minifyEnabled = true
, threw it into the Android Studio analyzer, downloaded mapping.txt
and searched for packages that, in theory, should not be present in the assembly. For example, kotlin.reflect
. After running ./gradlew app:dependencies
in the project folder for searching dependencies (do not forget to increase the length of the history in the terminal. The dependency tree can be large!). On this tree it is easy to understand that it refers to unnecessary dependencies and eliminate them. In your module's build.gradle
:
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 dependency of the ktor-client library on kotlin-reflect . If you want to exclude something else, substitute your values.
!!! Use this tip very carefully! Before excluding dependencies, make sure you do not need them. If you do not do this, the application may start to fall in production !!!
The result of optimization for us: -500.3 kb
Unfortunately, proguard does not remove unnecessary XML markup files from the layout folder. Unused XML can use heavy widgets and proguard cannot exclude them from the build too! To avoid this, remove unused resources using Refactor -> Remove unused resources...
If you, like us, use runtime DI, then check if you have providers for dependencies that you do not use. 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.
Debugging tools can take up unexpectedly a lot of space. For example, stetho
weighs about 0.2
after compression! In any case, it is better to exclude the entire debug infrastructure from the release build so that no one can learn too much about your application just by downloading it from Google Play.
You can make debugging and release use different versions of the same files. To do this, in the src
folder, next to main
, create debug
and release
folders. Now you can write the initStetho
function, which initializes Stetho in the src/debug/java/your/pkg/Stetho.kt
and the initStetho
function, which does not do anything, in the src/release/java/your/pkg/Stetho.kt
.
Just in case, make it so that this dependency is included only in debug builds. You can do this by replacing implementation
with debugImplementation
in build.gradle
. Most often, proguard eliminates unnecessary files even without this step, but not always. The answer to the question "why?" below in the text of the article .
Sometimes several different versions of an application are released on the same code base. These may be different versions for different countries or regions, or, as in our case, for different clients. Below are tips on how to unload the platform.
We are developing mobile application designer E-SHOP . We have several dozen customers and each has its own individual set of components. Some components are used by all customers, some only by part. Our task is to include in the client's assembly only those components that he needs.
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 to switch between flavors, and caches work well. You can also generate your own BuildConfig.java
for each client. The values of the fields of this class are known at compile time. That's what we need! Create a boolean
field for 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. This is difficult because of the integration with our CI.
Now it is known if the component is active at compile time, and proguard can exclude it from the assembly!
Now the problem with unused XML layouts takes on a new dimension! You can’t just take and remove the markup of any component simply because some customers don’t need it.
In our application in XML of one of the rarely used components, we used a widget that referred to the image recognition library firebase.ml.vision
. It weighs about 0.2 mb, which is quite a lot. It was decided to add this widget code instead of declaring it in the markup. After that, proguard was able to exclude vision
from the build for customers who do not need it.
Total optimization for us: -222.3 kb for medium APK
@Keep
There are 2 ways to tell proguard that your class cannot be minified: write a rule in the file proguard-rules.pro
or put the @Keep
annotation. In the play-services-vision
library, this annotation is on the root class. Therefore, 0.2 mb was a dead weight, 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 firebase.ml.vision
library, which is a newer version of play-services-vision
, does not use this annotation and we solved the problem by switching to it.
Last but not least item. DI with disabled components. Everything is simple: for each component we use our own container, and we connect the general dependencies through a separate module.
Total optimization for us: -20.1 kb for medium APK
4.4
to 3.1
, and the minimum to 2.5
!All the optimizations presented in the article are "low fruit". They are quite easy to implement and get results quickly. Up to -43% for an already optimized APK in our case. I hope I saved your time by listing everything in one place.
Source: https://habr.com/ru/post/452524/
All Articles