banner
jzman

jzman

Coding、思考、自觉。
github

Advanced Configuration of Android Gradle in the Gradle Series

This article mainly builds on previous learning, focusing on how to customize Android Gradle from a practical development perspective to meet different development needs. Below are several articles in the Gradle series:

The main content is as follows:

  1. Modify the generated Apk file name
  2. Unified management of version information
  3. Hide signature file information
  4. Dynamically configure the AndroidManifest file
  5. Customize BuildConfig
  6. Dynamically add custom resources
  7. Java compilation options
  8. ADB operation options configuration
  9. DEX options configuration
  10. Automatically clean up unused resources
  11. Break the 65535 method limit

Modify the generated Apk file name#

Modifying the output file name of the Apk mainly uses three properties:

applicationVariants // Android application Gradle plugin
libraryVariants     // Android library Gradle plugin
testVariants        // Applicable to both of the above plugins

Below is the code to modify the generated Apk file name:

android {
    //...
    
    /**
     * Modify the file name of the generated apk
     */
    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            if (output.outputFile != null && output.outputFile.name.endsWith('.apk') &&
                    'release' == variant.buildType.name) {
                // Output file name
                outputFileName = "AndroidGradleProject_v${variant.versionName}_${buildTime()}.apk"
            }
        }
    }   
}
// Current time
def static buildTime() {
    def date = new Date()
    return date.format("yyyMMdd")
}

At this point, when executing the task to build the Apk in release mode, the name of the generated Apk has been modified. Of course, you can also configure the corresponding file name to be generated in debug mode, etc.

Unified management of version information#

Every application has a version, which generally consists of three parts: major.minor.patch. The first is the major version number, the second is the minor version number, and the third is the patch number, such as version numbers in the format of 1.0.0. In Android development, the most primitive version configuration method is to configure the corresponding version number and version name in the defaultConfig in build.gradle, as follows:

// The most primitive version configuration method
android {
    defaultConfig {
        versionCode 1
        versionName "1.0"
        //...
    }
}

In actual development, this version-related information is generally defined separately in an independent version management file for unified management. Define the version.gradle file as follows:

ext {
    // Application version number, version name
    appversionCode = 1
    appVersionName = "1.0"
    // Other version numbers...
}

Then, in build.gradle, use the version number and version name defined in the version.gradle file, as follows:

// Import version.gradle file
apply from: "version.gradle"
android {
    //...
    defaultConfig {
        // Use the version number defined in version.gradle
        versionCode appversionCode
        // Use the version name defined in version.gradle
        versionName appVersionName
        //...
    }
}

Of course, it is not only the application version number; the versions of some third-party libraries used can also be managed in this way.

Hide signature file information#

Signature file information is very important. If the signature file information is directly configured into the project, it will be unsafe. So how can the signature file be kept safe? Storing the signature file locally is not safe, so it must be stored on a server to be secure. During packaging, the signature file information can be read from the server. Of course, this server can also be a computer specifically used for packaging the official Apk. The signature file and key information can be configured as environment variables, and the packaging process can directly read the signature file and key information from the environment variables.

Configure four environment variables: STORE_FILE, STORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD, corresponding to the signature file, signature file password, signature file key alias, and signature file key password. The configuration of environment variables will not be detailed here. The code is as follows:

android {
    // Signature file configuration
    signingConfigs {
        // Read the configured environment variables corresponding to the signature file information
        def appStoreFile = System.getenv('STORE_FILE')
        def appStorePassword = System.getenv('STORE_PASSWORD')
        def appKeyAlias = System.getenv('KEY_ALIAS')
        def appKeyPassword = System.getenv('KEY_PASSWORD')
        // If the relevant signature file information cannot be obtained, use the default signature file
        if (!appStoreFile || !appStorePassword || !keyAlias || !keyPassword) {
            appStoreFile = "debug.keystore"
            appStorePassword = "android"
            appKeyAlias = "androiddebugkey"
            appKeyPassword = "android"
        }
        release {
            storeFile file(appStoreFile)
            storePassword appStorePassword
            keyAlias appKeyAlias
            keyPassword appKeyPassword
        }
        debug {
            // By default, the signature in debug mode is configured as the debug signature file certificate automatically generated by the Android SDK
            //.android/debug.keystore
        }
    }
}

