文章

01.AGP配置

01.AGP配置

Android Gradle Plugin 使用

Android Gradle Plugin 介绍

Android Gradle 插件分类

Android 插件的分类是根据 Android 工程的属性进行分类的,Android 工程分为三类,具体如下:

  1. App 应用工程:可生成可运行的 apk ;
  2. Library 库工程:可生成 aar 共其他 App 应用工程使用,使用方式和 jar 一样,里面有相关的 Android 资源文件
  3. Test 测试工程:用于 App 应用过程或 Library 工程进行测试。
1
2
3
4
5
6
// App插件id
com.android.application
// Library插件id
com.android.library
// Test插件
com.android.test

Android Gradle 工程目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app
├─libs
├─proguard-rules.pro
├─build.gradle
└─src
    ├─androidTest
    │  └─java
    │   
    ├─main
    │  ├─AndroidManifest.xml
    │  ├─java
    │  └─res
    │     
    └─test
        └─java
  1. src 下面的 main、androidTest、test 是三个 SourceSet
  2. main 表示 App 的源代码及资源目录
  3. androidTest 和 test 表示 Android 相关测试代码目录
  4. proguard-rules.pro 是当前项目的混淆文件
  5. libs 用于存放 jar、aar 等库文件
  6. build.gradle 文件用于当前 module 的配置文件

Android Gradle 插件基础配置

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 使用Android Gradle插件
apply plugin: 'com.android.application'
// android{}是Android工程配置的入口
android {
    /**
     * 指定编译依赖的Android SDK的版本,同
     * compileSdkVersion android-26
     * android.compileSdkVersion = 26
     * android.compileSdkVersion = 'android-26'
     */
    compileSdkVersion 26
    // 指定构建工具的版本,也可以使用属性值buildToolsVersion设置其版本
    buildToolsVersion '26.0.2'
    /**
     * 默认配置,defaultConfig是一个ProductFlavor,可根据不同需求生成不同的Apk
     * 如果不自定义ProductFlavor进行单独配置,则该productFlavor会采用默认配置生成Apk
     * 这些具体配置,如applicationID都是ProductFlavor的属性
     */
    defaultConfig {
        //配置唯一包名
        applicationId "com.manu.androidgradleplugin"
        //最低支持的Android系统版本
        minSdkVersion 19
        //配置应用的目标Android系统版本
        targetSdkVersion 26
        //用于控制应用升级的版本号
        versionCode 1
        //用户看到的版本名称
        versionName "1.0"
        //测试时用到
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    /**
     * buildTypes是一个NamedDomainObjectContainer类型,是一个域对象,类似SourceSet(源集)
     * buildTypes中可自定义需要构建的类型,Gradle会自动创建一个相应的BuildType,如默认的release、debug
     */
    buildTypes {
        release {
            //设置是否为构建类型启用混淆
            minifyEnabled false
            //如果启用混淆则使用相应的混淆文件
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
    }
}
// 依赖配置
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation files('libs/plugin.jar')
}

defaultConfig 默认 ProductFlavor

defaultConfig 是 Android Gradle 配置文件中的一个配置块,defaultConfig 的类型是 ProductFlavor,如果没有自定义 ProductFlavor,则使用默认的 ProductFlavor 来配置 Android 工程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//默认的ProductFlavor配置块
defaultConfig {
    //配置App的包名
    applicationId "com.manu.base"
    //配合App最低支持的Android系统版本,下面两种minSdkVersion的配置效果是一致的
    minSdkVersion 19
    <!--minSdkVersion 'KitKat'-->
    //配置App基于哪个Android SDK开发
    targetSdkVersion 26
    //配置App的内部版本号,一般用于版本升级
    versionCode 1
    //配置App外部版本号,该版本号供用户查看
    versionName "1.0"
    //配置单元测试时使用的Runner
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    //配置测试App的包名
    testApplicationId "com.manu.androidgradleproject.test"
    //使用指定的签名文件配置
    signingConfig signingConfigs.release
}

signingConfigs 签名

配置 App 签名信息的好处无非是防止 App 被恶意篡改,签名可保证 App 的唯一性且只有使用相同签名的后续升级包才能正常安装,在创建完签名文件之后,如果不做配置打包时每次都必须要指定签名文件的密码、别名等,一般 App 开发时在 denug 和 release 模式下时配置不同的签名文件。

  1. 创建签名证书文件
  2. 使用 signConfigs 配置块配置已创建签名证书文件的相关信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 签名文件配置
signingConfigs {
    release{
        // 签名证书文件
        storeFile file("project.jks")
        // 签名证书文件密码
        storePassword "111111"
        // 签名证书密钥别名
        keyAlias "project_jks"
        // 签名证书中密钥密码
        keyPassword "111111"
    }
    debug{
        // 默认情况下,debug模式下的签名已配置为Android SDK自动生成的debug签名文件证书
        // 默认签名文件位置:.android/debug.keystore
    }
}
  1. 使用签名文件配置,在 android{} 下 defaultConfig{} 中使用上述配置,具体如下:
1
2
3
4
5
defaultConfig {
    //...
    //使用指定的签名文件配置
    signingConfig signingConfigs.release
}

除了在 defaultConfig{} 中配置,还可以在分别在 debug 或者是 release 模式下配置不同的签名文件,可在 buildTypes{} 中单独配置配置,具体如下:

1
2
3
4
5
6
7
8
9
10
11
buildTypes {
    release {
        signingConfig signingConfigs.release
        //...
    }
    debug{
        signingConfig signingConfigs.debug
        //...
    }
    //...
}

buildTypes 构建应用的类型

Android Gradle 内置了两种构建类型 debug 和 release,两者区别是前者一般用在调试状态,后者一般用于正式发布状态,其他方面都一样,那么如何增加新的构建类型呢,可直接在 buildTypes{} 中添加要添加的类型即可,buildTypes 接收的参数是一个域对象,添加的构建类型都是 BuildType,所以可以通过 BuildType 的相关属性来配置构建类型,下面是 BuildType 的常见的一些配置属性:

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
buildTypes {
    release {
        //...
    }
    debug{
        //配置签名
        signingConfig signingConfigs.debug
        //配置在当前构建类型下applicationId的后缀,构建生成Apk的包名会在applicationId的基础上添加后缀
        applicationIdSuffix '.debug'
        //配置是否生成一个可供调试的Apk
        denbuggable true
        //配置是否生成一个可供调试jni(c/c++)代码的Apk
        jniDebuggable true
        //是否启用proguard混淆
        minifyEnabled true
        //配置当程序中方法数超过65535个时,是否启用自动拆分多个dex的功能,
        multiDexEnabled true
        //配置proguard混淆使用的配置文件,可配置对个混淆文件
        proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
        //配置是否自动清理未使用的资源,默认为false
        shrinkResources true
        //开启zipalign优化
        zipAlignEnabled true
    }
}

当在 buildTypes{} 中添加新的构建类型之后,Android Gradle 都会自动生成一个 SourceSet,构建 Apk 会从对应的 SourceSet 中进行构建,切记新构建类型的名称不能和已有的相同。且要在 src 目录下为新创建的构建类型添加源代码目录和资源文件等,在创建好构建类型的同时,Android Gradle 插件也会生成对应的 assemble 任务来用于构建该类型的项目,如 release 对应的是 assembleRelease,执行该任务会生成对应的 Apk。

使用混淆

1
2
3
4
5
6
7
8
9
buildTypes {
    release {
        //开启混淆
        minifyEnabled false
        //配置混淆文件
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
   //...
}

启用 zipalign 优化

zipalign 是 Android 提供的一个整理优化 apk 文件的工具,可在一定程度上上提高系统和应用的运行效率,更快的读取 apk 中的资源,降低内存的使用,开启 zipalign 优化只需要在 buildTypes{} 中对应的构建类型下开启 zipalign 优化即可,具体如下:

1
2
3
4
5
6
7
8
buildTypes {
    release {
       //开启zipalign优化
       zipAlignEnabled true
       //''
    }
   //...
}

splits 和 abiFilters 配置

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
26
27
android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'x86', 'arm64-v8a' 
        }
    }
    // rest of your app's logic
    splits {
        abi {
            enable isEnabledSplitApks()
            reset()
            //include 'x86', 'x86_64', 'arm64-v8a', 'armeabi-v7a', 'armeabi'
            include 'armeabi-v7a', 'x86'
            // If you don’t want to generate a universal APK that includes all ABIs.
            universalApk false
        }

        // 默认情况下,Android Studio 将会生成一个包含所有屏幕密度的通用 APK。在此技能中,你能专门排除或包含你想要在app/build.gradle支持的屏幕密度,Android Studio 将会为你生成多个 APK。
        density {
            enable true
            // Specify a list of screen densities which Gradle won't create multiple APKs for
            exclude 'ldpi', 'mdpi'
            // Specify a list of compatible screen size for the manifest
            compatibleScreens 'small', 'normal', 'large', 'xlarge'
        }
    }
}
abiFilters

abiFilters 过滤就是在 splits 的基础之上,再选择打入 universal 中所包含的几种处理器架构的文件夹的

splits 主要是用于打包时的拆包,所以我们需要的是进行 apk 的打包编译
  • enable: 是否启用 ABI 拆分机制
  • reset():重置 ABI 列表为只空字符串,一半和 include、exclude 一起使用,如果要用必须在 include 和 exclude 前面
  • include:指明要包含哪些 ABI,目前使用最多的伪 64 位处理器的 arm64-v8a,其次才是 armeabi-v7a
  • exclude:默认包含下所有 ABI,可以移除一些 ABI
  • universalApk:是否打包一个通用版本,包含所有的 ABI,默认值为 false
举例子
1. 案例 1:未配置 abiFilters,配置了 splits
1
2
3
4
5
6
7
8
splits {
    abi {
        enable true
        reset()
        include 'arm64-v8a','armeabi-v7a'
        universalApk true
    }
}

那么这种情况下打包会生成 3 个 apk:

  • app-arm64-v8a-release.apk,里面只会包含 arm64-v8a 文件夹中的 so 文件
  • app-armeabi-v7a-release.apk,里面只会包含 armeabi-v7a 文件夹中的 so 文件
  • app-universal-release.apk,里面会包含 arm64-v8a 和 armeabi-v7a 文件夹中的 so 文件
