90%不知道的Android Build Variant的使用

Posted 码农 小生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了90%不知道的Android Build Variant的使用相关的知识,希望对你有一定的参考价值。

前言

通过AGP提供的Build Variant(构建变体)能力,我们可以将单个项目打包出不同的apk或者aar。Build Variant主要依赖BuildTypeProductFlavor提供的属性和方法,配置一系列规则,将代码和资源进行组合。

BuildType 我的理解是偏向于定义构建的模式,debug和release就是两种不同构建模式,通过BuildType我们可以配置签名信息等。

ProductFlavor可以理解为产品变种,作用是定义项目的不同版本,例如免费版和付费版,国内版和国际版等,这样的好处是只需要维护一个工程就行,最大程度上复用代码和资源。

说到ProductFlavor不得不提到SourceSetSourceSet是源集的意思,通过SourceSet,我们可以指定不同版本的资源路径。注意SourceSet不是AGP独有的概念,Java Plugin也有SourceSet的定义。

BuildType

BuildType 可以配置我们需要的构建类型,最常见的是debug和release,用于区分开发模式和发布模式,这两种类型是AGP默认创建的。当然我们还可以定义其他的build类型。在buildTypes闭包中我们可以配置很多属性,具体包含哪些呢?我们先来看看buildType对应的类com.android.build.gradle.internal.dsl.BuildType的继承结构

我们再看看defaultConfig对应的类com.android.build.gradle.internal.dsl.DefaultConfig的继承结构

可以看到defaultConfigbuildType最终到继承自BaseConfigImpl,所以为什么我们平时总感觉某个参数在哪都可以出现的,原因就在于映射的类都相关的继承关系。BaseConfigImpl中定义的属性包括如下

public abstract class BaseConfigImpl implements Serializable, BaseConfig {

    private String mApplicationIdSuffix = null;
    private String mVersionNameSuffix = null;
    private final Map<String, ClassField> mBuildConfigFields = Maps.newTreeMap();
    private final Map<String, ClassField> mResValues = Maps.newTreeMap();
    private final List<File> mProguardFiles = Lists.newArrayList();
    private final List<File> mConsumerProguardFiles = Lists.newArrayList();
    private final List<File> mTestProguardFiles = Lists.newArrayList();
    private final Map<String, Object> mManifestPlaceholders = Maps.newHashMap();
    @Nullable
    private Boolean mMultiDexEnabled;

    @Nullable
    private File mMultiDexKeepProguard;

    @Nullable
    private File mMultiDexKeepFile;
		....
}

基本使用

buildTypes中配置的属性会覆盖defaultConfig中的定义

android {
    defaultConfig {
        manifestPlaceholders = [hostName:"www.example.com"]
        ...
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        debug {
            applicationIdSuffix ".debug"
            debuggable true
        }
    }
}

BuildType的概念还是比较好理解的,具体的配置属性和方法就不一一列举了,具体可以查看[官方文档]

接下来我们的重点放在ProductFlavorSourceSet

ProductFlavor

首先我们来看看ProductFlavorDefaultConfig的关系,它们都继承自BaseFlavor,由于ProductFlavor的优先级高于DefaultConfig,所以DefaultConfig的属性都可以在ProductFlavor覆盖

基础使用

productFlavors最常见的用法就是配置不同的渠道包了,相信以下配置大家都非常熟悉

productFlavors{
    xiaomi{
        //指定manifest中CHANNEL_VALUE的值
        manifestPlaceholders = [CHANNEL_VALUE: "xiaomi"]
    }
    huawei{
        manifestPlaceholders = [CHANNEL_VALUE: "huawei"]
    }
}

上面的例子通过定义不同的flavor,覆盖manifest中的CHANNEL_VALUE配置参数,这样就起到对不同渠道进行区分的作用。

productFlavors{
        flavorDimensions 'isFree', 
        free {
            //免费版和付费版最低适配版本不同
            minSdkVersion 21
            //免费版和付费版使用不同的包名
            applicationId 'com.example.android.free'
            //写入不同的res值
            resValue "string",'tag','free'
            //指定所在的维度
            dimension 'isFree'
        }
        paid {
            minSdkVersion 24
            applicationId 'com.example.android.paid'
            resValue "string",'tag','paid'
            dimension 'isFree'
        }
    }

创建并配置产品变种后,点击通知栏中的 Sync Now。同步完成后,Gradle 会根据 build 类型和产品变种自动创建 build 变体,并按照 <product-flavor><Build-Type> 为其命名。在上面的例子中,则Gradle 会创建以下 build 变体:

  • freeDebug
  • freeRelease
  • paidDebug
  • paidRelease

flavorDimensions

注意,从AGP 3.0开始,必须至少明确指定一个flavor dimensiondimension的指定方式在上面的例子已经体现了。可能有部分同学按照要求随意给不同的flavor指定了某个dimension,但是不知道这个东西具体的作用。

实际上dimension的作用将多个产品变种的配置组合在一起。例如上面的例子,我们是通过是否付费这个角度定义了freepaid两个flavor。那么它们俩就应该属于同一个dimension。所以我们分配了isFree这个dimension

假如我们的app还区分国内版和国际版,那么我们还可以定义一个 areadimension,如下

productFlavors{
        flavorDimensions 'isFree',"area"
        free {
            minSdkVersion 21
            applicationId 'com.example.android.free'
            resValue "string",'tag','free'
            dimension 'isFree'
        }
        paid {
            minSdkVersion 24
            applicationId 'com.example.android.paid'
            resValue "string",'tag','paid'
            dimension 'isFree'
        }

        domestic{
            dimension 'area'
        }
        overseas {
            dimension 'area'
        }
    }

通过上面的定义,我们就拥有了四种组合,分别是

  • freeDomestic 免费国内版
  • freeOverseas 免费国际版
  • paidDomestic 付费国内版
  • paidOverseas 付费国际版

这里有个注意的地方是,flavor组合的顺序是根据flavorDimensions的元素排序决定的。假如我们将

isFreearea的顺序颠倒一下

flavorDimensions "area",'isFree'

那么原先的freeDomestic将变成domesticFree。这会造成什么影响呢?

实际上第一个flavor是具有高优先级的。 假如free和domestic都定义了各自的包名

productFlavors{
        flavorDimensions "area",'isFree'
        free {
            applicationId 'com.example.android.free'
            dimension 'isFree'
        }

        domestic{
            dimension 'area'
            applicationId 'com.example.android.domestic'
        }
    }

那么最终的包名将会是com.example.android.domestic

matchingFallbacks

在某些情况下,app模块包含了某些flavors而library模块却没有,在这种情况下,app无法和library的flavor相匹配,通过指定matchingFallbacks来兜底。例如下面这个例子,app依赖了library

//app build.gradle
productFlavors{
        flavorDimensions 'isFree'
        free {
            dimension 'isFree'
            matchingFallbacks = ['demo']
        }
        paid {
            dimension 'isFree'
        }
    }

//library build.gradle
productFlavors{
        flavorDimensions 'isFree'
        demo {
            dimension 'isFree'
        }
        paid {
            dimension 'isFree'
        }
    }

当执行assembleFreeRelease时,由于library不存在freeflavor,那么会使用demo进行替代。

如果app不指定matchingFallbacks的话,是无法通过编译的,会报如下错误

> Could not resolve all artifacts for configuration ':app:freeDebugCompileClasspath'.
   > Could not resolve project :library.
     Required by:
         project :app

所以,如果library和app都定义了ProductFlavor,那么需要对齐,否则需要指定matchingFallbacks进行兜底。注意,library和app需要定义在同个dimension下。

SourceSet

SourceSet即源代码集,我们可以使用 SourceSet 代码块更改 Gradle 为源代码集的每个组件收集文件的位置。这样我们就无需改变文件的位置。换句话说,有了SourceSet,我们可以按照自己的偏好指定代码和资源的路径

基本使用

属性

以下是AndroidSourceSets提供的属性

PropertyDescription
aidlAndroid AIDL目录
assetsAssets目录
javaJava目录
jniJNI目录
jniLibsJNI libs目录
manifestAndroidManifest路径
namesource set的名称
renderscriptRenderScript目录
resres资源目录
resourcesJava resources目录

以上的配置除了manifest对应的是AndroidSourceFile 对象,即为单一文件,其余的都是AndroidSourceDirectorySet对象,我们来看下AndroidSourceDirectorySet接口提供了哪些方法。

public interface AndroidSourceDirectorySet extends PatternFilterable {

    @NonNull
    String getName();

    //添加资源路径到集合中,最终AGP会从集合里取出所有的文件
    @NonNull
    AndroidSourceDirectorySet srcDir(Object srcDir);

    //添加多个资源路径到集合中
    @NonNull
    AndroidSourceDirectorySet srcDirs(Object... srcDirs);

    //指定资源的路径,与上面两个方法不同的时候,该方法会覆盖原有的集合
    @NonNull
    AndroidSourceDirectorySet setSrcDirs(Iterable<?> srcDirs);

    //以FileTree形式返回资源
    @NonNull
    FileTree getSourceFiles();

    //返回过滤规则
    @NonNull
    PatternFilterable getFilter();

    //将源文件夹作为一个列表返回
    @NonNull
    List<ConfigurableFileTree> getSourceDirectoryTrees();

    //返回资源文件列表
    @NonNull
    Set<File> getSrcDirs();

    /** Returns the [FileCollection] that represents this source sets. */
    @Incubating
    FileCollection getBuildableArtifact();
}

因此我们可以修改源集的位置,我们来看一个简单配置


    def basePath = projectDir.parentFile.absolutePath
    def resPath = new File(basePath, "res")
    def manifestPath = new File(basePath, "AndroidManifest.xml")
    sourceSets {
        main {
            res.srcDir(resPath)
            manifest.srcFile(manifestPath)
        }
    }

我们可以通过sourceSets任务来打印具体的配置

:app:sourceSets

//输出

main
----
Compile configuration: compile
build.gradle name: android.sourceSets.main
Java sources: [app/src/main/java]
//AndroidMnaifest路径被改到app根目录下
Manifest file: AndroidManifest.xml
//可以看刚才添加的res目录
Android resources: [app/src/main/res, res]
Assets: [app/src/main/assets]
AIDL sources: [app/src/main/aidl]
RenderScript sources: [app/src/main/rs]
JNI sources: [app/src/main/jni]
JNI libraries: [app/src/main/jniLibs]
Java-style resources: [app/src/main/resources]

paid
----
Compile configuration: paidCompile
build.gradle name: android.sourceSets.paid
Java sources: [app/src/paid/java]
Manifest file: app/src/paid/AndroidManifest.xml
Android resources: [app/src/paid/res]
Assets: [app/src/paid/assets]
AIDL sources: [app/src/paid/aidl]
RenderScript sources: [app/src/paid/rs]
JNI sources: [app/src/paid/jni]
JNI libraries: [app/src/paid/jniLibs]
Java-style resources: [app/src/paid/resources]

//省略其他源集
....

方法

方法描述
setRoot(path)将源集的根设置为给定的路径。源集合的所有条目都位于此根目录下。

通过setRoot方法,我们可以直接指定某个源集的目录,例如如果你有多个ProductFlavor,并且创建了对应的源集目录,那么我们可以把非main的目录都放到一起,避免src目录太多文件。

sourceSets.all { set ->
        if (set.name.toLowerCase().contains(flavor)
                && !set.name.equals("main")) {
            set.setRoot("src/other/$flavor")
        }
    }

源集类型

main 源集包含了所有其他构件变体共用的代码和资源,即所有的其他构建变体,src/main是其共同拥有的。

其他源集目录为可选项,如果我们想要为某个单独的构建变体添加特有的代码或者资源,可以创建对应的目录。例如,构建“demoDebug”这个变体, Gradle 会查看以下目录,并为它们指定以下优先级

  1. src/demoDebug/(build 变体源代码集)
  2. src/debug/(build 类型源代码集)
  3. src/demo/(产品变种源代码集)
  4. src/main/(主源代码集)

