Gradle 多渠道打包

我们在日常开发中多多少少都会遇到多渠道打包的情况。这些版本可能会上传到不同的应用市场,也可能是是线下多渠道推荐。有时候可能不同的渠道使用的资源图片都不一样。古老的做法就是,需要打多少个渠道包拉出多少份代码分支,分别替换对应的资源文件和包名配置信息等。这种做法非常的耗时耗力。Gradle 可以帮我们用一份代码通过配置实现打出所有的渠道包。

1. 创建多渠道资源文件目录

首先,我们新创建一个工程,然后在 main 模块下面,根据不同渠道创建不同的资源文件目录。我们先定义一个简单的页面,里面显示渠道跟一张图片。layout 布局文件如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="26sp"
        android:textColor="@color/colorPrimary"
        android:text="@string/chanl_name" />
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@mipmap/girl"
        >
    </ImageView>
</LinearLayout>

然后我们根据我们用到的资源,对不同渠道配置不同的资源。我们这里 model 比较简单,主要是图片和字符串,图片这里我们不同渠道显示的不同。具体目录如下所示:

图片描述
图片描述

Tips: 注意这里我们创建资源文件目录的时候不能包含 test ,否则会编译报错的。我亲自尝试过 res-test

2. 配置多渠道资源路径

前面我们创建了多渠道的资源目录,那么我们就需要将它配置在 build.gradle 中。我们前面介绍 AS 中 Android 项目的 Gradle 配置时讲到过,我们在 SourceSet 闭包配置。具体多渠道配置如下:

    //配置资源文件路径,可动态指定不同版本资源文件
    sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java.srcDirs = ['src/main/java']
            resources.srcDirs = ['src/main/resources']
            aidl.srcDirs = ['src/main/aidl']
            renderscript.srcDirs = ['src/maom']
            res.srcDirs = ['src/main/res']
            assets.srcDirs = ['src/main/assets']
            jniLibs.srcDir 'src/main/jniLibs'
        }
        //用各自对应的资源文件路径
        chanlA.res.srcDirs = ['src/main/res-a']
        chanlB.res.srcDirs = ['src/main/res-b']
        androidTest.setRoot('tests')
        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')
    }

3. 配置渠道及不同渠道包名等

配置多渠道在 Gradle 中使用 productFlavors 闭包,在这个闭包中我们可以配置所有的渠道,以渠道名为闭包,每个渠道可以配置 applicationId签名信息等。我们这里简单配置了applicationId。如下:

    /*
    * 渠道Flavors,配置不同Chanl的app
    * 资源文件不能用test字段命令(会运行报错的,如res-test)
    * */
    productFlavors {
        chanlA{
            applicationId "com.bthvi.chanla"//可为不同版本动态指定包名
        }
        chanlB{
            applicationId "com.bthvi.chanlb"//可为不同版本动态指定包名
        }
    }

4. 配置 apk 包输出的路径

我们如果不配置 APK 包的输出路径默认是在,build 文件夹下的。但是我们这里为了方便可以将编译生成的 APK 包放在一个自己指定的目录下面。我们这里放在根目录的 apks 文件夹下,然后以不同渠道的 applicationId 以及日期存放不同的渠道的包。具体的 Gradle 配置信息如下:

    // 版本比较多时,自定义导出的APK文件的名称
    applicationVariants.all {
        //获取是release还是debug版本
        def buildType = it.buildType.name
        def fileName
        //下面的channel是获取渠道号,你获取渠道号不一定会和我的代码一样,因为有可能你的渠道名称的规则和我的不一样,我的规则是${渠道名}-${applicationId},所以我是这样取的。
        def channel = it.productFlavors[0].name.split("-")[0]
        //获取当前时间的"YYYY-MM-dd"格式。
        def createTime = new Date().format("YYYY-MM-dd", TimeZone.getTimeZone("GMT+08:00"))
        it.getPackageApplication().outputDirectory = new File(project.rootDir.absolutePath + "/apks/${it.productFlavors[0].applicationId}/${createTime}")
        it.outputs.each {
                //我此处的命名规则是:渠道名_版本名_创建时间_构建类型.apk[大家也可以根据自己的需求命名]
                fileName = "${channel}_v${defaultConfig.versionName}_${createTime}-${buildType}.apk"
                //打印出apk文件名称,以便及时查看是否满足要求
                logger.quiet("文件名:>>>>>>>>>>>>>>>>>>>>>>${fileName}")
                //重新对apk命名。
                //Gradle4.0以下版本
                //it.outputFile = new File(it.outputFile.parent, fileName)
                //Gradle4.0(含)以上版本
                it.outputFileName = fileName
        }
    }