2. 案例 2:配置了 abiFilters,且配置了 splits
1
2
3
4
5
6
7
8
9
10
11
ndk {
    abiFilters 'arm64-v8a',"armeabi-v7a"
}
splits {
    abi {
        enable true
        reset()
        include 'arm64-v8a','armeabi-v7a','armeabi'
        universalApk true
    }
}
  1. 如果不配置 abiFilters,那么会生成 4 个 apk,3 个平台各一个,加 1 个所有平台(’arm64-v8a’,’armeabi-v7a’,’armeabi’)so 的 apk
  2. 如果配置了 abiFilters,那么会生成 4 个 apk,3 个平台各一个,加 1 个所有平台(’arm64-v8a’,”armeabi-v7a”)so 的 apk

abiFilters 过滤就是在 splits 的基础之上,再选择打入 universal 中所包含的几种处理器架构的文件夹的

Android Gradle 插件进阶配置

修改生成的 Apk 文件名

修改打包输出的 Apk 的文件名主要用到三个属性:

1
2
3
applicationVariants //Android应用Gradle插件
libraryVariants     //Android库Gradle插件
testVariants        //上述两种插件都适用

修改 apk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
android{
    //...
    /**
     * 修改打包生成的apk的文件名
     */
    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            if (output.outputFile != null && output.outputFile.name.endsWith('.apk') && 'release' == variant.buildType.name) {
                //输出文件名
                outputFileName = "AndroidGradleProject_v${variant.versionName}_${buildTime()}.apk"
            }
        }
    }   
}
//当前时间
def static buildTime() {
    def date = new Date()
    return date.format("yyyMMdd")
}

隐藏签名文件信息

  1. 将签名文件和密钥信息配置成环境变量,打包是直接从环境变量中读取签名文件和密钥信息即可。配置四个环境变量 STORE_FILESTORE_PASSWORDKEY_ALIASKEY_PASSWORD 分别对应签名文件、签名文件密码、签名文件密钥别名、签名文件密钥密码
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
26
27
android {
    //签名文件配置
    signingConfigs {
        //读取配置的与签名文件信息对应的环境变量
        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(!appStoreFile || !appStorePassword || !keyAlias || !keyPassword){
            appStoreFile = "debug.keystore"
            appStorePassword = "android"
            appKeyAlias = "androiddebugkey"
            appKeyPassword = "android"
        }
        release {
            storeFile file(appStoreFile)
            storePassword appStorePassword
            keyAlias appKeyAlias
            keyPassword appKeyPassword
        }
        debug {
            //默认情况下,debug模式下的签名已配置为Android SDK自动生成的debug签名文件证书
            //.android/debug.keystore
        }
    }
}

manifestPlaceholders - 动态配置 AndroidManifest 文件

动态配置 AndroidManifest 配置就是动态的去修改 AndroidManifest 文件中的一些内容,如友盟等第三方统计平台分析统计的时候,一般会要求要在 AndroidManifest 文件中指定渠道名称:

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

用 Manifest 占位符和 manifestPlaceholder,manifestPlaceholder 是 ProductFlavor 的一个属性,是一个 Map 类型,可以配置多个占位符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
android{
    //维度
    flavorDimensions "channel"
    productFlavors{
        miui{
            dimension "channel"
            manifestPlaceholders.put("CHANNEL","google")
        }
        baidu{
            dimension "channel"
            manifestPlaceholders.put("CHANNEL","baidu")
        }
    }
}

遍历批量完成渠道名称的替换

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

自定义 BuildConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
android{
    // 维度
    flavorDimensions "channel"
    productFlavors{
        miui {
            dimension "channel"
            manifestPlaceholders.put("CHANNEL","miui")
            buildConfigField 'String' ,'URL','"http://www.miui.com"'
        }
        baidu {
            dimension "channel"
            manifestPlaceholders.put("CHANNEL","baidu")
            //buildConfigField方法参数value中的内容是单引号中的,如果value是String,则String的双引号不能省略
            buildConfigField 'String' ,'URL','"http://www.baidu.com"'
        }
    }
}

也可以配置在 buildType 中

resValue 动态添加自定义资源

Android 开发中资源文件都是放置在 res 目录下,还可以在 Android Gradle 中定义,自定义资源需要使用到 resValue 方法,该方法在 BuildType 和 ProductFlavor 对象中可以使用,使用 resValue 方法会生成相对应的资源,使用方式和在 res/values 文件中定义的一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
android {
    //...
    productFlavors {
        miui {
            //...
           /**
            * resValue(String type,String name,String value)
            * type:生成字段的类型(id、string、bool等)
            * name:生成字段的常量名称
            * value:生成字段的常量值
            */
            resValue 'string', 'welcome','miui'
        }

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

}

compileOptions Java 编译选项

在 Android Gradle 中还可以配置 Java 源代码的编译版本,这里使用到 compileOptions 方法, compileOptions 可配置三个属性:encodingsourceCompatibilitytargetCompatibility,通过这些属性来配置 Java 相关的编译选项,具体参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//配置Java编译选项
android {
    compileSdkVersion 26
    buildToolsVersion '26.0.2'
    compileOptions{
        //设置源文件的编码
        encoding = 'utf-8'
        //设置Java源代码的编译级别()
        sourceCompatibility = JavaVersion.VERSION_1_8
//        sourceCompatibility  "1.8"
//        sourceCompatibility  1.8
//        sourceCompatibility  "Version_1_8"
        //设置Java字节码的版本
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

adb 操作选项设置

adb 的全称是 Android Debug Bridge,adb 主要用来连接手机来进行一些操作,比如调试 Apk、安装 Apk、复制文件等操作,在 Android Gradle 中可借助 adpOptions 来配置,可配置的有两个属性:installOptions 和 timeOutInMs,也可以通过相应的 setter 方法来设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
android {
    // adb配置选项
    adbOptions {
        //设置执行adb命令的超时时间
        timeOutInMs = 5 * 1000
        /**
         * 设置adb install安装这个操作的设置项
         * -l:锁定应用程序
         * -r:替换已存在的应用程序
         * -t:允许测试包
         * -s:把应用程序安装到SD卡上
         * -d:允许应用程序降级安装
         * -g:为该应用授予所有运行时的权限
         */
        installOptions '-r', '-s'
    }    
}

installOptions 的配置对应 adb install [-lrtsdg] 命令,如果安装、运行或调试 Apk 的时候,如果出现 CommandRejectException 可以尝试设置 timeOutInMs 来解决,单位是毫秒。

dexOptions DEX 选项配置

Android 中的源代码被编译成 class 字节码,在打包成 Apk 的时候又被 dx 命令优化成 Android 虚拟机可执行的 DEX 文件,DEX 格式的文件是专为 Android 虚拟机设计的,在一定程度上会提高其运行速度,默认情况下给 dx 分配的内存是 1024M,在 Android Gradle 中可以通过 dexOptions 的五个属性:incrementaljavaMaxHeapSizejumboModethreadCountpreDexLibraries 来对 DEX 进行相关配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
android {
    //DEX选项配置
    dexOptions {
        // 设置是否启用dx增量模式
        incremental true
        // 设置执行dx命令为其分配的最大堆内存
        javaMaxHeapSize '4g'
        // 设置是否开启jumbo模式,如果项目方法数超过65535,需要开启jumbo模式才能构建成功
        jumboMode true
        // 设置Android Gradle运行dx命令时使用的线程数量,可提高dx执行的效率
        threadCount 2
        /**
         * 设置是否执行dex Libraries库工程,开启后会提高增量构建的速度,会影响clean的速度,默认为true
         * 使用dx的--multi-dex选项生成多个dex,为避免和库工程冲突,可设置为false
         */
        preDexLibraries true
    }
}

自动清理未使用资源

在打包之前删除没有使用的资源文件或打包时不将无用的资源打包到 Apk 中,可以使用 Resource Shrinking,可在打包之前检查资源,如果没有使用则不会被打包到 Apk 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 自动清理未使用资源
android {
    buildTypes {
        release {
            //开启混淆,保证某些资源在代码中未被使用,以便于自动清理无用资源,两者配合使用
            minifyEnabled true
            /**
             * 打包时会检查所有资源,如果没有被引用,则不会被打包到Apk中,会处理第三方库不使用的资源
             * 默认不启用
             */
            shrinkResources true
            //开启zipalign优化
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug{
        }
    }
    //...
}

为防止有用资源未被打包到 Apk 中,Android Gradle 提供了 keep 方法来配置那些资源不被清理,在 res/raw/ 下新建一个 xml 文件来使用 keep 方法,参考如下:

1
2
3
4
5
6
<!--keep.xml文件-->
<?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"/>
  1. keep 表示要保留的资源文件,可使用以 (,) 分割的资源列表,可使用 (*) 作为通配符
  2. discard 表示要移除的资源,和 keep 类似
  3. shrinkMode 用于设置自动清理资源的模式,一般设置为 safe 即可,如果设置为 strict 则有可能清除可能会使用的资源
resConfigs 和 resConfig
1
2
3
4
5
6
7
android{
    defaultConfig{
       //参数可以是Android开发时的资源限定符
        resConfigs 'zh'
        //...
    }
}

变体(Variants)的作用

要理解 Variants 的作用,就必须先了解 flavor、dimension 与 variant 这三者之间的关系。在 android gradle plugin V3.x 之后,每个 flavor 必须对应一个 dimension,可以理解为 flavor 的分组,然后不同 dimension 里的 flavor 会组合成一个 variant。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
flavorDimensions "size", "color"
productFlavors {
    JsonChao {
        dimension "size"
    }
    small {
        dimension "size"
    }
    blue {
        dimension "color"
    }
    red {
        dimension "color"
    }
}

在 Android 对 Gradle 插件的扩展支持之中,其中最常用的便是 利用变体(Variants)来对构建过程中的各个默认的 task 进行 hook。关于 Variants 共有 三种类型:

  • applicationVariants:只适用于 app plugin。
  • libraryVariants:只适用于 library plugin。
  • testVariants:在 app plugin 与 library plugin 中都适用。

使用 applicationVariants.all 在配置阶段之后去获取所有 variant 的 name 与 baseName

1
2
3
4
5
6
7
this.afterEvaluate {
    this.android.applicationVariants.all { variant ->
        def name = variant.name
        def baseName = variant.baseName
        println "name: $name, baseName: $baseName"
    }
}

使用 applicationVariants.all 在配置阶段之后去修改输出的 APK 名称

1
2
3
4
5
6
7
8
9
this.afterEvaluate {
    this.android.applicationVariants.all { variant ->
        variant.outputs.each {
            // 由于我们当前的变体是application类型的,所以这个output就是我们  APK  文件的输出路径,我们可以通过重命名这个文件来修改我们最终输出的  APK  文件
            outputFileName = "app-${variant.baseName}-${variant.versionName}.apk"
            println outputFileName
        }
    }
}

对 applicationVariants 中的 Task 进行 Hook

可以在 android.applicationVariants.all 的闭包中通过 variant.task 来获取相应的 Task

获取 checkManifest task

1
2
3
4
5
6
this.afterEvaluate {
    this.android.applicationVariants.all { variant ->
        def task = variant.checkManifest
        println task.name
    }
}

获取 mergeDebugResources task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 收集App中自定义view
 */
project.afterEvaluate {
    println("project.afterEvaluate : ${this.name}, android:${this.android}, applicationVariants:${this.android.applicationVariants}")
    this.android.applicationVariants.all { variant ->
        def buildType = variant.buildType.name.capitalize()
        def mergeResourcesTask = project.tasks.findByName("merge${buildType}Resources")
        println("=========project.afterEvaluate find task buildType=${buildType},$mergeResourcesTask: $mergeResourcesTask")
        if (mergeResourcesTask != null) {
            def resParseTask = project.tasks.create("resParse${buildType}Task", ResParseTask.class)
            resParseTask.buildType = buildType
            mergeResourcesTask.finalizedBy(resParseTask)
        }
    }
}

manifestPlaceholders 的妙用

manifestPlaceholders 可以替换 androidmanifest 文件中的标签,可作为快速渠道打包替换渠道名的一种方式,也可以自定义标签用来替换需要的文本,多作为不同环境不同 key 的修改。
需要实现以下功能:

h68cv

  • 在 androidmanifest 文件配置一个节点
1
2
3
<meta-data
    android:name="UMENG_CHANNEL"
    android:value="${UMENG_CHANNEL_VALUE}" />
  • 不同的包,UMENG_CHANNEL_VALUE 需要配置不同的值,这里引入不同的 buildType
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
android {
    defaultConfig {
        manifestPlaceholders.put("UMENG_CHANNEL_VALUE", "dev") // 渠道
         manifestPlaceholders.put("APPLOG_SCHEME", "rangersapplog.6daa4b17b8965a4f") // applog统计
    }
    
    buildTypes {
        release { // 线上包,发版本用
            // 是否进行混淆
            minifyEnabled true
            // 让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗
            zipAlignEnabled true
            // 移除无用资源
            shrinkResources true
            // 混淆文件的位置
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

            signingConfig signingConfigs.key
            manifestPlaceholders.put("UMENG_CHANNEL_VALUE", "remix") // remix 应用更新
        }
        preview { // 测试包,给测试用
            signingConfig signingConfigs.key
            minifyEnabled true
            zipAlignEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            manifestPlaceholders.put("UMENG_CHANNEL_VALUE", "dev") // 渠道,开测试埋点数据
            manifestPlaceholders.put("APPLOG_SCHEME", "rangersapplog.29c82c0b4ce5f80c") // 区分applog debug统计
        }
        cloundtest { // cloudtest 云测用
            signingConfig signingConfigs.key
            minifyEnabled true
            zipAlignEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            manifestPlaceholders.put("UMENG_CHANNEL_VALUE", "cloudtest") // 渠道
        }
        debug { // 开发包
            signingConfig signingConfigs.key
            minifyEnabled false
            // 停用 PNG 处理
            crunchPngs false

            manifestPlaceholders.put("UMENG_CHANNEL_VALUE", "dev") // 渠道
            manifestPlaceholders.put("APPLOG_SCHEME", "rangersapplog.29c82c0b4ce5f80c") // 区分applog debug统计
        }
    }
}
  • Java 代码获取
1
2
3
4
5
6
7
8
9
10
11
12
public static String getUmengChannel(Context context) {
    String channel = null;
    try {
        ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
        if (appInfo != null && appInfo.metaData != null) {
            channel = appInfo.metaData.get("UMENG_CHANNEL").toString();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return channel;
}

sign signingConfigs 签名配置技巧

1、写死在 build.gradle 文件中

1
2
3
4
5
6
7
8
9
signingConfigs {
// 1、写死在gradle文件中
   release {
      storeFile file("../keystore/bdmobile.keystore")
      storePassword "baidu2010"
      keyAlias "bdmobile.keystore"
      keyPassword "baidu2010"
    }
}

2、System.env 访问

通过 System.env 来访问系统环境变量中的值,这样你就可以把一些私有的内容排除在代码外,这样当你提交代码的时候,就不会泄露这些内容。通过 System.getenv(“JAVA_HOME”) 或者 “要在引号”“内)

1
2
3
4
5
6
7
8
signingConfigs {
   release {
        storeFile "${System.env.PRIVATE_KEY}"
        keyAlias "${System.env.ALIAS}"
       storePassword "${System.env.STORE_PW}"
       keyPassword "${System.env.APP_PW}"
   }
}

3、local.properties

通过 local.properties 配置指定文件来配置 keystore
把私有数据放到 local.properties 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
signingConfigs {
    release {
        Properties keyProps = new Properties()
        keyProps.load(new FileInputStream(file(rootDir.getAbsolutePath() + '/local.properties')))
        println "storeFile:" + keyProps["store"]
        println "keyAlias:" + keyProps["keyAlias"]
        println "storePassword:" + keyProps["storePassword"]
        println "keyPassword:" + keyProps["keyPassword"]
        // 从指定的keystore文件中读取keystore的alias和pass信息
        storeFile keyProps["store"] != null ? file(keyProps["store"]) : null
        keyAlias keyProps["keyAlias"] ?: ""
        storePassword keyProps["storePassword"] ?: ""
        keyPassword keyProps["keyPassword"] ?: ""
    }
}

local.perperties 文件中配置:

1
2
3
4
store=../config/keystore/qsbkvoicechat.keystore
keyAlias=qsbkvoicechat.alias
keyPassword=qsbkvoicechat
storePassword=qsbkvoicechat

keystore 目录:<br

1
			>![jgvfe](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian/jgvfe.png)

4、控制台动态输入

https://testerhome.com/topics/1711

resourcePrefix

则所有资源必须以此 prefix 开头,否则会报错。

1
2
3
4
5
6
android {
    compileSdkVersion config['compileSdkVersion']
//    buildToolsVersion config['buildToolsVersion']

    resourcePrefix "hk_"
}

split 配置

生成不同 ABI 版本 APK 在 build.gradle 中的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
android {
    ... 
    splits {
        abi {
            enable true
            reset()
            include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
            universalApk true //generate an additional APK that contains all the ABIs
        }
    }
    // map for the version code
    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
        }
    }
}

参数:

1
2
3
4
5
enable:启用ABI拆分机制
exclude:默认情况下所有ABI都包括在内,你可以移除一些ABI。
include:指明要包含哪些ABI
reset():重置ABI列表为只包含一个空字符串(这可以实现,在与include一起使用来可以表示要使用哪一个ABI,而不是要忽略哪一些ABI)
universalApk:指示是否打包一个通用版本(包含所有的ABI)。默认值为 false。

配置 debug 不可以用,release 可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
android {
    // rest of your app's logic
    splits {
        abi {
            enable isEnabledSplitApks()
            reset()
            //include 'x86', 'x86_64', 'arm64-v8a', 'armeabi-v7a', 'armeabi'
            include 'armeabi-v7a'//, 'x86'
            universalApk false
        }
    }
}
def isEnabledSplitApks() {
    if (project.hasProperty("disabledSplitApks")) {
        println "spilt不可用"
        return false
    }
    println "spilt可用"
    return true
}

传入参数 disabledSplitApks 表示 split 不可用:

1
./gradlew assembleDebug -P disabledSplitApks

productFlavor 和 buildType 区分不同渠道不同的代码

具体的配置

  1. 配置 Gradle
    app.gradle 配置:
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
26
27
28
android {
    buildTypes {
        debug {
        }
        beta {
        }
        release {
        }
    }
    // 处理app与lib的依赖匹配问题,lib没有的buildType降级到debug
    buildTypes.all { type ->
        type.matchingFallbacks = ['debug']
    }
    flavorDimensions "app"
    productFlavors {
        // 华为渠道
        huawei {
            dimension "app"
            buildConfigField "String", "BUILD_CHANNEL", '"CHANNEL_HW"'
        }
        // Google Play渠道
        google {
            dimension "app"
            isDefault true
            buildConfigField "String", "BUILD_CHANNEL", '"CHANNEL_GOOGLE"'
        }
    }
}
  1. 建立不同 flavor 和 buildType 的代码目录,里面代码是不同的实现;一个 productFlavor 和一个 buildType 对应一个文件夹
    xxx module
    –src
    – main 主代码存放
    – debug buildType 为 debug
    – release buildType 为 release
    – huawei productFlavor 为 huawei
    – google productFlavor 为 google

main 是主代码仓库,google 和 huawei 是 flavor,可引用 main 中的代码;debug 和 release 是 buildType,可引用 main 代码;具体目录和代码见https://github.com/hacket/TestMultiBuildTypeAndFlavor

  1. app 和 lib 尽量配置相同的 productFlavor 和 buildType,否则编译可能会出错,可以用 matchingFallbacks 来降级
1
2
3
4
5
6
7
8
9
10
11
12
13
// app build.gradle
// 与lib的依赖匹配问题
android {
    buildType {
        debug { }
        beta { }
        release { }
    }
    buildTypes.all { type ->
        // lib module中没有的buildType都降级为debug
        type.matchingFallbacks = ['debug']
    }
}
  1. 如果不同的 flavor 依赖的库不一样,也可以配置;buildType 也可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 通过云信来集成小米等厂商推送需要
huaweiImplementation rootProject.ext.dependencies.NimNetPushSdk
//Firebase
huaweiImplementation platform( rootProject.ext.dependencies.FirebaseCore )
huaweiImplementation rootProject.ext.dependencies.FirebaseMessaging

// 华为
huaweiImplementation ('com.huawei.hms:hwid:6.1.0.302'){
    exclude group:'com.huawei.hms', module:'base'
}
huaweiImplementation ('com.huawei.hms:iap:6.2.0.300'){
    exclude group:'com.huawei.hms', module:'base'
}
huaweiImplementation ('com.huawei.hms:game:6.1.0.301'){
    exclude group:'com.huawei.hms', module:'base'
}
huaweiImplementation 'com.huawei.hms:hianalytics:6.3.0.303'
huaweiImplementation 'com.huawei.hms:push:6.5.0.300'

//====================================================
//Google内购
googleApi "com.android.billingclient:billing:4.0.0"
//Play Core库(内部更新)
googleApi 'com.google.android.play:core:1.10.0'

注意:productFlavor 名称只能用小写的,大写的会编译不过

遇到的问题

lib 和 app 中的 productFlavor 和 buildType 不匹配

  1. lib 和 app productFlavor 不匹配
  2. lib 和 app buildType 不匹配
1
2
3
Required by:
 project :modules:common
> The consumer was configured to find an API of a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'beta', attribute 'app' with value 'huawei', attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'. However we cannot choose between the following variants of project :baseui:
  • 解决 1:app 和 lib 都配置一样的 buildType 和 flavor
  • 解决 2:利用 matchingFallbacks 进行 buildType 的降级;lib 中没有对应的 flavor 时,好像不需要配置对应的 flavor

productFlavor 配置成大写, xxxImplementation 编译不过

解决:productFlavor 的名称改成小写

AGP3.0 设置 apk aar 输出路径和文件名

设置 apk 输出路径和文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
android {
    // ...
    android.applicationVariants.all { variant ->
        if (variant.buildType.name == 'debug') { // debug包
            // 改apk路径
            variant.packageApplication.outputDirectory = new File(rootDir.getAbsolutePath() + "/build/apk/${variant.buildType.name}")
            variant.outputs.all { output ->
                def fileName = "${config['appName']}_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${buildTime()}_${buildGitRevision()}_${variant.buildType.name}.apk"
                outputFileName = fileName
            }
            // qiubai-voicechat_v1.0.0_1_20190509190553_3cfd465_debug.apk
        } else { // release或者releaseLog包
            variant.packageApplication.outputDirectory = new File(rootDir.getAbsolutePath() + "/build/apk/${variant.buildType.name}")
            variant.outputs.all { output ->
                def fileName = "${config['appName']}_v${defaultConfig.versionName}_${defaultConfig.versionCode}_${variant.buildType.name}.apk"
                outputFileName = fileName
            }
            // qiubai-voicechat_v1.0.0_1_release.apk
        }
    }

}

agp3.0+ 设置 aar 路径和 aar 名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
libraryVariants.all { variant ->
  if (variant.buildType.name == "debug") {
      variant.getPackageLibrary().destinationDir = new File(project.rootDir.absolutePath + "/xxx")
  }

  if (variant.buildType.name == 'debug') {
      variant.outputs.all { output ->
          def outputFile = output.outputFile
          if (outputFile != null && outputFile.name.endsWith('debug.aar')) {
             outputFileName = "${project.name}-${variant.flavorName}-${variant.buildType.name}.aar"
          }
      }
  }
}

上面这个只能改 aar 路径,改不了 aar 名称。如果需要改路径和名称,需要先改名称,再改路径

1
2
3
4
5
6
7
8
9
10
11
12
13
libraryVariants.all { variant ->
  if (variant.buildType.name == 'debug') {
      variant.outputs.all { output ->
          def outputFile = output.outputFile
          if (outputFile != null && outputFile.name.endsWith('debug.aar')) {
             outputFileName = "${project.name}-${variant.flavorName}-${variant.buildType.name}.aar"
          }
      }
  }
  if (variant.buildType.name == "debug") {
      variant.getPackageLibrary().destinationDir = new File(project.rootDir.absolutePath + "/xxx")
  }
}

从 strings.xml 中读取 app_name 改名到 apk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private def getAppName() {
    try {
//        def s1 = System.currentTimeMillis()
//        def stringsXml = android.sourceSets.main.res.sourceFiles.find { it.name.equals 'strings.xml' } // 平均10ms左右
        def stringsXml = "${rootDir}/Remix/res/values/strings.xml" // 平均2ms左右
        def parser = new XmlParser()

        parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false)
//        parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        def root = parser.parse(new File(stringsXml.toString()))
        def iterator = root.iterator()
        while (iterator.hasNext()) {
            def it = iterator.next()
            if (it.@name == "app_name") {
//                println("----------- 获取到appName=${it.text()} cost:${System.currentTimeMillis() - s1}ms")
                return it.text()
            }
        }
    } catch (Exception e) {
        e.printStackTrace()
    }
    return "Remix"
}

应用

1
2
3
4
5
6
7
8
9
10
android {
    applicationVariants.all {
        variant ->
            variant.outputs.all {
                def appName = getAppName()
                println '--------------'+appName + '-' + defaultConfig.versionName + '-' + defaultConfig.versionCode + '-' + variant.buildType.name + '.apk'
                outputFileName = appName + '-' + defaultConfig.versionName + '-' + defaultConfig.versionCode + '-' + variant.buildType.name + '.apk'
            }
    }
}

Ref

Android 资源目录分模块

使用 gradle 管理的安卓程序,默认没有分模块,所有的资源文件等都集中在 res 中,包括图片、图标、布局文件以及其他资源文件等等

  1. 添加目录
    u6uq8
  2. 添加 sourceSets 这一项,修改资源目录即可
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
26
27
28
29
30
android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.example.qq12cvhj.chowhound"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        main {
            res.srcDirs =
                [
                    'src/main/res/Layouts/cookPage',
                    'src/main/res/Layouts/friendsPage',
                    'src/main/res/Layouts/mePage',
                    'src/main/res/Layouts/homePage',
                    'src/main/res/Layouts',
                    'src/main/res'
                ]
        }
    }
}

Gradle 命令行参数 -P

  • -P 定义参数
  • 判断是否有该属性 hasProperty
  • 获取该参数,直接使用
1
./gradlew clean assembledebug -PabiType=64

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private def getCustomAbiFilters() {
    def temp = ["armeabi-v7a", "arm64-v8a"] as String[]
    if (!hasProperty('abiType')) {
        println("---->>>> has not Property abiType")
        return temp
    }
    if (abiType == "32") {
        temp = ["armeabi-v7a"] as String[]
    } else if (abiType == "64") {
        temp = ["arm64-v8a"] as String[]
    } else if (abiType == "all") {
        temp = ["armeabi-v7a", "arm64-v8a"] as String[]
    }
    println("---->>>> abiType=$abiType, temp=$temp")
    return temp
}

Android Gradle Plugin 分析

APK 打包流程

  • 简洁版

xkqay

  • 详细版
    w72q1

AGP 常见 Task

compileDebugAidl

将.aidl 文件通过 aidl 工具转换成编译器能够处理的 Java 接口文件

相关代码:AidlCompile.java → AidlProcessor.java → call()

checkDebugManifest

检查 AndroidManifest.xml 文件是否存在

相关代码:CheckManifest.java

compileDebugRenderscript

处理 Renderscript 文件 (.rs)

相关代码:RenderscriptCompile.java

generateDebugBuildConfig

生成 BuildConfig.java 文件

相关代码:GenerateBuildConfig.java

mainApkListPersistenceDebug

持久化 APK 数据到 apk-list.gson 中

相关代码:MainApkListPersistence.kt

generateDebugResValues

遍历 res 下的 values 目录下 xml 文件,生成 resValues 文件 generated.xml

相关代码:GenerateResValues.java → generate() → ResValueGenerator.java

mergeDebugResources

使用 AAPT2 合并资源文件

相关代码:MergeResources.doFullTaskAction() → ResourceMerger.mergeData() → MergedResourceWriter.end() → mResourceCompiler.submitCompile() → AaptV2CommandBuilder.makeCompileCommand()

1
2
3
4
5
6
7
8
9
10
11
public abstract class MergeResources extends ResourceAwareTask {
    // 实现了isIncremental()方法,返回true,说明支持增量编译,跟下全量编译方法 doFullTaskAction()
    @Override
    protected boolean getIncremental() {
        return true;
    }
    @Override
    protected void doFullTaskAction() throws IOException, JAXBException {
        
    }
}

将图片转为 webp 格式的插件一般在此 Task 前处理

createDebugCompatibleScreenManifests

manifest 文件中生成 compatible-screens,用于屏幕适配