当存在重复的资源时,Gradle 将按以下优先顺序决定使用哪一个文件(左侧源集替换右侧源集的文件和设置):

构建变体 > 构建类型[BuildType] > 产品风味[ProductFlavor] > 主源集[main] > 库依赖项
  • java/ 目录中的所有源代码将一起编译以生成单个输出

    注意的是,java文件是不能被覆盖的,如果我们在main目录中创建了src/main/Utility.java,那么是不能其他源集目录中定义同名文件进行覆盖的,因为,Gradle 在构建过程中会查看这两个目录并抛出“重复类”错误。如果我们想要在不同的 build 类型有不同版本的 Utility.java,只能让每个 build 类型定义各自的文件版本,这样是比较麻烦的。

  • 所有Manifest都将合并为一个清单。合并的优先级和上面提到的一致。

  • 同样,values/ 目录中的文件也会合并在一起。如果两个文件同名,例如存在两个 strings.xml 文件,按照上述的优先级覆盖。

  • res/ 和 asset/ 目录中的资源会打包在一起。

  • 最后,在构建 APK 时,Gradle 会为库模块依赖项随附的资源和清单指定最低优先级。

配置过滤规则

回顾上面的AndroidSourceDirectorySet接口,其继承了PatternFilterable接口

public interface PatternFilterable {

    Set<String> getIncludes();

    Set<String> getExcludes();

    PatternFilterable setIncludes(Iterable<String> includes);

    PatternFilterable setExcludes(Iterable<String> excludes);

    PatternFilterable include(String... includes);

    PatternFilterable include(Iterable<String> includes);

    PatternFilterable include(Spec<FileTreeElement> includeSpec);

    PatternFilterable include(Closure includeSpec);

    PatternFilterable exclude(String... excludes);

    PatternFilterable exclude(Iterable<String> excludes);

    PatternFilterable exclude(Spec<FileTreeElement> excludeSpec);

    PatternFilterable exclude(Closure excludeSpec);

该接口提供了一系列的includeexclude方法,我们可以对源集目录做一些过滤。

sourceSets {
        main {
            java {
                exclude 'com/cooke/library/Test.java'
                exclude 'com/cooke/library/model/**.java'
            }

        }
    }

上面例子提到,其他的源集目录无法覆盖同名java文件,但是我们可以通过SourceSet对main目录中的java进行exclude.

注意:includeexclude并不能对res生效,如果想要对res进行过滤,需要通过定义res/raw/keep.xml,详见Android文档,这里就不具体展开了。

Variant

Variant 即为变体,可以分为ApplicationVariantLibraryVariant,分别对应了apk的变体和aar的变体。变体的构成由BuildTypeProductFlavor组合而成.即

variant = buildType * productFlavor

例如上面我们定义了freepaid两种productFlavor,结合debugrelease两种buildType,就产生了4种组合,如下图

我们可以遍历ApplicationVariantLibraryVariant列表,干预构建apk和aar的过程。

最常见的就是重命名apk的名称。

android.applicationVariants.all {
        variant ->
            variant.outputs.all {
                outputFileName = "${applicationId}_${buildType.name}_v${defaultConfig.versionName}_${releaseTime()}.apk"
            }
    }

选择某个Variant

AGP会为每种variant创建一系列的variant任务。例如apk打包对应的是 assemble$VariantName

,aab打包对应的是bundle$VariantName, 如果我们需要在开发过程中选中某个variant,可以在Build Vairants窗口修改。


最后,我整理了一下Android相关的核心笔记 ,面试题等一下资料,我免费分享给大家。如果需要直接点击链接领取点击这里免费领取

以上是关于90%不知道的Android Build Variant的使用的主要内容,如果未能解决你的问题,请参考以下文章

90%的开发者都不知道的 Kotlin技巧以及原理解析

Android Studio 中build.gradle文件的详细解析

Android编译系统-概览

Android Project和app中两个build.gradle配置的区别

Unity Android build 随机崩溃

90%的人都不知道的项目范围验收工具