5. 经常遇到的坑

  1. 这里我们需要注意的一点就是自己的 Gradle 版本,根据不同的版本命名 apk 文件的命令也是不一样的。在 Gradle 4.0 以上使用it.outputFile = new File(it.outputFile.parent, fileName)会抛出以下的错误.

  2. 我们还是需要注意自己的 Gradle 版本。由于 Gradle 3.0.0 之后有一种自动匹配消耗库的机制,便于 debug variant 自动消耗一个库,然后就是必须要所有的flavor 都属于同一个维度。所以如果你的 Gradle 是 3.0.0 之后的版本,我们需要在主 app 的 build.gradle 里面的 defaultConfig 闭包中加入 flavorDimensions “versionCode”。意思是flavor dimension 它的维度就是版本号。否则编译会报如下错:

6. 最终的效果

配置完上面的所有之后,我们先看下完整的 build.gradle 长啥样。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.bthvi.chanl"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        flavorDimensions "versionCode"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    //配置资源文件路径,可动态指定不同版本资源文件
    sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java.srcDirs = ['src/main/java']
            resources.srcDirs = ['src/main/resources']
            aidl.srcDirs = ['src/main/aidl']
            renderscript.srcDirs = ['src/maom']
            res.srcDirs = ['src/main/res']
            assets.srcDirs = ['src/main/assets']
            jniLibs.srcDir 'src/main/jniLibs'
        }
        //用各自对应的资源文件路径
        chanlA.res.srcDirs = ['src/main/res-a']
        chanlB.res.srcDirs = ['src/main/res-b']
        androidTest.setRoot('tests')
        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')
    }

    // 版本比较多时,自定义导出的APK文件的名称
    applicationVariants.all {
        //获取是release还是debug版本
        def buildType = it.buildType.name
        def fileName
        //下面的channel是获取渠道号,你获取渠道号不一定会和我的代码一样,因为有可能你的渠道名称的规则和我的不一样,我的规则是${渠道名}-${applicationId},所以我是这样取的。
        def channel = it.productFlavors[0].name.split("-")[0]
        //获取当前时间的"YYYY-MM-dd"格式。
        def createTime = new Date().format("YYYY-MM-dd", TimeZone.getTimeZone("GMT+08:00"))
        it.getPackageApplication().outputDirectory = new File(project.rootDir.absolutePath + "/apks/${it.productFlavors[0].applicationId}/${createTime}")
        it.outputs.each {
                //我此处的命名规则是:渠道名_版本名_创建时间_构建类型.apk[大家也可以根据自己的需求命名]
                fileName = "${channel}_v${defaultConfig.versionName}_${createTime}-${buildType}.apk"
                //打印出apk文件名称,以便及时查看是否满足要求
                logger.quiet("文件名:>>>>>>>>>>>>>>>>>>>>>>${fileName}")
                //重新对apk命名。
                //Gradle4.0以下版本
                //it.outputFile = new File(it.outputFile.parent, fileName)
                //Gradle4.0(含)以上版本
                it.outputFileName = fileName
        }
    }
    /*
    * 渠道Flavors,配置不同Chanl的app
    * 资源文件不能用test字段命令(会运行报错的,如res-test)
    * */
    productFlavors {
        chanlA{
            applicationId "com.bthvi.chanla"//可为不同版本动态指定包名

        }
        chanlB{
            applicationId "com.bthvi.chanlb"//可为不同版本动态指定包名
        }
    }
}
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

然后我们就可以执行命令打包了,我们这里执行 gradle build 就可以打出所有渠道的 debug 和 release 包了。具体打包输出及 APK 包的输出目录如下所示:


下面我们装上不同渠道的包,看下是否不同渠道的图片和字符串是不同的

7. 小结

这一节内容我们主要讲了如何通过 Gradle 配置不同资源文件实现一份代码打出多个渠道包。相较于之前我们用不同的代码分支来打多渠道包,效率提升很多。