相关代码:CompatibleScreensManifest.kt

processDebugManifest

合并 AndroidManifest.xml 文件

相关代码:ProcessApplicationManifest.java、ProcessLibraryManifest.java

processDebugResources

调用 aapt2 link 打包资源并生成 R.java 文件

相关代码:TaskManager.java → createProcessResTask()

compileDebugKotlin

编译 Kotlin 文件为字节码

prepareLintJar

拷贝 lint jar 包到指定位置

相关代码:PrepareLintJar.java

javaPreCompileDebug

生成 annotationProcessors.json 文件

相关代码:JavaPreCompileTask.java

compileDebugJavaWithJavac

编译 java 文件

相关代码:AndroidJavaCompile.java

compileDebugNdk

编译 NDK

相关代码:NdkCompile.java

mergeDebugShaders

合并 Renderscript 文件 (.rs)

相关代码:MergeSourceSetFolders.java

compileDebugShaders

编译 Renderscript 文件 (.rs)

相关代码:ShaderCompile.java

mergeDebugAssets

合并 assets 文件

相关代码:MergeSourceSetFolders.java

validateSigningDebug

验证签名

检查当前 Variant 的签名配置中是否存在密钥库文件,如果当前密钥库默认为 debug keystore,那密钥库不存在也会进行相应的创建。

