Android Build Variant的使用

Android Build Variant的使用通过AGP提供的Build Variant(构建变体)能力,我们可以将单个项目打包出不同的apk或者aar。Build Variant主要依赖BuildType和ProductFlavor提供的属性和

前言

通过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的继承结构

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

DefaultConfig.jpeg

可以看到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的概念还是比较好理解的,具体的配置属性和方法就不一一列举了,具体可以查看官方文档

image.png

接下来我们的重点放在ProductFlavorSourceSet

ProductFlavor

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

image.png

基础使用

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提供的属性

Property Description
aidl Android AIDL目录
assets Assets目录
java Java目录
jni JNI目录
jniLibs JNI libs目录
manifest AndroidManifest路径
name source set的名称
renderscript RenderScript目录
res res资源目录
resources Java 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")
        }
    }

image.png

源集类型

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种组合,如下图

image.png

我们可以遍历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窗口修改。

image.png

参考

配置 build 变体

Android Studio Set of source 代码源集

buildTypes——安卓gradle

今天的文章Android Build Variant的使用分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20346.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注