June 17, 2018

    Optimizing Android resources

    The Android framework provides an extensive mechanism for managing resources that enables app developers to separate content and external files from their application code and maintain them independently. Resources can be made configuration-aware so that strings are localized and UI layouts are adapted at runtime based on the language settings and screen properties of the device the app is running on.

    The heart of all this is the resources.arsc file that is compiled by the Android build tool chain alongside the actual code. The resources.arsc file contains simple values, like integers, booleans and strings, as well as references to more complex resources, like UI layouts which are stored in separate binary XML files.

    In this blog, we look closer at the inner structure of the resource.arsc file and discuss how its footprint can be reduced.

    Resources under the hood

     The compiled resources.arsc file that can be found inside a built .apk is a binary file in a proprietary format. Its contents can be printed out in a human-readable format using the aapt command that is part of the Android Sdk.

    aapt dump --values resources  app-release.apk

    Here's an abridged snippet of what the resources of a simple mail app could look like.

    Package 0 id=0x7f name=com.example.mail
     ...
     type 5 configCount=2 entryCount=3
      config (default):
       resource 0x7f060000 com.example.mail:string/account_settings: (string8) "Account settings"
       resource 0x7f060001 com.example.mail:string/email_address: (string8) "Email address"
       resource 0x7f060002 com.example.mail:string/password: (string8) "Password"
      config de:
       resource 0x7f060000 com.example.mail:string/account_settings: (string8) "Kontotyp"
       resource 0x7f060001 com.example.mail:string/email_address: (string8) "E-Mail-Adresse"
       resource 0x7f060002 com.example.mail:string/password: (string8) "Passwort"
     ...
     type 10 configCount=2 entryCount=2
      config (default):
       resource 0x7f0b0000 com.example.mail:dimen/button_width: (dimension) 100.000000sp
       resource 0x7f0b0001 com.example.mail:dimen/button_height: (dimension) 24.000000sp
      config small-v4:
       resource 0x7f0b0000 com.example.mail:dimen/button_width: (dimension) 80.000000sp
       resource 0x7f0b0001 com.example.mail:dimen/button_height: (dimension) 20.000000sp
     ...
    

    In the above example, we see two types of resources: string and dimension resources.

    String resources usually represent labels that are visible in the UI of the app. In the example, there are three such strings and they have a default value in English as part of “config (default)” and an alternative value in German as part of “config de”.

    Dimension resources usually represent sizes of UI components. In our example, there are two such dimensions that have a default value as part of  “config (default)” and an alternative value for small screen sizes as part of “config small-v4”.

    string
    Resource ID config (default) config de
    0x7f060000 Account settings Kontotyp
    0x7f060001 Email address E-Mail-Adresse
    0x7f060002 Password Passwort

     

    dimen
    Resource ID config (default) config small-v4
    0x7f0b0000 100.0sp 80.0sp
    0x7f0b0001 24.0sp 20.0sp

     

    The Android runtime decides at runtime which value is appropriate for each resource based on the configuration of the environment it is running in. If the runtime configuration doesn’t match any of the specific configurations in the resource table, the Android runtime falls back on the value specified in the default configuration.

    Every advantage has a disadvantage

     It is clear that the resource framework was designed with flexibility and extensibility in mind. It could accommodate new device types and even new resource types in the future without any real structural changes to the format.

    However, the flexibility of the resources.arsc file can come at the cost of efficiency. In a fully featured app, the resources.arsc file can easily be a few megabytes in size. And when we look closer, we can see that not all of these bytes are necessarily bytes well spent.

    Something that can enlarge the footprint of the resources drastically is using external libraries in an app. Let’s imagine that we want to incorporate Google Drive integration in the simple mail app we used as an example before.

    We can simply add a dependency to the Google Drive API in our build.gradle file and start coding away.

        dependencies {
            compile 'com.google.android.gms:play-services-drive:7.8.0'
        }
    

    Not only will this instantly increase the number of resources by about 200 entries (many of which we might not even need), it will also significantly increase the number of configurations.

    The APIs from Google Play Services conveniently provide localized strings for dozens of languages. Yet, the string resources of the mobile application itself come only in one language (German) in addition to the default (English). For the unsupported languages, there will still be empty strings added to the the resources.arc file.

    Another source of redundancy is the duplication of resources. String resources are not prone to duplication since the resources.arsc file stores each unique string in a string pool. String, xml, drawable and layout resources simply refer to the corresponding entry in the string pool. Numerical values like dimensions, integers and fractions, however, can repeat themselves and you might end up with hundreds of 12.0 dip, 20.0 sp, etc. entries.

    Lastly, the resource names (e.g. account_settings) are present in the resource table even though most application code doesn’t rely on the name of the resource to retrieve its value. Instead, it uses the identifier from the R class (e.g. R.id.account_settings = 0x7f060001). Because of this, most of these resource names are not required at runtime and are just taking up space in the string pool.

    Some quick fixes

     The above issues can be solved to some extent by following the recommendations on the Android Developers website.

    You can limit the alternative translations to the languages you want your app to support by specifying the resource configurations in the build.gradle file.

    android {
        defaultConfig {
            ...
            resConfigs "en", "de"
            ...
        }
    }

    The Android Gradle Plugin also has a resource shrinking option that removes resources that are not referenced from code.

    android {
        buildTypes {
            release {
                ...
                shrinkResources true
                ...
            }
        }
    }

    The Android Asset Packaging Tool 2.0 (AAPT2) introduced some improvements to the way the resources are compiled and has built-in deduplication, which is turned on by default.

    In addition to that, AAPT2 also allows for alternative resource encoding more suited to sparse resource tables. It can be enabled with the flag --enable-sparse-encoding. This will replace the usual flat table representation with a more compact key-value lookup structure that can be queried using a binary search. The alternative resource encoding is only available on Android Oreo or later (minSdk>=26), however.

    Migrating your build from AAPT to AAPT2 might shrink the resources.arsc file, but bear in mind that it is also stricter than AAPT at reporting errors at build time.

    Optimizing resources with DexGuard

     To further optimize the resources in your Android applications, you can use a code shrinking tool. ProGuard is very effective at optimizing the compiled Java code of an Android app, but it leaves all the resource files untouched. DexGuard, on the other hand, processes an Android app in its entirety and optimizes every aspect of it, including its resources. Applying DexGuard to your application reduces the size of its resources.arsc file by removing unused resources, avoiding duplication and reorganizing the resources in a way that reduces sparseness.

    Here is a comparison of the size of the resources.arsc file of some open-source apps before and after enabling Dexguard.

    App resources.arsc file size in bytes (without DexGuard) resources.arsc file size in bytes (DexGuard enabled)
    Avare 511.280 309.516 (-39.5%)
    PocketHub 868.952 490.816 (-43.5%)
    RateBeer 494.600 293.524 (-40.7%)
    Stavor 564.124 270.616 (-52.0%)
    Transdroid 776.804 584.432 (-24,8%)

    These kinds of results can be obtained by DexGuard without requiring any additional configuration.

    Guardsquare

    Discover how Guardsquare provides industry-leading protection for Android mobile apps.

    Download the DexGuard factsheet >

    Other posts you might be interested in