Note that after configuring the environment variables, if the new configured environment variables cannot be read, restarting the computer will allow them to be read. As for how to use a dedicated server for packaging and reading signature file information, it will be introduced later.

Dynamically configure the AndroidManifest file#

Dynamically configuring the AndroidManifest file means dynamically modifying some content in the AndroidManifest file. For example, when using third-party statistical platforms like Umeng, it generally requires specifying the channel name in the AndroidManifest file, as shown below:

<meta-data android:value="CHANNEL_ID" android:name="CHANNEL"/>

Here, CHANNEL_ID needs to be replaced with different channel names, such as baidu, miui, etc. So how can we dynamically modify these changing parameters? This requires the use of manifest placeholders and manifestPlaceholder. The manifestPlaceholder is a property of ProductFlavor and is a Map type that can configure multiple placeholders. The specific code is as follows:

android {
    // Dimensions
    flavorDimensions "channel"
    productFlavors {
        miui {
            dimension "channel"
            manifestPlaceholders.put("CHANNEL", "google")
        }
        baidu {
            dimension "channel"
            manifestPlaceholders.put("CHANNEL", "baidu")
        }
    }
}

The above code configures the flavorDimensions property, which can be understood as a dimension. For example, release and debug are one dimension, different channels are another dimension, and free or paid versions are yet another dimension. If all three dimensions need to be considered, the format for generating Apk would be 2 * 2 * 2, resulting in 8 different Apks. Starting from Gradle 3.0, whether it is one dimension or multiple dimensions, flavorDimensions must be used for constraints. The code above defines a dimension called channel, and combined with the debug and release in buildType, the number of different Apks generated is 4, as shown in the following image:

image

Of course, if flavorDimensions is not configured, the following error will occur:

Error: All flavors must now belong to a named flavor dimension.

In actual development, configure the corresponding flavorDimensions according to the actual situation.

Then, in the AndroidManifest file, use placeholders to introduce the parameters passed during packaging. Add in the AndroidManifest file as follows:

<meta-data android:value="${CHANNEL}" android:name="channel"/>

Finally, execute the corresponding channel package task. For example, executing assembleBaiduRelease will replace the channel in the AndroidManifest with baidu. You can execute the command or use Android Studio to select the corresponding task to execute. The command is as follows:

gradle assembleBaiduRelease

If using Android Studio, open the right Gradle control panel, find the corresponding task to execute, as shown in the following image:

image

Selecting the corresponding task to execute will generate the corresponding Apk. Use Android Killer to decompile and open the generated Apk, and view the AndroidManifest file as follows:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.manu.androidgradleproject">
    <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" roundIcon="@mipmap/ic_launcher_round">
        <!-- AndroidManifest file modification successful -->
        <meta-data android:name="channel" android:value="baidu"/>
        <activity android:name="com.manu.androidgradleproject.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <meta-data android:name="android.support.VERSION" android:value="26.1.0"/>
        <meta-data android:name="android.arch.lifecycle.VERSION" android:value="27.0.0-SNAPSHOT"/>
    </application>
</manifest>

In the above example, the channel name is consistent, and the replacement of channel names can be conveniently completed through iteration, as follows:

productFlavors.all { flavor ->
    manifestPlaceholders.put("CHANNEL", name)
}

An important point in this section is the use of manifestPlaceholders.

Customize BuildConfig#

BuildConfig is a class generated after compiling the Android Gradle build script. The default content generated in BuildConfig is as follows:

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.manu.androidgradleproject;

public final class BuildConfig {
  public static final boolean DEBUG = false;
  public static final String APPLICATION_ID = "com.manu.androidgradleproject";
  public static final String BUILD_TYPE = "release";
  public static final String FLAVOR = "baidu";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
}

Some constants in the above BuildConfig are key information about the application, where DEBUG is true in debug mode and false in release mode. Additionally, there are the application package name, build type, build channel, version number, and version name. Therefore, if these values are needed during development, they can be directly obtained from BuildConfig. For example, obtaining the package name is generally done using context.getPackageName(). If you can directly get it from BuildConfig, it is not only convenient but also beneficial for application performance improvement. Therefore, some additional useful information can be added to this file during the build using the buildConfigField method, as follows:

/**
 * type: The type of the generated field
 * name: The constant name of the generated field
 * value: The constant value of the generated field
 */
public void buildConfigField(String type, String name, String value) {
    //...
}

Below, the buildConfigField method is used to configure a related address for each channel, as follows:

android {
    // Dimensions
    flavorDimensions "channel"
    productFlavors {
        miui {
            dimension "channel"
            manifestPlaceholders.put("CHANNEL", "miui")
            buildConfigField 'String', 'URL', '"http://www.miui.com"'
        }
        baidu {
            dimension "channel"
            manifestPlaceholders.put("CHANNEL", "baidu")
            // The content in the value parameter of the buildConfigField method is in single quotes. If the value is String, the double quotes of String cannot be omitted.
            buildConfigField 'String', 'URL', '"http://www.baidu.com"'
        }
    }
}

When packaging, the added fields will be automatically generated. After the build is complete, check the BuildConfig file, and the added fields will be generated, as follows:

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.manu.androidgradleproject;

public final class BuildConfig {
  public static final boolean DEBUG = false;
  public static final String APPLICATION_ID = "com.manu.androidgradleproject";
  public static final String BUILD_TYPE = "release";
  public static final String FLAVOR = "baidu";
  public static final int VERSION_CODE = -1;
  public static final String VERSION_NAME = "";
  // Fields from product flavor: baidu
  public static final String URL = "http://www.baidu.com";
}

Thus, the study of customizing BuildConfig comes to an end. Of course, buildConfigField can also be used in build types; the key is the use of the buildConfigField method.

Dynamically add custom resources#

In Android development, resource files are placed in the res directory, and custom resources can also be defined in Android Gradle. Custom resources require the use of the resValue method, which can be used in BuildType and ProductFlavor objects. The resValue method generates the corresponding resource, and its usage is the same as defining in res/values files:

android {
    //...
    productFlavors {
        miui {
            //...
           /**
            * resValue(String type, String name, String value)
            * type: The type of the generated field (id, string, bool, etc.)
            * name: The constant name of the generated field
            * value: The constant value of the generated field
            */
            resValue 'string', 'welcome', 'miui'
        }

        baidu {
            //...
            resValue 'string', 'welcome', 'baidu'
        }
    }
}

When generating different channel packages, the value obtained through R.string.welcome will be different. For example, when generating the Baidu channel package, the value of R.string.welcome will be baidu, and when generating the Xiaomi channel package, the value of R.string.welcome will be miui. The resources generated during the build are located in build/generated/res/resValues/baidu/... under the generated.xml file, with the content as follows:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- Automatically generated file. DO NOT MODIFY -->

    <!-- Values from product flavor: baidu -->
    <string name="welcome" translatable="false">baidu</string>

</resources>

Java compilation options#

In Android Gradle, you can also configure the Java source code compilation version using the compileOptions method. The compileOptions can configure three properties: encoding, sourceCompatibility, and targetCompatibility. These properties are used to configure Java-related compilation options, as follows:

// Configure Java compilation options
android {
    compileSdkVersion 26
    buildToolsVersion '26.0.2'
    compileOptions {
        // Set the encoding of source files
        encoding = 'utf-8'
        // Set the compilation level of Java source code
        sourceCompatibility = JavaVersion.VERSION_1_8
//        sourceCompatibility  "1.8"
//        sourceCompatibility  1.8
//        sourceCompatibility  "Version_1_8"
        // Set the version of Java bytecode
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

ADB operation options configuration#

ADB stands for Android Debug Bridge, which is mainly used to connect to phones for various operations, such as debugging Apk, installing Apk, copying files, etc. In Android Gradle, you can use adbOptions to configure it, which has two configurable properties: installOptions and timeOutInMs. You can also set them using the corresponding setter methods, as follows:

android {
    // ADB configuration options
    adbOptions {
        // Set the timeout for executing adb commands
        timeOutInMs = 5 * 1000
        /**
         * Set the installation options for adb install
         * -l: Lock the application
         * -r: Replace the existing application
         * -t: Allow test packages
         * -s: Install the application on the SD card
         * -d: Allow downgrading the application
         * -g: Grant all runtime permissions to the application
         */
        installOptions '-r', '-s'
    }    
}

The configuration of installOptions corresponds to the adb install [-lrtsdg] command. If a CommandRejectException occurs when installing, running, or debugging the Apk, you can try setting timeOutInMs to resolve it, with the unit in milliseconds.

DEX options configuration#

In Android, the source code is compiled into class bytecode, and when packaging into an Apk, it is optimized into DEX files executable by the Android virtual machine using the dx command. The DEX format is specifically designed for the Android virtual machine and can improve its running speed to some extent. By default, the memory allocated to dx is 1024M. In Android Gradle, you can configure DEX using five properties of dexOptions: incremental, javaMaxHeapSize, jumboMode, threadCount, and preDexLibraries, as follows:

android {
    // DEX options configuration
    dexOptions {
        // Set whether to enable dx incremental mode
        incremental true
        // Set the maximum heap memory allocated to the dx command
        javaMaxHeapSize '4g'
        // Set whether to enable jumbo mode. If the number of methods in the project exceeds 65535, jumbo mode must be enabled to build successfully.
        jumboMode true
        // Set the number of threads used by Android Gradle when running the dx command, which can improve the efficiency of dx execution.
        threadCount 2
        /**
         * Set whether to execute dex Libraries library projects. Enabling this will improve the speed of incremental builds but will affect the speed of clean. The default is true.
         * Use the dx --multi-dex option to generate multiple dex files. To avoid conflicts with library projects, it can be set to false.
         */
        preDexLibraries true
    }
}

Automatically clean up unused resources#

In Android development, when packaging an Apk, it is always desirable to keep the Apk size as small as possible while maintaining the same functionality. This requires deleting unused resource files before packaging or ensuring that useless resources are not packaged into the Apk. Android Lint can be used to check for unused resources, but it cannot clean up unused resources in some third-party libraries. Resource Shrinking can also be used to check resources before packaging; if they are not used, they will not be packaged into the Apk, as follows:

// Automatically clean up unused resources
android {
    buildTypes {
        release {
            // Enable obfuscation to ensure that certain resources are not used in the code, allowing for automatic cleanup of unused resources. Both should be used together.
            minifyEnabled true
            /**
             * During packaging, all resources will be checked. If they are not referenced, they will not be packaged into the Apk, and unused resources from third-party libraries will be processed.
             * Disabled by default.
             */
            shrinkResources true
            // Enable zipalign optimization
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
        }
    }
    //...
}

To prevent useful resources from being excluded from the Apk, Android Gradle provides the keep method to configure which resources should not be cleaned up. Create an XML file under res/raw/ to use the keep method, as follows:

<!-- keep.xml file -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/l_used"
    tools:shrinkMode="safe"/>

Three configurable properties: keep indicates the resource files to be retained, which can be a list of resources separated by commas, and (*) can be used as a wildcard. Discard indicates the resources to be removed, similar to keep, and shrinkMode is used to set the mode for automatically cleaning resources, which is generally set to safe. If set to strict, it may remove resources that might be used.

Additionally, you can use the methods resConfigs and resConfig provided by ProductFlavor to configure which resources are packaged into the Apk. The usage is as follows:

android {
    defaultConfig {
       // Parameters can be Android development resource qualifiers
        resConfigs 'zh'
        //...
    }
}

The above method of automatically cleaning resources only prevents them from being packaged into the Apk; they are not actually removed from the project. You can check the logs to see which resources were cleaned up and then decide whether to remove them from the project.

Break the 65535 method limit#

In Android development, exceptions occur when the number of methods exceeds 65535. Why is there this limit? Because Java source files are packaged into a single DEX file, which is an optimized file executable on the Dalvik virtual machine. When executing DEX files, Dalvik uses a short to index the methods in the DEX file, meaning that a single DEX file can define a maximum of 65535 methods. The solution is to create multiple DEX files when the number of methods exceeds 65535.

Starting from Android 5.0, the Android system uses ART for execution, which natively supports multiple DEX files. ART performs pre-compilation when installing apps, merging multiple DEX files into a single oat file for execution. Before Android 5.0, the Dalvik virtual machine only supported a single DEX file. To break the limit of more than 65535 methods in a single DEX, the Multidex library must be used, which will not be elaborated on here.

Summary#

This article contains many contents that can be applied in actual development. It was completed while learning and verifying, taking about a week intermittently. It has been a week since the last update. I hope this article can be helpful to you.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.