相关代码:ValidateSigningTask.kt

signingConfigWriterDebug

编写 SigningConfig 信息

相关代码:SigningConfigWriterTask.kt

checkDebugDuplicateClasses

检查重复 class

检查项目外部依赖是否不包含重复类,打包成 dex 的时候再检测报错不怎么友好,所以引入了这个 Task 用于快速失败。

相关代码:CheckDuplicateClassesTask.kt

transformClassesWithDexBuilderForDebug

将 class 打包成 dex

相关代码:DexArchiveBuilderTransform.java

transformDexArchiveWithExternalLibsDexMergerForDebug

打包第三方库的 dex

相关代码:ExternalLibsMergerTransform.kt

transformDexArchiveWithDexMergerForDebug

打包最终的 dex

相关代码:DexMergerTransform.transform() → mergeDex()

mergeDebugJniLibFolders

合并 jni lib 文件

相关代码:MergeSourceSetFolders.java

transformNativeLibsWithMergeJniLibsForDebug

合并 jnilibs

相关代码:MergeJavaResourcesTransform.java

transformNativeLibsWithStripDebugSymbolForDebug

去掉 native lib 里的 debug 符号

相关代码:StripDebugSymbolTransform.java

processDebugJavaRes

处理 java res

相关代码:MergeJavaResourcesTransform.java

transformResourcesWithMergeJavaResForDebug

合并 java res
相关代码:MergeJavaResourcesTransform.java

packageDebug

打包 APK
相关代码:PackageApplication.java → PackageAndroidArtifact.doTask()

extractProguardFiles

生成混淆文件
相关代码:ExtractProguardFiles.java

锚点 Task → 空 Task

  • preBuild
本文由作者按照 CC BY 4.0 进行授权