Reduce APK size (2/2): Optimizing code

Optimizing the graphics resources of your android application was the main subject of my previous post. The second post from the “how to reduce APK size” series focuses on optimizing the source code.

In this post, first I’ll describe a few tips for a better optimized code. Then I’ll show you some of the methods of dependency analysis and eventually, I’ll present several tips on how to reduce APK size of your application in case you’re using native libraries.

Reduce APK size: Source code

Let’s start our optimization efforts with the source code of your application. There is one simple way how to get rid of all the unused code and it’s been a part of the Android build tools since the beginning — it’s called ProGuard.

ProGuard

The ProGuard tool shrinks, optimizes, and obfuscates your code by removing unused code and renaming classes, fields, and methods with semantically obscure names.

Code shrinking detects and removes unused classes, methods, fields, etc. from your APK. This also works for code from included libraries. One of the added benefits is that by doing this, it helps you to avoid the 64k methods limit. Also, ProGuard optimizes your code and removes unused code instructions.

To enable code shrinking with ProGuard, make sure to have the following lines in your build.gradle file:

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile(‘proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

Also, if you want to have your unused resources removed by ProGuard and even further reduce APK size, add this line:
shrinkResources true

Enumerations in Android

It is a well-known fact that it’s not recommended to use enumerations in Android programming. Less is known why you should not do that. Let’s find out then.

According to the documentation, a single enumeration can add about 1.0 to 1.4 KB of size to your application’s classes.dex file. If you’re a keen enumeration fan and use them throughout your code a lot, this could lead to an unnecessary increase of the size of your application’s APK.

A recommended way is to use the @IntDef annotation together with ProGuard’s code optimalizations to strip enumerations out of the code and convert them to integers instead. This conversion should preserve all the type safety benefits of enumerations while saving precious storage space.

An example code should then looks like this:

@IntDef({Status.IDLE, Status.RUNNING, Status.FINISHED})
@Retention(RetentionPolicy.SOURCE)
@interface Status {
	int IDLE = 0;
	int RUNNING = 1;
	int FINISHED = 2;
}

instead of this:
enum Status {
    IDLE,
    RUNNING,
    FINISHED,
}

Dependency analysis

The ability to use libraries in your Android projects vastly improves the quality of code and speed of development. However, as your project grows, you keep adding new and new libraries starting with the Android Support library, Google Play services, an image loading library, a HTTP client library, etc.

Soon, you might hit the 64k methods limit and have to set up your project to use multidex. ProGuard might be able to help you with this but first, let’s look at some tips on how to analyze and maybe get rid of libraries and their dependencies that you probably don’t need anyway. This might help you in your efforts to reduce APK size of your application even further.

Analyze dependencies

Sometimes, after adding a single small library dependency, your methods count and application size might increase suddenly. That might happen because of transitive dependencies which you cannot normally see in your build.gradle file.

Luckily, there’s an easy way of finding out what dependencies each of your linked libraries use. To do that, execute gradlew command in your project’s root with arguments in format <modulename>:dependencies, for example like this:

./gradlew app:dependencies

An example output of the tool looks like this (shortened):
+--- project :datamodel
|    +--- org.greenrobot:greendao:3.1.0
|    |    \--- org.greenrobot:greendao-api:3.1.0
|    \--- com.android.support:appcompat-v7:24.1.1
|         +--- com.android.support:animated-vector-drawable:24.1.1
|         |    \--- com.android.support:support-vector-drawable:24.1.1
|         |         \--- com.android.support:support-v4:24.1.1
|         |              \--- com.android.support:support-annotations:24.1.1
|         +--- com.android.support:support-v4:24.1.1 (*)
|         \--- com.android.support:support-vector-drawable:24.1.1 (*)
+--- com.android.support:appcompat-v7:24.1.1 (*)
+--- com.android.support:design:24.1.1
|    +--- com.android.support:support-v4:24.1.1 (*)
|    +--- com.android.support:recyclerview-v7:24.1.1
|    |    +--- com.android.support:support-v4:24.1.1 (*)
|    |    \--- com.android.support:support-annotations:24.1.1
|    \--- com.android.support:appcompat-v7:24.1.1 (*)
+--- com.android.support:support-v4:24.1.1 (*)
+--- com.android.support:cardview-v7:24.1.1
|    \--- com.android.support:support-annotations:24.1.1
+--- com.android.support:recyclerview-v7:24.1.1 (*)
+--- com.github.bumptech.glide:glide:3.7.0
+--- com.android.databinding:library:1.1
|    +--- com.android.support:support-v4:21.0.3 -> 24.1.1 (*)
|    \--- com.android.databinding:baseLibrary:1.1 -> 2.1.2
+--- com.android.databinding:baseLibrary:2.1.2
\--- com.android.databinding:adapters:1.1
     +--- com.android.databinding:library:1.1 (*)
     \--- com.android.databinding:baseLibrary:1.1 -> 2.1.2

The asterisk (*) next to the dependency version number means that the particular dependency has been already mentioned in the list before and so it would have been included in the project anyway.

If you require a more detailed analysis, there’s just the right tool for you called ClassyShark.

ClassyShark

ClassyShark is a standalone tool for Android developers allowing to browse any Android executable and show information about it including class interfaces and members, dex counts and dependencies. Also, ClassyShark can be useful in showing the exact count of methods for each package, which might help you identify the packages that make your application hit the 64k methods limit.

Working with ClassyShark is straight-forward and there’s also an easy-to-follow user guide.

Final tips on working with libraries

Let me present the last two tips regarding working with libraries in your Android projects which might help you reduce the APK size.

The first tip is focused on developers using the Google Play services functionality. It is often the case that you don’t use the whole set of Google Play functionality in your app, but only a part of it. For example you might only use Google Maps and Google Analytics.

So instead of pulling the whole package of Google Play services, include only specific parts of it that you’re actually using. There is a convenient list of all available individual API packages in the user guide.

The second tip is simple in its nature. If you find yourself including a library and eventually using only a very small part of it (e.g. a single utility class or even method), consider pulling the particular part of the library out by copying the source code of what you’re interested in. This of course has to be done with respect to the library licence and only if the source code is available.

Native libraries

Native libraries usage migh bring a big performance increase to your application in specific cases. They often however also bring a huge increase in the APK size of your application because the native code has to be available for all the ABIs (CPU architectures) in the APK.

Let’s have a look at how to reduce APK size with native libraries.

Disable native libraries extraction

When an application is installed on a phone, the compressed library files (*.so) are copied out to the /data partition. This allows fast access to the library code without the need to decompress them each time they are needed. However, the original compressed version of the libraries is still inside of the APK (they cannot be deleted from the APK because this would break the signature).

Therefore, to save space, Android M came with a new way of accessing these native libraries directly from within the APK package. To enable this feature, add the following flag to your AndroidManifest.xml :

<application
   android:extractNativeLibs=”false”
   ...
>

There are a few conditions though. First, the native library files must be stored in the APK uncompressed. And second, they must be page aligned too. Both of these steps happen automatically starting from Android Studio 2.2 Preview 2. So make sure you’re using the newest available version in order to have everything working automatically.

An additional benefit of using the uncompressed native libraries in your APKs is that it will help Google Play Store produce smaller update packages of your application (because of more efficient delta algorithm computation). The initial download size of your application will stay roughly the same thanks to the compression algorithm that is used for the traffic from Google Play.

The size of installed application will only be smaller on Android Marshmallow and newer only though because older versions of Android systems aren’t able to recognize the new flag in the manifest file.

Split APKs

Having all of the versions of native library code for every ABI in your APK might add a lot of unnecessary space to your application’s APK. To reduce APK size, you might want to try to use the APK ABI splits and have a single APK for each CPU architecture.

In order to do that, add the following lines (or similar depending on your preferences) to the build.gradle file:

splits {
    abi {
        enable true
        reset()
        include 'x86', 'armeabi-v7a', 'mips'
        universalApk false
    }
}

The universalApk option, when enabled, builds also the universal APK with all the ABIs included.

Remember though, that you have to provide a different version numbers for each of the split APK. You should also make sure that you assign a higher version number to x86_64 and x86 architectures as many x86 devices can also run the ARM code through an emulation layer (which is slower).

Here is a code to help you assign a correct version numbers to your builds:

project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]

android.applicationVariants.all { variant ->
	// assign different version code for each output
	variant.outputs.each { output ->
		output.versionCodeOverride =
				project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
	}
}

Conclusion

In this post, I’ve presented several tips on how to reduce APK size of your application including tips on reducing the size of your source code, dependency analysis, and tips on saving space when using native libraries.

For more tips on how to reduce APK size, please see my previous post on graphics resources optimization.

Sources

Share this: