构建配置 defaultConfig signingConfigs buildTypes productFlavors dependencies

项目结构:
795730-20180721235347460-145586553.png

project 目录的 build.gradle 文件

// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config/config_dependencies.gradle" //目的是将所依赖的系统版本、第三方库版本等抽到专门的文件中进行统一的管理
apply from: "config/config_android.gradle" //后面引用的 gradle 配置中的属性会覆盖掉前面引用的配置中的属性

buildscript {

    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.3'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
25
25
 
1
// Top-level build file where you can add configuration options common to all sub-projects/modules.
2
apply from: "config/config_dependencies.gradle" //目的是将所依赖的系统版本、第三方库版本等抽到专门的文件中进行统一的管理
3
apply from: "config/config_android.gradle" //后面引用的 gradle 配置中的属性会覆盖掉前面引用的配置中的属性
4
 
          
5
buildscript {
6
 
          
7
    repositories {
8
        google()
9
        jcenter()
10
    }
11
    dependencies {
12
        classpath 'com.android.tools.build:gradle:3.1.3'
13
    }
14
}
15
 
          
16
allprojects {
17
    repositories {
18
        google()
19
        jcenter()
20
    }
21
}
22
 
          
23
task clean(type: Delete) {
24
    delete rootProject.buildDir
25
}
自定义的部分只有最上面的那两行,其他都是默认的

自定义的两个 .gradle 文件

作用:对所依赖的系统版本、第三方库版本等进行统一的管理。
这两个文件我都放到了根目录下的新建的 config 目录中。
config_android.gradle
//作用:对所依赖的系统版本、第三方库版本等进行统一的管理。
// 注意:每一个此类型的文件必须包含一个根节点ext,且ext下必须包含一个属性android,除此之外可以随意定义其他属性或方法
ext {
    android = [compileSdkVersion: 26,
               buildToolsVersion: "27.0.3",
               minSdkVersion    : 14,
               targetSdkVersion : 22,
               versionName      : getGitBranch() + "_" + getGitTag() + "_" + getGitSHA(), //根据git信息生成版本名
               versionCode      : getGitCommitCount(),  //根据git提交次数生成版本号
    ]
}

//获取Git 分支名,参考 https://blog.csdn.net/ouyang_peng/article/details/77802596
static def getGitBranch() {
    return 'git symbolic-ref --short -q HEAD'.execute().text.trim() //例如 master
}

//获取Git Tag
def getGitTag() {
    return 'git describe --tags'.execute([], project.rootDir).text.trim() //例如 bqt20094
    //注意,如果没有设置 tag 的话结果为:fatal: No names found, cannot describe anything.
}

//获取Git 版本号
static def getGitSHA() {
    return 'git rev-parse --short HEAD'.execute().text.trim()  //例如 3d5851e
}

//获取Git 提交次数
static def getGitCommitCount() {
    return 'git rev-list --count HEAD'.execute().text.trim().toInteger() //例如 8
}
32
32
 
1
//作用:对所依赖的系统版本、第三方库版本等进行统一的管理。
2
// 注意:每一个此类型的文件必须包含一个根节点ext,且ext下必须包含一个属性android,除此之外可以随意定义其他属性或方法
3
ext {
4
    android = [compileSdkVersion: 26,
5
               buildToolsVersion: "27.0.3",
6
               minSdkVersion    : 14,
7
               targetSdkVersion : 22,
8
               versionName      : getGitBranch() + "_" + getGitTag() + "_" + getGitSHA(), //根据git信息生成版本名
9
               versionCode      : getGitCommitCount(),  //根据git提交次数生成版本号
10
    ]
11
}
12
 
          
13
//获取Git 分支名,参考 https://blog.csdn.net/ouyang_peng/article/details/77802596
14
static def getGitBranch() {
15
    return 'git symbolic-ref --short -q HEAD'.execute().text.trim() //例如 master
16
}
17
 
          
18
//获取Git Tag
19
def getGitTag() {
20
    return 'git describe --tags'.execute([], project.rootDir).text.trim() //例如 bqt20094
21
    //注意,如果没有设置 tag 的话结果为:fatal: No names found, cannot describe anything.
22
}
23
 
          
24
//获取Git 版本号
25
static def getGitSHA() {
26
    return 'git rev-parse --short HEAD'.execute().text.trim()  //例如 3d5851e
27
}
28
 
          
29
//获取Git 提交次数
30
static def getGitCommitCount() {
31
    return 'git rev-list --count HEAD'.execute().text.trim().toInteger() //例如 8
32
}
config_dependencies.gradle
//作用:对所依赖的系统版本、第三方库版本等进行统一的管理。
// 注意:每一个此类型的文件必须包含一个根节点ext,且ext下必须包含一个属性android,除此之外可以随意定义其他属性或方法
ext {
    android = [] //因为后面引用的 gradle 配置中的属性会覆盖掉前面引用的配置中的属性,所以不应该在这里操作任何 android 属性的成员


    version = [
            support_v7: "27.1.1",
            gson      : "2.6.2", // gson
    ]

    dependencies = [
            "support_v7": "com.android.support:appcompat-v7:${version["support_v7"]}", // android  support v7 library
            "gson"      : "com.google.code.gson:gson:${version["gson"]}", // gson
    ]
}
16
16
 
1
//作用:对所依赖的系统版本、第三方库版本等进行统一的管理。
2
// 注意:每一个此类型的文件必须包含一个根节点ext,且ext下必须包含一个属性android,除此之外可以随意定义其他属性或方法
3
ext {
4
    android = [] //因为后面引用的 gradle 配置中的属性会覆盖掉前面引用的配置中的属性,所以不应该在这里操作任何 android 属性的成员
5
 
          
6
 
          
7
    version = [
8
            support_v7: "27.1.1",
9
            gson      : "2.6.2", // gson
10
    ]
11
 
          
12
    dependencies = [
13
            "support_v7": "com.android.support:appcompat-v7:${version["support_v7"]}", // android  support v7 library
14
            "gson"      : "com.google.code.gson:gson:${version["gson"]}", // gson
15
    ]
16
}

app 目录的 build.gradle 文件

这个是最核心、最关键、最复杂的一个配置文件。
apply plugin: 'com.android.application'

android {
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    buildToolsVersion rootProject.ext.android["buildToolsVersion"]

    //*******************************************************************************************************************************
    //                                                                                         【defaultConfig】
    //*******************************************************************************************************************************
    defaultConfig {
        applicationId "com.bqt.test"
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionName rootProject.ext.android["versionName"]
        versionCode rootProject.ext.android["versionCode"]

        flavorDimensions "bqt" //必须带上一个 flavorDimensions[维度、尺寸],值可以随意
        testInstrumentationRunner "android.support.com.bqt.test.runner.AndroidJUnitRunner"
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }

        // 放在这里可以在任何地方获取到。key可以不必用引号括起来,但是字符串类型的值必须使用【'"】和【"'】括起来才可以
        // 否则,值最匹配什么类型就是什么类型。比如【"1"】和【"@drawable/icon"】是int类型,而【'"1"'】才代表字符串类型
        manifestPlaceholders = [release_time_value: releaseTime()]
    }

    //*******************************************************************************************************************************
    //                                                                                          【signingConfigs】
    //*******************************************************************************************************************************
    signingConfigs { //配置签名信息,例如各个不同产品的签名文件位置、密码、昵称、昵称密码等,要放在 buildTypes 上面配置
        beakeystore { //除了默认的 debug 签名外,signingConfigs 中的其他签名必须完整的配置签名相关的信息
            storeFile file('../config/bea.keystore')
            storePassword 'beachinambk'
            keyAlias 'bea.keystore'
            keyPassword 'beachinambk'

            v2SigningEnabled true
        }

        debug { //默认的 debug 签名,不需要指定签名信息,当然你也可以专门生成并配置一个 debug 签名
            v2SigningEnabled true
        }

        beakeystore2.initWith(beakeystore)  //initWith的作用是:拷贝指定签名 beakeystore 中的所有配置到新的签名中
        beakeystore2 {
            storeFile file('../config/bea2.keystore') //除了签名文件不一样外,密码、昵称、昵称密码等和 beakeystore 完全一样
        }
    }

    //*******************************************************************************************************************************
    //                                                                                          【buildTypes】
    //*******************************************************************************************************************************
    buildTypes { // 用于区分release包和debug等不同构建类型的包。默认正式签名时会走release脚本,调试签名时会走debug脚本
        release {
            debuggable false //是否可调试。debug模式默认为true,release模式默认是false
            zipAlignEnabled true// 是否启用Zipalign优化,对齐app所有资源。因为对齐处理发生在签名之后,所以启用时必须指定了签名信息
            shrinkResources true// 在构建时是否自动移除无用的资源以减小apk包的大小(包括图片,布局,菜单等,但不包括value资源文件)

            minifyEnabled true //是否启用混淆。debug与release的默认值都为false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //配置混淆文件所在路径
            //前一部分代表系统默认的android程序的混淆文件,该文件已经包含了基本的混淆声明,后一个文件是自己的定义混淆文件
            consumerProguardFiles 'consumer-proguard-rules.pro'

            signingConfig signingConfigs.beakeystore //使用自己配置的签名信息

            manifestPlaceholders = [baidu_map_key_value: "20094"]  //注意,这里的值用双引号括起来并不代表其就是String类型
            buildConfigField "String", "BASE_URL", '"http://110.com/"' //自定义属性,可以在 BuildConfig 中获取设置的值
        }

        debug {
            signingConfig signingConfigs.debug  //使用默认的 debug 签名,即使不配置,也是使用的此默认签名

            manifestPlaceholders = [baidu_map_key_value: "10086"]  //注意,这里的值用双引号括起来并不代表其就是String类型
            buildConfigField "String", "BASE_URL", '"http://120.com/"' //自定义属性,可以在 BuildConfig 中获取设置的值
        }

        pre.initWith(release) //initWith的作用是:拷贝指定构建类型 release 中的所有变量到 pre,然后gradle就会自动生成新的task
        pre { //预发布版本,该版本除了下面指定的配置与release版本不同外,其他都与release相同
            debuggable true
        }
    }

    //*******************************************************************************************************************************
    //                                                                                          【productFlavors】
    //*******************************************************************************************************************************
    productFlavors { //用于为不同的产品分配专有属性;构建基于同一份代码的不同Android项目有差异的部分;多渠道打包
        productA {  //新建产品productA,在defaultConfig的基础上做修改
            applicationId "com.bqt.test.productA"  // defaultConfig 中可以设置的属性在这里基本也都可以设置

            resValue "string", "app_icon_name", "产品A" //替换掉指定的资源文件,比如应用名
            resValue "color", "color_footer", "#ffff0000" //替换掉的资源可以是没有定义过的

            buildConfigField "boolean", "isHongkongUser", "true"//自定义属性,可以在 BuildConfig 中获取设置的值
            buildConfigField "int", "countryCode", "20094"//自定义属性,可以在 BuildConfig 中获取设置的值

            manifestPlaceholders = [app_logo         : "@drawable/icon",//用于替换清单文件中的标签,比如应用logo,多渠道信息
                                    app_channel_value: "小米应用市场",
            ]
        }

        productB {  //新建产品productB
            applicationIdSuffix ".productB" //在defaultConfig中默认applicationId的基础上在后面追加一段
            versionNameSuffix "_productB" //在defaultConfig中默认versionName的基础上在后面追加一段

            resValue "string", "app_icon_name", "产品B" //替换掉指定的资源文件,比如应用名
            resValue "color", "color_footer", "#ff0000ff" //替换掉的资源可以是没有定义过的

            buildConfigField "boolean", "isHongkongUser", "false"//自定义属性,可以在 BuildConfig 中获取设置的值
            buildConfigField "int", "countryCode", "20095"//自定义属性,可以在 BuildConfig 中获取设置的值

            manifestPlaceholders = [app_logo         : "@drawable/icon2",//用于替换清单文件中的标签,比如应用logo,多渠道信息
                                    app_channel_value: "应用宝",
            ]
        }

        // 批量处理所有的 productFlavors,常用于多渠道打包时的统一配置渠道名等信息
        productFlavors.all {
            //注意,这里配置后会覆盖掉上面每个Flavor已配置的 manifestPlaceholders ,所以设置时要保证上面配置的所有标签都要有才行
            //flavor -> flavor.manifestPlaceholders = [app_logo: "@drawable/icon2", app_channel_value: name + " 首发"]
        }

        //自定义所有输出的APK包的位置与名称,多渠道打包时也非常有用,如果不设置,每个Flavor构建的APK都会
        android.applicationVariants.all { variant ->
            def sep = "_"  //分隔符
            def time = releaseTime()  //打包时间
            def buildType = variant.buildType.name  //构建类型,比如 release 或 debug
            def versionCode = variant.versionCode  //版本号
            variant.outputs.all { output ->
                variant.productFlavors.each { flavor ->
                    def applicationId = flavor.applicationId == null ? defaultConfig.applicationId : flavor.applicationId // flavor没配置时为空
                    def flavorName = flavor.name //应用名
                    def channelName = flavor.manifestPlaceholders["app_channel_value"] //渠道名

                    if (buildType == "release") { //变更输出路径。变更后会导致通过Run方式构建的Apk无法自动安装
                        def apkPath = project.rootDir.absolutePath + "/apks/${applicationId}/" //可以通过占位符【${}】或【+】来拼接字符串
                        variant.getPackageApplication().outputDirectory = new File(apkPath)
                    }
                    def apkName = flavorName + sep + buildType + sep + channelName + sep + "v" + versionCode + sep + time + ".apk"
                    println "【文件名】" + apkName //例如【productB_release_应用宝_v16_20180719_21.18.15_星期四.apk】
                    output.outputFileName = apkName  //重新对apk命名
                    //注意,Gradle4.0以下版本对apk重命名的方式为:variant.outputFile = new File(variant.outputFile.parent, apkName)
                }
            }
        }
    }
}

//***********************************************************************************************************************************
//                                                                                          【dependencies】
//***********************************************************************************************************************************
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation rootProject.ext.dependencies["support_v7"] // android support library
    implementation rootProject.ext.dependencies["gson"] // gson
}

static def releaseTime() {
    return new Date().format("yyyyMMdd_HH.mm.ss_E", TimeZone.getDefault()) // 或 TimeZone.getTimeZone("GMT+08:00")
}
161
161
 
1
apply plugin: 'com.android.application'
2
 
          
3
android {
4
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
5
    buildToolsVersion rootProject.ext.android["buildToolsVersion"]
6
 
          
7
    //*******************************************************************************************************************************
8
    //                                                                                         【defaultConfig】
9
    //*******************************************************************************************************************************
10
    defaultConfig {
11
        applicationId "com.bqt.test"
12
        minSdkVersion rootProject.ext.android["minSdkVersion"]
13
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
14
        versionName rootProject.ext.android["versionName"]
15
        versionCode rootProject.ext.android["versionCode"]
16
 
          
17
        flavorDimensions "bqt" //必须带上一个 flavorDimensions[维度、尺寸],值可以随意
18
        testInstrumentationRunner "android.support.com.bqt.test.runner.AndroidJUnitRunner"
19
        compileOptions {
20
            sourceCompatibility JavaVersion.VERSION_1_8
21
            targetCompatibility JavaVersion.VERSION_1_8
22
        }
23
 
          
24
        // 放在这里可以在任何地方获取到。key可以不必用引号括起来,但是字符串类型的值必须使用【'"】和【"'】括起来才可以
25
        // 否则,值最匹配什么类型就是什么类型。比如【"1"】和【"@drawable/icon"】是int类型,而【'"1"'】才代表字符串类型
26
        manifestPlaceholders = [release_time_value: releaseTime()]
27
    }
28
 
          
29
    //*******************************************************************************************************************************
30
    //                                                                                          【signingConfigs】
31
    //*******************************************************************************************************************************
32
    signingConfigs { //配置签名信息,例如各个不同产品的签名文件位置、密码、昵称、昵称密码等,要放在 buildTypes 上面配置
33
        beakeystore { //除了默认的 debug 签名外,signingConfigs 中的其他签名必须完整的配置签名相关的信息
34
            storeFile file('../config/bea.keystore')
35
            storePassword 'beachinambk'
36
            keyAlias 'bea.keystore'
37
            keyPassword 'beachinambk'
38
 
          
39
            v2SigningEnabled true
40
        }
41
 
          
42
        debug { //默认的 debug 签名,不需要指定签名信息,当然你也可以专门生成并配置一个 debug 签名
43
            v2SigningEnabled true
44
        }
45
 
          
46
        beakeystore2.initWith(beakeystore)  //initWith的作用是:拷贝指定签名 beakeystore 中的所有配置到新的签名中
47
        beakeystore2 {
48
            storeFile file('../config/bea2.keystore') //除了签名文件不一样外,密码、昵称、昵称密码等和 beakeystore 完全一样
49
        }
50
    }
51
 
          
52
    //*******************************************************************************************************************************
53
    //                                                                                          【buildTypes】
54
    //*******************************************************************************************************************************
55
    buildTypes { // 用于区分release包和debug等不同构建类型的包。默认正式签名时会走release脚本,调试签名时会走debug脚本
56
        release {
57
            debuggable false //是否可调试。debug模式默认为true,release模式默认是false
58
            zipAlignEnabled true// 是否启用Zipalign优化,对齐app所有资源。因为对齐处理发生在签名之后,所以启用时必须指定了签名信息
59
            shrinkResources true// 在构建时是否自动移除无用的资源以减小apk包的大小(包括图片,布局,菜单等,但不包括value资源文件)
60
 
          
61
            minifyEnabled true //是否启用混淆。debug与release的默认值都为false
62
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //配置混淆文件所在路径
63
            //前一部分代表系统默认的android程序的混淆文件,该文件已经包含了基本的混淆声明,后一个文件是自己的定义混淆文件
64
            consumerProguardFiles 'consumer-proguard-rules.pro'
65
 
          
66
            signingConfig signingConfigs.beakeystore //使用自己配置的签名信息
67
 
          
68
            manifestPlaceholders = [baidu_map_key_value: "20094"]  //注意,这里的值用双引号括起来并不代表其就是String类型
69
            buildConfigField "String", "BASE_URL", '"http://110.com/"' //自定义属性,可以在 BuildConfig 中获取设置的值
70
        }
71
 
          
72
        debug {
73
            signingConfig signingConfigs.debug  //使用默认的 debug 签名,即使不配置,也是使用的此默认签名
74
 
          
75
            manifestPlaceholders = [baidu_map_key_value: "10086"]  //注意,这里的值用双引号括起来并不代表其就是String类型
76
            buildConfigField "String", "BASE_URL", '"http://120.com/"' //自定义属性,可以在 BuildConfig 中获取设置的值
77
        }
78
 
          
79
        pre.initWith(release) //initWith的作用是:拷贝指定构建类型 release 中的所有变量到 pre,然后gradle就会自动生成新的task
80
        pre { //预发布版本,该版本除了下面指定的配置与release版本不同外,其他都与release相同
81
            debuggable true
82
        }
83
    }
84
 
          
85
    //*******************************************************************************************************************************
86
    //                                                                                          【productFlavors】
87
    //*******************************************************************************************************************************
88
    productFlavors { //用于为不同的产品分配专有属性;构建基于同一份代码的不同Android项目有差异的部分;多渠道打包
89
        productA {  //新建产品productA,在defaultConfig的基础上做修改
90
            applicationId "com.bqt.test.productA"  // defaultConfig 中可以设置的属性在这里基本也都可以设置
91
 
          
92
            resValue "string", "app_icon_name", "产品A" //替换掉指定的资源文件,比如应用名
93
            resValue "color", "color_footer", "#ffff0000" //替换掉的资源可以是没有定义过的
94
 
          
95
            buildConfigField "boolean", "isHongkongUser", "true"//自定义属性,可以在 BuildConfig 中获取设置的值
96
            buildConfigField "int", "countryCode", "20094"//自定义属性,可以在 BuildConfig 中获取设置的值
97
 
          
98
            manifestPlaceholders = [app_logo         : "@drawable/icon",//用于替换清单文件中的标签,比如应用logo,多渠道信息
99
                                    app_channel_value: "小米应用市场",
100
            ]
101
        }
102
 
          
103
        productB {  //新建产品productB
104
            applicationIdSuffix ".productB" //在defaultConfig中默认applicationId的基础上在后面追加一段
105
            versionNameSuffix "_productB" //在defaultConfig中默认versionName的基础上在后面追加一段
106
 
          
107
            resValue "string", "app_icon_name", "产品B" //替换掉指定的资源文件,比如应用名
108
            resValue "color", "color_footer", "#ff0000ff" //替换掉的资源可以是没有定义过的
109
 
          
110
            buildConfigField "boolean", "isHongkongUser", "false"//自定义属性,可以在 BuildConfig 中获取设置的值
111
            buildConfigField "int", "countryCode", "20095"//自定义属性,可以在 BuildConfig 中获取设置的值
112
 
          
113
            manifestPlaceholders = [app_logo         : "@drawable/icon2",//用于替换清单文件中的标签,比如应用logo,多渠道信息
114
                                    app_channel_value: "应用宝",
115
            ]
116
        }
117
 
          
118
        // 批量处理所有的 productFlavors,常用于多渠道打包时的统一配置渠道名等信息
119
        productFlavors.all {
120
            //注意,这里配置后会覆盖掉上面每个Flavor已配置的 manifestPlaceholders ,所以设置时要保证上面配置的所有标签都要有才行
121
            //flavor -> flavor.manifestPlaceholders = [app_logo: "@drawable/icon2", app_channel_value: name + " 首发"]
122
        }
123
 
          
124
        //自定义所有输出的APK包的位置与名称,多渠道打包时也非常有用,如果不设置,每个Flavor构建的APK都会
125
        android.applicationVariants.all { variant ->
126
            def sep = "_"  //分隔符
127
            def time = releaseTime()  //打包时间
128
            def buildType = variant.buildType.name  //构建类型,比如 release 或 debug
129
            def versionCode = variant.versionCode  //版本号
130
            variant.outputs.all { output ->
131
                variant.productFlavors.each { flavor ->
132
                    def applicationId = flavor.applicationId == null ? defaultConfig.applicationId : flavor.applicationId // flavor没配置时为空
133
                    def flavorName = flavor.name //应用名
134
                    def channelName = flavor.manifestPlaceholders["app_channel_value"] //渠道名
135
 
          
136
                    if (buildType == "release") { //变更输出路径。变更后会导致通过Run方式构建的Apk无法自动安装
137
                        def apkPath = project.rootDir.absolutePath + "/apks/${applicationId}/" //可以通过占位符【${}】或【+】来拼接字符串
138
                        variant.getPackageApplication().outputDirectory = new File(apkPath)
139
                    }
140
                    def apkName = flavorName + sep + buildType + sep + channelName + sep + "v" + versionCode + sep + time + ".apk"
141
                    println "【文件名】" + apkName //例如【productB_release_应用宝_v16_20180719_21.18.15_星期四.apk】
142
                    output.outputFileName = apkName  //重新对apk命名
143
                    //注意,Gradle4.0以下版本对apk重命名的方式为:variant.outputFile = new File(variant.outputFile.parent, apkName)
144
                }
145
            }
146
        }
147
    }
148
}
149
 
          
150
//***********************************************************************************************************************************
151
//                                                                                          【dependencies】
152
//***********************************************************************************************************************************
153
dependencies {
154
    implementation fileTree(dir: 'libs', include: ['*.jar'])
155
    implementation rootProject.ext.dependencies["support_v7"] // android support library
156
    implementation rootProject.ext.dependencies["gson"] // gson
157
}
158
 
          
159
static def releaseTime() {
160
    return new Date().format("yyyyMMdd_HH.mm.ss_E", TimeZone.getDefault()) // 或 TimeZone.getTimeZone("GMT+08:00")
161
}

设置完之后,点击下面 Build Variants 栏可以切换默认点击 Run 时运行的任务:
795730-20180721235350861-1075918481.png
我们在上面的 productFlavors 中设置了两个产品,在 buildTypes 中设置了三个构建类型,所以我们可选的打包任务就有 2*3 = 6 个。

Generate Signed APK 时也有  2*3 = 6 个选择
795730-20180721235351426-1227315978.png    795730-20180721235352014-310983334.png

可以对不同的产品引入不同的包,例如:
productACompile "com....gson..."   //产品A需要引入gson
productBCompile "com....glide..."  //产品B需要引入glide
2
2
 
1
productACompile "com....gson..."   //产品A需要引入gson
2
productBCompile "com....glide..."  //产品B需要引入glide

可以对不同的产品引入不同的包,例如:
sourceSets {
	main {
		aidl.srcDirs = ['src/main/aidl']
		jni.srcDirs 'src/main/jni'
	}
	productA {
		java.srcDirs = ['src/productA/java']
	}
	productB {
		java.srcDirs = ['src/productB/java']
	}
}
12
12
 
1
sourceSets {
2
    main {
3
        aidl.srcDirs = ['src/main/aidl']
4
        jni.srcDirs 'src/main/jni'
5
    }
6
    productA {
7
        java.srcDirs = ['src/productA/java']
8
    }
9
    productB {
10
        java.srcDirs = ['src/productB/java']
11
    }
12
}

实际 build 时的配置信息

注意,下面的文件不是哪里生成的,而是我自己提取出来的。
{
    "debuggable": "false",
    "embedMicroApp": "true",
    "jniDebuggable": "false",
    "mBuildConfigFields": {
        "BASE_URL": "http://120.com/",
        "countryCode": 20094,
        "isHongkongUser": "com.android.builder.internal.ClassFieldImpl@f39f32b9"
    },
    "mConsumerProguardFiles": [
        "你指定的自动寻找的第三方库里的混淆文件"
    ],
    "mManifestPlaceholders": {
        "baidu_map_key_value": "20094"
    },
    "mProguardFiles": [
        "C:/Android/_coder/_workspace_as/FragmentTest/build/intermediates/proguard-files/proguard-android.txt-3.1.3",
        "C:/Android/_coder/_workspace_as/FragmentTest/app/proguard-rules.pro"
    ],
    "mResValues": {
        "app_icon_name": "com.android.builder.internal.ClassFieldImpl@efa088c5",
        "color_footer":"com.android.builder.internal.ClassFieldImpl@391e62f"
    },
    "minifyEnabled": "true",
    "name": "release",
    "pseudoLocalesEnabled": "false",
    "renderscriptDebuggable": "false",
    "renderscriptOptimLevel": "3",
    "signingConfig": {
        "keyAlias": "bea.keystore",
        "keyPassword": "beachinambk",
        "name": "beakeystore",
        "storeFile": "C:/Android/_coder/_workspace_as/FragmentTest/config/bea.keystore",
        "storePassword": "beachinambk",
        "storeType": "C:/Android/_coder/_workspace_as/FragmentTest/config/bea.keystore",
        "v1SigningEnabled": "true",
        "v2SigningEnabled": "true"
    },
    "testCoverageEnabled": "false",
    "zipAlignEnabled": "true"
}
41
41
 
1
{
2
    "debuggable": "false",
3
    "embedMicroApp": "true",
4
    "jniDebuggable": "false",
5
    "mBuildConfigFields": {
6
        "BASE_URL": "http://120.com/",
7
        "countryCode": 20094,
8
        "isHongkongUser": "com.android.builder.internal.ClassFieldImpl@f39f32b9"
9
    },
10
    "mConsumerProguardFiles": [
11
        "你指定的自动寻找的第三方库里的混淆文件"
12
    ],
13
    "mManifestPlaceholders": {
14
        "baidu_map_key_value": "20094"
15
    },
16
    "mProguardFiles": [
17
        "C:/Android/_coder/_workspace_as/FragmentTest/build/intermediates/proguard-files/proguard-android.txt-3.1.3",
18
        "C:/Android/_coder/_workspace_as/FragmentTest/app/proguard-rules.pro"
19
    ],
20
    "mResValues": {
21
        "app_icon_name": "com.android.builder.internal.ClassFieldImpl@efa088c5",
22
        "color_footer":"com.android.builder.internal.ClassFieldImpl@391e62f"
23
    },
24
    "minifyEnabled": "true",
25
    "name": "release",
26
    "pseudoLocalesEnabled": "false",
27
    "renderscriptDebuggable": "false",
28
    "renderscriptOptimLevel": "3",
29
    "signingConfig": {
30
        "keyAlias": "bea.keystore",
31
        "keyPassword": "beachinambk",
32
        "name": "beakeystore",
33
        "storeFile": "C:/Android/_coder/_workspace_as/FragmentTest/config/bea.keystore",
34
        "storePassword": "beachinambk",
35
        "storeType": "C:/Android/_coder/_workspace_as/FragmentTest/config/bea.keystore",
36
        "v1SigningEnabled": "true",
37
        "v2SigningEnabled": "true"
38
    },
39
    "testCoverageEnabled": "false",
40
    "zipAlignEnabled": "true"
41
}
里面的一些关键部分:
  • mBuildConfigFields 对应通过 buildConfigField 自定义的属性
  • mConsumerProguardFiles 对应通过 consumerProguardFiles 指定的自动寻找的第三方库里的混淆文件
  • mManifestPlaceholders  对应通过 manifestPlaceholders 替换清单文件中的标签
  • mProguardFiles 对应通过 proguardFiles 设置的混淆文件
  • mResValues 对应通过 resValue 替换掉指定的资源文件
  • signingConfig 代表设置的签名信息
其他字段基本见名知意。

以上内容我是这么产生的,首先将某一个 buildConfigField 的定义故意改错,比如定义了一个这个东西:
android {
    productFlavors {
        productB {
            buildConfigField "int", "countryCode", 20095//正确的格式是要用引号包住值,这里没有加引号所以编译就会报错
        }
    }
}
7
7
 
1
android {
2
    productFlavors {
3
        productB {
4
            buildConfigField "int", "countryCode", 20095//正确的格式是要用引号包住值,这里没有加引号所以编译就会报错
5
        }
6
    }
7
}
则编译时就会报错:
Could not find method buildConfigField() for arguments [int, countryCode, 20095] on ProductFlavor_Decorated{name=productB, dimension=null, minSdkVersion=null, targetSdkVersion=null, renderscriptTargetApi=null, renderscriptSupportModeEnabled=null, renderscriptSupportModeBlasEnabled=null, renderscriptNdkModeEnabled=null, versionCode=null, versionName=null, applicationId=null, testApplicationId=null, testInstrumentationRunner=null, testInstrumentationRunnerArguments={}, testHandleProfiling=null, testFunctionalTest=null, signingConfig=null, resConfig=null, mBuildConfigFields={isHongkongUser=com.android.builder.internal.ClassFieldImpl@f39f32b9}, mResValues={app_icon_name=com.android.builder.internal.ClassFieldImpl@efa088c6, color_footer=com.android.builder.internal.ClassFieldImpl@391e62f}, mProguardFiles=[], mConsumerProguardFiles=[], mManifestPlaceholders={}, mWearAppUnbundled=null} of type com.android.build.gradle.internal.dsl.ProductFlavor.
1
1
 
1
Could not find method buildConfigField() for arguments [int, countryCode, 20095] on ProductFlavor_Decorated{name=productB, dimension=null, minSdkVersion=null, targetSdkVersion=null, renderscriptTargetApi=null, renderscriptSupportModeEnabled=null, renderscriptSupportModeBlasEnabled=null, renderscriptNdkModeEnabled=null, versionCode=null, versionName=null, applicationId=null, testApplicationId=null, testInstrumentationRunner=null, testInstrumentationRunnerArguments={}, testHandleProfiling=null, testFunctionalTest=null, signingConfig=null, resConfig=null, mBuildConfigFields={isHongkongUser=com.android.builder.internal.ClassFieldImpl@f39f32b9}, mResValues={app_icon_name=com.android.builder.internal.ClassFieldImpl@efa088c6, color_footer=com.android.builder.internal.ClassFieldImpl@391e62f}, mProguardFiles=[], mConsumerProguardFiles=[], mManifestPlaceholders={}, mWearAppUnbundled=null} of type com.android.build.gradle.internal.dsl.ProductFlavor.
从这些错误信息里面我可以提取出构建时的一些配置信息,我是将它转换为了json格式。

清单文件

注意,清单文件里面设置的 package 和 build.gradle 中设置的 applicationId 的作用是不一样的。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.bqt.test">

    <!--下面 icon 的值 ${app_logo} 和下面的 meta-data 一样,是在 app 的 build.gradle 中通过 manifestPlaceholders 定义的-->
    <!--下面 label 的值 @string/app_icon_name 是在 app 的 build.gradle 中通过 resValue 定义的-->
    <application
        android:allowBackup="false"
        android:icon="${app_logo}"
        android:label="@string/app_icon_name"
        android:theme="@android:style/Theme.Holo.Light.DarkActionBar">

        <!--下面 label 的值 @string/main_activity_label_name 是在各个产品目录(如productA)里面的 res/values/strings.xml 中定义的-->
        <activity
            android:name=".MainActivity"
            android:label="@string/main_activity_label_name"
            android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <!--下面这个 Activity 也不是在 main 目录里面定义的,也是各个产品目录(如productA)里面的 java/完整包名/... 中定义的-->
        <activity android:name=".SecondActivity"/>

        <!--这些设置的值都可以在JAVA代码中通过 ApplicationInfo 获取到-->
        <meta-data
            android:name="chanel"
            android:value="${app_channel_value}"/>

        <meta-data
            android:name="releaseTime"
            android:value="${release_time_value}">
        </meta-data>

        <meta-data
            android:name="baiduMapKey"
            android:value="${baidu_map_key_value}">
        </meta-data>
    </application>

</manifest>
44
44
 
1
<?xml version="1.0" encoding="utf-8"?>
2
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
          package="com.bqt.test">
4
 
          
5
    <!--下面 icon 的值 ${app_logo} 和下面的 meta-data 一样,是在 app  build.gradle 中通过 manifestPlaceholders 定义的-->
6
    <!--下面 label 的值 @string/app_icon_name 是在 app  build.gradle 中通过 resValue 定义的-->
7
    <application
8
        android:allowBackup="false"
9
        android:icon="${app_logo}"
10
        android:label="@string/app_icon_name"
11
        android:theme="@android:style/Theme.Holo.Light.DarkActionBar">
12
 
          
13
        <!--下面 label 的值 @string/main_activity_label_name 是在各个产品目录(如productA)里面的 res/values/strings.xml 中定义的-->
14
        <activity
15
            android:name=".MainActivity"
16
            android:label="@string/main_activity_label_name"
17
            android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
18
            <intent-filter>
19
                <action android:name="android.intent.action.MAIN"/>
20
 
          
21
                <category android:name="android.intent.category.LAUNCHER"/>
22
            </intent-filter>
23
        </activity>
24
 
          
25
        <!--下面这个 Activity 也不是在 main 目录里面定义的,也是各个产品目录(如productA)里面的 java/完整包名/... 中定义的-->
26
        <activity android:name=".SecondActivity"/>
27
 
          
28
        <!--这些设置的值都可以在JAVA代码中通过 ApplicationInfo 获取到-->
29
        <meta-data
30
            android:name="chanel"
31
            android:value="${app_channel_value}"/>
32
 
          
33
        <meta-data
34
            android:name="releaseTime"
35
            android:value="${release_time_value}">
36
        </meta-data>
37
 
          
38
        <meta-data
39
            android:name="baiduMapKey"
40
            android:value="${baidu_map_key_value}">
41
        </meta-data>
42
    </application>
43
 
          
44
</manifest>

构建后生成的 generated 文件

生成的目录在【\app\build\generated\res\ resValues\ productA\debug\values\ generated.xml】
里面都是我们通过 resValue 用于替换掉指定的资源文件的配置
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Automatically generated file. DO NOT MODIFY -->
    <!-- Values from product flavor: productA -->
    <string name="app_icon_name" translatable="false">产品A</string>
    <color name="color_footer">#ffff0000</color>
</resources>
7
7
 
1
<?xml version="1.0" encoding="utf-8"?>
2
<resources>
3
    <!-- Automatically generated file. DO NOT MODIFY -->
4
    <!-- Values from product flavor: productA -->
5
    <string name="app_icon_name" translatable="false">产品A</string>
6
    <color name="color_footer">#ffff0000</color>
7
</resources>

构建后生成的 BuildConfig 文件

package com.bqt.test;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.bqt.test.productA";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "productA";
  public static final int VERSION_CODE = 13;
  public static final String VERSION_NAME = "master_tag_0718-2-g2a93c3e_2a93c3e";
  // Fields from build type: debug
  public static final String BASE_URL = "http://120.com/";
  // Fields from product flavor: productA
  public static final int countryCode = 20094;
  public static final boolean isHongkongUser = true;
}
x
15
 
1
package com.bqt.test;
2
 
          
3
public final class BuildConfig {
4
  public static final boolean DEBUG = Boolean.parseBoolean("true");
5
  public static final String APPLICATION_ID = "com.bqt.test.productA";
6
  public static final String BUILD_TYPE = "debug";
7
  public static final String FLAVOR = "productA";
8
  public static final int VERSION_CODE = 13;
9
  public static final String VERSION_NAME = "master_tag_0718-2-g2a93c3e_2a93c3e";
10
  // Fields from build type: debug
11
  public static final String BASE_URL = "http://120.com/";
12
  // Fields from product flavor: productA
13
  public static final int countryCode = 20094;
14
  public static final boolean isHongkongUser = true;
15
}

在代码中获取配置的值

public class MainActivity extends ListActivity {
	
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		Bundle metaData = getMetaData(this);
		String[] array = {
				"applicationId:" + BuildConfig.APPLICATION_ID + "\nApp的包名:" + getPackageName()
						+ "\n类的限定类名:" + getLocalClassName() + "  \n类所在的包名:" + getClass().getPackage().getName(),
				"Flavor:" + BuildConfig.FLAVOR,
				"versionName:" + BuildConfig.VERSION_NAME,
				"versionCode:" + BuildConfig.VERSION_CODE,
				"buildType:" + BuildConfig.BUILD_TYPE,
				"debuggable:" + BuildConfig.DEBUG,
				"",
				"buildConfigField,是否是香港用户:" + BuildConfig.isHongkongUser,
				"buildConfigField,国家代码:" + BuildConfig.countryCode,
				"buildConfigField,域名:" + BuildConfig.BASE_URL,
				"",
				"meta-data,打包时间:" + metaData.getString("releaseTime"),
				"meta-data,渠道名称:" + metaData.getString("chanel"),
				"meta-data,百度地图密钥:" + metaData.getInt("baiduMapKey"),//注意这里是 int 类型
				"",
				"resValue,应用名称:" + getResources().getString(R.string.app_icon_name),
				"resValue,设置的颜色:" + Integer.toHexString(getResources().getColor(R.color.color_footer)),
				"",
				"应用SHA1签名:" + getAppSignatureSHA1(this),
		};
		setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new ArrayList<>(Arrays.asList(array))));
	}
	
	@Override
	protected void onListItemClick(ListView l, View v, int position, long id) {
		super.onListItemClick(l, v, position, id);
		//注意:这个 SecondActivity 不是在 main 目录里面定义的,也是各个产品目录(如productA)里面的 java/完整包名/... 中定义的
		startActivity(new Intent(this, SecondActivity.class));
	}
	
	public static Bundle getMetaData(Context mContext) {
		try {
			ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(mContext.getPackageName(), PackageManager.GET_META_DATA);
			return applicationInfo.metaData;
		} catch (PackageManager.NameNotFoundException e) {
			e.printStackTrace();
			return new Bundle();
		}
	}
	
	public static String getAppSignatureSHA1(Context mContext) {
		Signature[] signature = getAppSignature(mContext);
		if (signature == null || signature.length <= 0) return "";
		String encryptSHA1 = encryptSHA1ToString(signature[0].toByteArray());
		return encryptSHA1.replaceAll("(?<=[0-9A-F]{2})[0-9A-F]{2}", ":$0");
	}
	
	public static Signature[] getAppSignature(Context mContext) {
		try {
			@SuppressLint("PackageManagerGetSignatures")
			PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), PackageManager.GET_SIGNATURES);
			return packageInfo == null ? null : packageInfo.signatures;
		} catch (PackageManager.NameNotFoundException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	private static String encryptSHA1ToString(final byte[] data) {
		return bytes2HexString(encryptSHA1(data));
	}
	
	private static String bytes2HexString(final byte[] bytes) {
		if (bytes == null) return "";
		int len = bytes.length;
		if (len <= 0) return "";
		char[] ret = new char[len << 1];
		for (int i = 0, j = 0; i < len; i++) {
			ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f];
			ret[j++] = HEX_DIGITS[bytes[i] & 0x0f];
		}
		return new String(ret);
	}
	
	private static byte[] encryptSHA1(final byte[] data) {
		if (data == null || data.length <= 0) return null;
		try {
			MessageDigest md = MessageDigest.getInstance("SHA1");
			md.update(data);
			return md.digest();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	private static final char HEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
}
96
96
 
1
public class MainActivity extends ListActivity {
2
    
3
    protected void onCreate(Bundle savedInstanceState) {
4
        super.onCreate(savedInstanceState);
5
        
6
        Bundle metaData = getMetaData(this);
7
        String[] array = {
8
                "applicationId:" + BuildConfig.APPLICATION_ID + "\nApp的包名:" + getPackageName()
9
                        + "\n类的限定类名:" + getLocalClassName() + "  \n类所在的包名:" + getClass().getPackage().getName(),
10
                "Flavor:" + BuildConfig.FLAVOR,
11
                "versionName:" + BuildConfig.VERSION_NAME,
12
                "versionCode:" + BuildConfig.VERSION_CODE,
13
                "buildType:" + BuildConfig.BUILD_TYPE,
14
                "debuggable:" + BuildConfig.DEBUG,
15
                "",
16
                "buildConfigField,是否是香港用户:" + BuildConfig.isHongkongUser,
17
                "buildConfigField,国家代码:" + BuildConfig.countryCode,
18
                "buildConfigField,域名:" + BuildConfig.BASE_URL,
19
                "",
20
                "meta-data,打包时间:" + metaData.getString("releaseTime"),
21
                "meta-data,渠道名称:" + metaData.getString("chanel"),
22
                "meta-data,百度地图密钥:" + metaData.getInt("baiduMapKey"),//注意这里是 int 类型
23
                "",
24
                "resValue,应用名称:" + getResources().getString(R.string.app_icon_name),
25
                "resValue,设置的颜色:" + Integer.toHexString(getResources().getColor(R.color.color_footer)),
26
                "",
27
                "应用SHA1签名:" + getAppSignatureSHA1(this),
28
        };
29
        setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new ArrayList<>(Arrays.asList(array))));
30
    }
31
    
32
    @Override
33
    protected void onListItemClick(ListView l, View v, int position, long id) {
34
        super.onListItemClick(l, v, position, id);
35
        //注意:这个 SecondActivity 不是在 main 目录里面定义的,也是各个产品目录(如productA)里面的 java/完整包名/... 中定义的
36
        startActivity(new Intent(this, SecondActivity.class));
37
    }
38
    
39
    public static Bundle getMetaData(Context mContext) {
40
        try {
41
            ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(mContext.getPackageName(), PackageManager.GET_META_DATA);
42
            return applicationInfo.metaData;
43
        } catch (PackageManager.NameNotFoundException e) {
44
            e.printStackTrace();
45
            return new Bundle();
46
        }
47
    }
48
    
49
    public static String getAppSignatureSHA1(Context mContext) {
50
        Signature[] signature = getAppSignature(mContext);
51
        if (signature == null || signature.length <= 0) return "";
52
        String encryptSHA1 = encryptSHA1ToString(signature[0].toByteArray());
53
        return encryptSHA1.replaceAll("(?<=[0-9A-F]{2})[0-9A-F]{2}", ":$0");
54
    }
55
    
56
    public static Signature[] getAppSignature(Context mContext) {
57
        try {
58
            @SuppressLint("PackageManagerGetSignatures")
59
            PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), PackageManager.GET_SIGNATURES);
60
            return packageInfo == null ? null : packageInfo.signatures;
61
        } catch (PackageManager.NameNotFoundException e) {
62
            e.printStackTrace();
63
            return null;
64
        }
65
    }
66
    
67
    private static String encryptSHA1ToString(final byte[] data) {
68
        return bytes2HexString(encryptSHA1(data));
69
    }
70
    
71
    private static String bytes2HexString(final byte[] bytes) {
72
        if (bytes == null) return "";
73
        int len = bytes.length;
74
        if (len <= 0) return "";
75
        char[] ret = new char[len << 1];
76
        for (int i = 0, j = 0; i < len; i++) {
77
            ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f];
78
            ret[j++] = HEX_DIGITS[bytes[i] & 0x0f];
79
        }
80
        return new String(ret);
81
    }
82
    
83
    private static byte[] encryptSHA1(final byte[] data) {
84
        if (data == null || data.length <= 0) return null;
85
        try {
86
            MessageDigest md = MessageDigest.getInstance("SHA1");
87
            md.update(data);
88
            return md.digest();
89
        } catch (NoSuchAlgorithmException e) {
90
            e.printStackTrace();
91
            return null;
92
        }
93
    }
94
    
95
    private static final char HEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
96
}

productA 目录中的部分内容

可以定义任意Java类,包括四大组件,比如Activity:
//注意:这个 SecondActivity 不是在 main 目录里面定义的,也是各个产品目录(如productA)里面的 java/完整包名/... 中定义的
public class SecondActivity extends Activity {
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.layout_a);
	}
}
7
7
 
1
//注意:这个 SecondActivity 不是在 main 目录里面定义的,也是各个产品目录(如productA)里面的 java/完整包名/... 中定义的
2
public class SecondActivity extends Activity {
3
    protected void onCreate(Bundle savedInstanceState) {
4
        super.onCreate(savedInstanceState);
5
        setContentView(R.layout.layout_a);
6
    }
7
}
当然也可以定义任意资源,包括 清单文件 AndroidManifest.xml 以及任意 res 目录中的文件,比如 strings.xml:
<resources>
    <string name="main_activity_label_name">productA目录中的字符串</string>
</resources>
3
 
1
<resources>
2
    <string name="main_activity_label_name">productA目录中的字符串</string>
3
</resources>
注意,最终 productA 目录中的东西都是要合并到 main 中的。
PS:当前选取的产品的文件夹颜色、图标符号等会与未选取的有差异,并且由于未选中的并不会参与编译,所以 java代码、xml布局文件看起来会有明细的不同,例如:
795730-20180721235354613-2072190138.png
2018-7-18

附件列表

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值