Putting Your APKs on Diet

原文地址:http://cyrilmottier.com/2014/08/26/putting-your-apks-on-diet/

It’s no secret to anyone, APKs out there are getting bigger and bigger. While simple/single-task apps were 2MB at the time of the first versions of Android, it is now very common to download 10 to 20MB apps. The explosion of APK file size is a direct consequence of both users expectations and developers experience acquisition. Several reasons explain this dramatic file size increase:

  • The multiplication of dpi categories ([l|m|tv|h|x|xx|xxx]dpi)
  • The evolution of the Android platform, development tools and the libraries ecosystem
  • The ever-increasing users' expectations regarding high quality UIs
  • etc.

Publishing light-weight applications on the Play Store is a good practice every developer should focus on when designing an application. Why? First, because it is synonymous with a simple, maintainable and future-proof code base. Secondly, because developers would generally prefer staying below the Play Store current 50MB APK limit rather than dealing with download extensions files. Finally because we live in a world of constraints: limited bandwidth, limited disk space, etc. The smaller the APK, the faster the download, the faster the installation, the lesser the frustration and, most importantly, the better the ratings.

In many (not to say all) cases, the size growth is mandatory in order to fulfill the customer requirements and expectations. However, I am convinced the weight of an APK, in general, grows at a faster pace than users expectations. As a matter of fact, I believe most apps on the Play Store weight twice or more the size they could and should. In this article, I would like to discuss about some techniques/rules you can use/follow to reduce the file size of your APKs making both your co-workers and users happy.

The APK file format

Prior to looking at some cool ways to reduce the size of our apps, it is mandatory to first understand the actual APK file format. Put simply, an APK is an archive file containing several files in a compressed fashion. As a developer, you can easily look at the content of an APK just by unzipping it with the unzip command. Here is what you usually get when executing unzip <your_apk_name>.apk1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/assets /lib  /armeabi  /armeabi-v7a  /x86  /mips /META-INF  MANIFEST.MF  CERT.RSA  CERT.SF /res AndroidManifest.xml classes.dex resources.arsc

Most of the directories and files shown above should look familiar to developers. They mostly reflect the project structure observed during the design & development process: /assets/lib/resAndroidManifest.xml. Some others are quite exotic at first sight. In practice, classes.dex, contains the dex compiled version of you Java code while resources.arscincludes precompiled resources e.g. binary XML (values, XML drawables, etc.).

Because an APK is a simple archive file, it means it has two different sizes: the compressed file size and the uncompressed one. While both sizes are important, I will mainly focus on the compressed size in this article. In fact, a great rule of thumb is to consider the size of the uncompressed version to be proportional to the archive: the smaller the APK, the smaller the uncompressed version.

Reducing APK file size

Reducing the file size of an APK can be done with several techniques. Because each app is different, there is no absolute rule to put an APK on diet. Nevertheless, an APK consists of 3 significant components we can easily act on:

  • Java source code
  • resources/assets
  • native code

The tips and tricks below all consist on minimizing the amount of space used per component reducing the overall APK size in the process.

Have a good coding hygiene

It probably seems obvious but having a good coding hygiene is the first step to reducing the size of your APKs. Know your code like the back of one’s hand. Get rid of all unused dependency libraries. Make it better day after day. Clean it continuously. Focusing on keeping a clean and up-to-date code base is generally a great way to produce small APKs that only contain what is strictly essential to the app.

Maintaining an unpolluted code base is generally easier when starting a project from scratch. The older the project is, the harder it is. As a fact, projects with a large historical background usually have to deal with dead and/or almost useless code snippets. Fortunately some development tools are here to help you do the laundry…

Run Proguard

Proguard is an extremely powerful tool that obfuscates, optimizes and shrinks your code at compile time. One of its main feature for reducing APKs size is tree-shaking. Proguard basically goes through your all of your code paths to detect the snippets of code that are unused. All the unreached (i.e. unnecessary) code is then stripped out from the final APK, potentially radically reducing its size. Proguard also renames your fields, classes and interfaces making the code as light-weight as possible.

As you may have understood, Proguard is extremely helpful and efficient. But with great responsibilities comes great consequences. A lot of developers consider Proguard as an annoying development tool because, by default, it breaks apps heavily relying on reflection. It’s up to developers to configure Proguard to tell it which classes, fields, etc. can be processed or not.

Use Lint extensively

Proguard works on the Java side. Unfortunately, it doesn’t work on the resources side. As a consequence, if an image my_image in res/drawable is not used, Proguard only strips it’s reference in the R class but keeps the associated image in place.

Lint is a static code analyzer that helps you to detect all unused resources with a simple call to ./gradlew lint. It generates an HTML-report and gives you the exhaustive list of resources that look unused under the “UnusedResources: Unused resources” section. It is safe to remove these resources as long as you don’t access them through reflection in your code.

Lint analyzes resources (i.e. files under the /res directory) but skips assets (i.e. files under the/assets directory). Indeed, assets are accessed through their name rather than a Java or XML reference. As a consequence, Lint cannot determine whether or not an asset is used in the project. It is up to the developer to keep the /assets folder clean and free of unused files.

Be opinionated about resources

Android supports a very large set of devices at its core. In fact, Android has been designed to support devices regardless of their configuration: screen density, screen shape, screen size, etc. As of Android 4.4, the framework natively supports various densities: ldpi, mdpi, tvdpi, hdpi, xhdpi, xxhdpi and xxxhdpi. Android supporting all these densities doesn’t mean you have to export your assets in each one of them.

Don’t be afraid of not bundling some densities into your application if you know they will be used by a small amount of people. I personally only support hdpi, xhdpi and xxhdpi2 in my apps. This is not an issue for devices with other densities because Android automatically computes missing resources by scaling an existing resource.

The main point behind my hdpi/xhdpi/xxhdpi rule is simple. First, I cover more than 80% of my users. Secondly xxxhdpi exists just to make Android future-proof but the future is not now (even if it’s coming very quickly…). Finally I actually don’t care about the crappy/low-res densities such as mdpi or ldpi. No matter how hard I work on these densities, the result will look as horrible as letting Android scaling down the hdpi variant.

On a same note, having a single variant of an image in drawable-nodpi also can save you space. You can afford to do that if you don’t think scaling artifacts are outrageous or if the image is displayed very rarely throughout the app on day-to-day basis.

Minimize resources configurations

Android development often relies on the use of external libraries such as Android Support Library, Google Play Services, Facebook SDK, etc. All of theses libraries comes with resources that are not necessary useful to your application. For instance, Google Play Services comes with translations for languages your own application don’t even support. It also bundles mdpi resources I don’t want to support in my application.

Starting Android Gradle Plugin 0.7, you can pass information about the configurations your application deals with to the build system. This is done thanks to the resConfig and resConfigs flavor and default config option. The DSL below prevents aapt from packaging resources that don’t match the app managed resources configurations:

build.gradle
1
2
3
4
5
6
defaultConfig {  // ...   resConfigs "en", "de", "fr", "it"  resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi" } 

Compress images

Aapt comes with a lossless image compression algorithm. For instance, a true-color PNG that does not require more than 256 colors may be converted to an 8-bit PNG with a color palette. While it may reduce the size of your resources, it shouldn’t prevent you from embracing the lossy PNG preprocessor optimization path. A quick Google search yields several tools such as pngquant, ImageAlpha or ImageOptim. Just pick the one that best fits your designer workflow and requirements and use it!

A special type of Android-only images can also be minimized: 9-patches. As far as I know, no tools have been specifically created for this. However, this can be done fairly easily just by asking your designer to reduce the stretchable and content areas to a minimum. In addition to optimizing the asset weight, it will also make the assets maintenance way easier in the long term.

Limit the number of architectures

Android is generally about Java but there are some rare cases where applications need to rely on some native code. Just like you should be opinionated about resources, you should too when it comes to native code. Sticking to armabi and x86 architecture is usually enough in the current Android eco-system. Here is an excellent article about native libraries weight reduction.

Reuse whenever possible

Reusing stuff is probably one of the first important optimization you learn when starting developing on mobile. In a ListView or a RecyclerView, reusing helps you keep a smooth scrolling performance. But reusing can also help you reduce the final size of your APK. For instance, Android provides several utilities to re-color an asset either using the new android:tint and android:tintMode on Android L or the good old ColorFilter on all versions.

You can also prevent packaging resources that are only a rotated equivalent of another resource. Let’s say you have 2 images named ic_arrow_expand and ic_arrow_collapse :

You can easily get rid of ic_arrow_collapse by creating a RotateDrawable relying on ic_arrow_expand. This technique also reduces the amount of time your designer requires to maintain and export the collapsed asset variant:

res/drawable/ic_arrow_collapse.xml
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?> <rotate xmlns:android="http://schemas.android.com/apk/res/android"  android:drawable="@drawable/ic_arrow_expand"  android:fromDegrees="180"  android:pivotX="50%"  android:pivotY="50%"  android:toDegrees="180" /> 

Render in code when appropriate

In some cases rendering graphics directly for the Java code can have a great benefit. One of the best example of a mammoth weight gain is with frame-by-frame animations. I’ve been struggling with Android Wear development recently and had a look at the Android wearable support library. Just like the regular Android support library, the wearable variant contains several utility classes when dealing with wearable devices.

Unfortunately, after building a very basic “Hello World” example, I noticed the resulting APK was more than 1.5MB. After a quick investigation into wearable-support.aar, I discovered the library bundles 2 frame-by-frame animations in 3 different densities: a “success” animation (31 frames) and an “open on phone” animation (54 frames).

The frame-by-frame success animation is built with a simple AnimationDrawable defined in an XML file:

res/drawable/confirmation_animation.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?> <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true">  <item android:drawable="@drawable/generic_confirmation_00163" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00164" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00165" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00166" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00167" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00168" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00169" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00170" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00171" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00172" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00173" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00174" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00175" android:duration="333"/>  <item android:drawable="@drawable/generic_confirmation_00185" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00186" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00187" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00188" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00189" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00190" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00191" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00192" android:duration="33"/>  <item android:drawable="@drawable/generic_confirmation_00193" android:duration="33"/> </animation-list> 

The good point is (I’m being sarcastic of course) that each frame is displayed for a duration of 33ms making the animation run at 30fps. Having a frame every 16ms would have ended up with a library twice larger… It gets really funny when you continue digging in the code. The generic_confirmation_00175 frame (line 15) is displayed for a duration of 333ms. generic_confirmation_00185 follows it. This is a great optimization that saves 9 similar frames (176 to 184 included) from being bundled into application. Unfortunately, I was totally disappointed to see that wearable-support.aar actually contains all of these 9 completely unused and useless frames in 3 densities.3

Doing this animation in code obviously requires development time. However, it may dramatically reduce the amount of assets in your APK while maintaining a smooth animation running at 60fps.. At the time of the writing, Android doesn’t provide a easy tool to render such animations. But I really hope Google is working on a new light-weight real-time rendering system to animate all of these tiny details that material design is so fond of. An “Adobe After Effect to VectorDrawable” designer tool or equivalent would help a lot.

Going even further ?

All of the techniques described above mainly target the app/library developers side. Could we go further if we had total control over the distribution chain? I guess we could but that would mainly involve some work server-side or more specifically Play Store-side. For instance, we could imagine a Play Store packaging system that bundles only the native libraries required for the target device.

On a similar note, we could imagine only packaging the configuration of the target device. Unfortunately that would completely break one of the most important functionalities of Android: configuration hot-swapping. Indeed, Android has always been designed to deal with live configuration changes (language, orientation, etc.). For instance, removing resources that are not compatible with the target screen density would be a great benefit. Unfortunately, Android apps are able to deal on the fly with a screen density change. Even though we could imagine deprecating this capability, we would still have to deal with drawables defined for a different density than the target density as well as those having more than a single density qualifier (orientation, smallest width, etc.).

Server-side APK packaging looks extremely powerful. But is is also very risky because the final APK delivered to the user would be completely different from the one sent to the Play Store. Delivering an APK with some missing resources/assets would just break apps.

Conclusion

Designing is all about getting the best out of a set of constraints. The weight of an APK file is clearly one of these constraints. Don’t be afraid of pulling the strings out of one apsect of your application to make some other better in some ways. For instance, do not hesitate to reduce the quality of the UI rendering if it reduce the size of the APK and make the app smoother. 99% of your users won’t even notice the quality drop while they will notice the app is light-weight and smooth. After all, your application is judged as a whole, not as a sum of severed aspects.

Thanks to Frank Harper for reading drafts of this

  • 1 The .aar library extension is a pretty similar archive. The only difference being that the files are stored in a regular non-compiled jar/xml form. Resources and Java code are actually compiled at the very moment the Android application using them is built.

  • 2 There is just one optional exception to this rule: the launcher icon. The new Google experience launcher relies on the density “above” the current screen density to render the icon on the launcher. Thus, I always bundle an xxxhdpi version of this icon.

  • 3 I personally consider this as a huge flaw in the Android wearable support library and decided not to use it. I couldn’t afford adding a 1.5MB Android Wear app to my 3.5MB Android app (especially knowing it is sent to devices probably not having a connected Android Wear device). As a solution, I re-implemented on my own the only interesting utilities.

你可能感兴趣的:(android,apk)