不同的Gradle多渠道配置总结

*本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布java

很久没有写博客了,忽然想把这段时间项目中使用到的技术和多渠道相关的认识总结分享一下~android

1、新增渠道

使用AndroidStudio配合gradle,能够很方便的输出多个渠道包,只须要在app Module下的build.gradle中,对productFlavors领域进行配置便可,假设我当前开发的项目,须要上线不一样的地区,一个是国内版,一个美国版,还有一个免费版,那么gradle能够这么配:api

android {
    productFlavors {
        china { // 中国版
        }
        america { // 美国版
        }
        free { // 免费版
        }
    }
}
复制代码

以上多渠道配置完成后,在Android Studio的Build Variants标签中,就会有不一样渠道变体供咱们选择了。当咱们想使用AS直接运行某个渠道的app时,就须要先在Build Variants标签中选择好变体,再点击"运行"按钮运行项目。微信

在productFlavors中还能够配置包名(applicationId)、版本号(versionCode)、版本名(versionName)、icon、应用名 等等,举个例子:架构

free {
	applicationId 'com.lqr.demo.free'
	versionCode 32
	versionName '1.3.2'
	manifestPlaceholders = [
			app_icon: "@drawable/ic_launcher",
			app_name: "菜鸡【免费版】",
	]
}
复制代码

注意:
这里配置的包名是applicationId,而不是清单文件里的packageName,applicationId与packageName是不同的。
咱们常说,一部Android设备上不能同时安装2个相同包名的app,指的是applicationId不能同样。
applicationId与packageName的区别可查阅:《ApplicationId versus PackageName》app

若是工程要求不一样渠道共存,或者对版本号、icon、应用名等有定制需求的话,那么这个多渠道配置就显得很是有用了。其中,app_icon、app_name是放在manifestPlaceholders的,这个实际上是在对AndroidManifest.xml中的占位符进行变量修改,也就是说,要定制icon或者应用名的话,还须要对清单文件作些小修改才行(增长一些占位符),如:ide

<application xmlns:tools="http://schemas.android.com/tools" android:icon="${app_icon}" android:label="${app_name}" android:theme="@style/AppTheme" android:largeHeap="true" tools:replace="android:label">
	...
</application>
复制代码

2、生成渠道变量

在新增渠道以后,咱们能够对这些渠道进行一块儿更多的配置,假设项目代码须要根据不一样的渠道,赋予不一样的数据,固然你能够选择在java代码中经过判断当前渠道名,配合switch来设置静态常量,但其实不用那么烦琐,并且有些静态数据经过相似config.gradle或config.properties这类配置文件来配置有比较好,那么gradle中的applicationVariants彻底能够帮助到咱们,如下面的配置Demo为例进行说明:布局

// 多渠道相关设置
applicationVariants.all { variant ->
    buildConfigField("String", "PROUDCT", "\"newapp\"")
    buildConfigField("String[]", "DNSS", "{\"http://119.29.29.29\",\"http://8.8.8.8\",\"http://114.114.114.114\"}")
    if (variant.flavorName == 'china') {
        buildConfigField("String", "DNS", "\"http://119.29.29.29\"")
    } else if (variant.flavorName == 'america') {
        buildConfigField("String", "DNS", "\"http://8.8.8.8\"")
    } else if (variant.flavorName == 'free') {
        buildConfigField("String", "DNS", "\"http://114.114.114.114\"")
    }
}
复制代码

经过gradle中提供的buildConfigField(),AndroidStudio会在执行脚本初始化时,根据当前所选变体将对于的配置转变为BuildConfig.java中的一个个静态常量:post

当我切换其余变体时,BuildConfig中的DNS也会跟着一块儿改变,这样,咱们在工程代码中,就不须要去判断当前渠道名来为某些静态常量赋值了。这里只是举例了使用buildConfigField()来生成String和String[]常量,固然也能够用来生成其它类型的常量数据,有兴趣的话,能够百度了解下。学习

3、变体的使用

上面提到了变体,那么变体是什么?能够这样理解,变体是由【Build Type】和【Product Flavor】组合而成的,组合状况有【Build Type】*【Product Flavor】种,举个例子,有以下2种构建类型,并配置了2种渠道:

Build Type:release debug
Product Flavor:china free
复制代码

那么最终会有四种 Build Variant 组成:

chinaRelease chinaDebug freeRelease freeDebug
复制代码

变体在复杂多渠道工程中是至关有用的,能够作到资源文件合并以及代码整合,这里的合并与整合怎么理解?咱们使用Android Studio进行项目开发时,会把代码文件与资源文件都存放在app/src目录下,一般是main下会有java、res、assets来区分存放代码文件和资源文件,你能够把main看做是默认渠道工程文件目录,也就是说main下存放在代码文件和资源文件对全部渠道来讲都是共同持有的。

那么,一旦出来了某些代码文件或者资源文件是个别渠道专属时,应该怎么办呢?由于main是共有的,因此理想状态下,咱们并不会把这类"不通用"的文件放在main下(这样作不会出错,可是作法很low,会增大apk包体积),Android Studio为变体作了很好的支持,咱们能够在app/src下,建立一个以渠道名命名的目录,用于存放这类个别渠道专属的代码文件和资源文件,如:

能够看到,当我选择freeDebug变体时,app/src/free下的目录高亮了,说明它们被Android Studio识别,在运行工程时,Android Studio会将free和main下的全部资源文件进行合并,将代码文件进行整合。同理,若是我选择的是chinaDebug变体,那么app/src/china下目录就会高亮。知道如何建立变体目录后,下面就开始进行资源合并与代码整合了。

一、资源合并

资源文件有哪些?咱们能够这样认为:

资源文件 = res下的全部文件 + AndroidManifest.xml 
复制代码

变体的资源合并功能简直是"神器"通常的存在,能够解决不少业务需求,如不一样渠道显示的icon不一样,应用名不一样等等。Android Studio在对变体目录和main目录进行资源合并时,会遵照这样的规则,假设当前选中的变体是freeDebug:

  • 某资源在free下有,在main中没有,那么在打包时,会将该资源直接合并到main资源中。
  • 某资源在free下有,在main中也有,那么在打包时,会以free为主,将free中资源替换掉main中资源。

针对上述2个规则,这里以string.xml为例进行说明,main下的string.xml是:

<resources>
    <string name="app_name">Demo</string>
    <string name="app_author">Lin</string>
</resources>
复制代码

free下的string.xml是:

<resources>
    <string name="error_append">发生错误</string>
    <string name="app_author">Lqr</string>
</resources>
复制代码

那么最终打出的apk包里的string.xml是:

<resources>
    <string name="app_name">Demo</string>
    <string name="error_append">发生错误</string>
    <string name="app_author">Lqr</string>
</resources>
复制代码

除了字符串合并外,还有图片(drawable、mipmap)、布局(layout)、清单文件(AndroidManifest.xml)的合并,具体能够本身尝试一下。其中,清单文件的合并须要提醒一点,若是渠道目录下的AndroidManifest.xml与main下的AndroidManifest.xml拥有相同的节点属性,但属性值不一样时,那么就须要对main下的AndroidManifest.xml进行修改了,具体修改要根据编译时报错来处理,因此,报错时不要慌,根据错误提示修改就是了。

注意:布局(layout)文件的合并是对整个文件进行替换的~。

二、代码整合

代码文件,顾名思义就是指java目录下的.java文件了,为何代码叫整合,而资源倒是合并呢?由于代码文件是没办法合并的,只能是整合,整合是什么意思?假设当前选中的变体是freeDebug,有一个java文件是Test.java,这个Test.java要么只存在free/java下,要么只存在于main/java下,如:

能够看到,一切正常,Test.java被AndroidStudio识别,但若是此时在main/java下也存在Test.java,那么Android Studio就会报错了:

代码整合是一个比较头痛的事,由于若是你是在渠道目录free下去引用main下的类,那么是彻底没有问题的,但若是反过来,在main下去引用free下的专属类时,状况就会变得很糟糕,当你切换其余变体时(如,切换成chinaDebug),这时工程就会报错了,由于变体切换,Test.java是free专属的,在chinaDebug变体下,free不会被识别,因而main就找不到对应的类了。

选择freeDebug变体时,正常引用Test.java:

选择chinaDebug变体时,找不到Test.java(只找到junit下的Test.java):

因此,对于代码整合,须要咱们在开发过程当中慎重考虑,多想一想如何将渠道目录与main目录进行解耦。好比可使用Arouter来解耦main与渠道目录下全部的Activity、Fragment,将类引用转换为字符串引用,所有将由Arouter来管理,又或者经过反射来处理,等等,这里顺带记录一下,我项目中使用ARouter来判断Activity、Fragment是否存在,和获取的相关方法:

/** * 获取到目标Delegate(仅仅支持Fragment) */
public <T extends CommonDelegate> T getTargetDelegate(String path) {
    return (T) ARouter.getInstance().build(path).navigation();
}

/** * 获取到目标类class(支持Activity、Fragment) */
public Class<?> getTargetClass(String path) {
    Postcard postcard = ARouter.getInstance().build(path);
    LogisticsCenter.completion(postcard);
    return postcard.getDestination();
}
复制代码

三、其余

前面只说到了res和java这2个目录,那么assets呢,它是属于哪一种?很惋惜,assets虽然是资源,但它不是合并,而是整合,也就是说,assets文件的处理方式跟java文件的处理方式是同样的,不能在渠道目录和main目录下同时存在相同的assets文件,这将对某些需求实现形成阻碍,举个例子,假设china与free使用的assets资源是同样的,而america单独使用本身的assets资源,而且这些assets资源文件名都是同样的,那这时要怎么办呢?给每一个渠道都放一份各自的assets资源吗?这种作法可行,但很low,缘由以下:

  1. 复用性差:都说了china与free使用的资源是同样的,从整个工程的角度来看,一个工程里放了2份如出一辙的assets资源文件,若是我有10个渠道,其中9个渠道使用的assets资源是同样的要怎么办,copy9次?
  2. 维护成本高:在开发行业里,需求变更是很常见的事,产品经理会时不时改下需求,因此,叫你改assets资源文件也是颇有可能的,若是你采用每一个渠道都放一份,那么当assets资源须要修改时,你就须要将每一个渠道的assets目录资源替换一遍。记得,是每次修改都要替换一遍。

正确的解决方案是使用sourceSets,对于sourceSets的使用,放到下一节去说明。

4、sourceSets

强大的gradle,经过sourceSets可让开发者可以自定义项目结构,如自定义assets目录、java目录、res目录,并且还能够是多个,但要知道的是,sourceSets并不会破坏变体的合并规则,它们是分开的,sourceSets只是起到了“扩充”的做用。这里先摆一下sourceSets的常规使用:

sourceSets {
	main {
		manifest.srcFile 'AndroidManifest.xml'
		java.srcDirs = ['src']
		aidl.srcDirs = ['src']
		renderscript.srcDirs = ['src']
		res.srcDirs = ['res']
		assets.srcDirs = ['assets']
	}
}
复制代码

一、复用assets资源

对于多渠道共用同一套assets资源文件这个问题,结合sourceSets,咱们能够这么处理,步骤以下:

  1. 把共用的assets资源存放到一个渠道目录下,如free/assets。
  2. 修改sourceSets规则,强制指定china渠道的assets目录为free/assets。
sourceSets {
	china {
		sourceSet.assets.srcDirs = ['src/free/assets']
	}
}
复制代码

这样配置之后,若是下次须要统一修改china与free的assets资源文件时,你就只须要把free/assets目录下的资源文件替换掉就行了。虽然这种写法已经知足前面说的需求了,可是还不够,还能够再优化一下,假设你有20个渠道,都使用同一套assets资源的话,按前面的写法你就要写19遍sourceSets配置了。

sourceSets {
	china {
		sourceSet.assets.srcDirs = ['src/free/assets']
	}
	a{
		sourceSet.assets.srcDirs = ['src/free/assets']
	}
	b{
		sourceSet.assets.srcDirs = ['src/free/assets']
	}
	...
}
复制代码

能够想像,在这个gradle文件中,光sourceSets配置就会有多长,你可能会说,一个项目怎么会有这么多渠道,很差意思,本人所处公司的业务需求就有20+个渠道的状况,话很少说,下面就来看看怎么优化好这段配置,若是你有学习过gradle,就应该知道,gradle是一种脚本,脚本是能够像写代码同样写逻辑的,那么上面的配置就能够转化为一个if-else代码片断:

sourceSets {
	sourceSets.all { sourceSet ->
	// println("sourceSet.name = ${sourceSet.name}")
	if (sourceSet.name.contains('Debug') || sourceSet.name.contains('Release')) {
		if (sourceSet.name.contains("china") 
				|| sourceSet.name.contains("a")
				|| sourceSet.name.contains("b")
				|| ...) {
				sourceSet.assets.srcDirs = ['src/free/assets']
			}
		}
	}
}
复制代码

如今你可能会以为这样写好像精简不了多少,不过一旦你的业务复杂起来,像这样用代码的逻辑思惟来处理配置,相信这会是一种不错的选择。

有兴趣的能够打印下sourceSet.name;if的写法不必定要用contains(),也能够用其余的判断方式,具体看开发者本身决定。

二、修改程序主入口

对于sourceSets的使用,除了针对修改assets之外,java文件、res资源文件、清单文件等等都是能够用一样的方式进行“扩充”的,好比不一样渠道共用一套java代码逻辑,那么咱们能够把这套代码单独抽取出来存放在一个其余目录下,而后使用sourceSets对其进行添加。这里就以我亲身经从来说明,我是如何经过sourceSets对于java和清单文件进行指定,而且完美解决此类"变态"需求的。

1)背景

新的app项目开发完成,如今须要将项目定制化后上线,项目总体采用 1个Activity + n个Fragment架构,这个Activity即是程序主入口,由于咱们产品是作机顶盒app开发,产品开发完成后,须要上线到盒子运营商(局方)的应用商店,而后经过盒子推荐位(EPG)启动咱们开发的app,所以上线后,须要提供app的包名和类名给到局方,假设新app的包名和类名分别以下:

包名:com.lqr.newapp
类名:com.lqr.newapp.MainActivity
复制代码

2)需求

把新app的包名和类名改为跟旧app的同样,由于局方那边不想换~~假设旧app的包名和类名以下:

包名:com.lqr.oldapp
类名:com.lqr.oldapp.MainActivity
复制代码

3)问题

修改包名很简单,可是修改入口类名就很麻烦了,若是我在该渠道目录下新增一个com.lqr.oldapp.MainActivity,并在其清单文件中进行注册,那么,在打包时,渠道目录下的AndroidManifest.xml会与main目录下的AndroidManifest.xml进行合并。

而main目录下的AndroidManifest.xml中已经注册了com.lqr.newapp.MainActivity,这样就会致使,最终输出apk包中的清单文件会有2个入口类。

是的,这样的产品交付出去,确实也能够应付掉局方的需求,可是,一旦盒子安装了这个app,那么盒子Launcher上可能会同时出现2个入口icon,到时又是一顿折腾,毕竟app上线流程比较麻烦,咱们最好是保证产品就一个入口。

4)分析

由于变体的资源合并规则,只要渠道目录和main目录下都存在AndroidManifest.xml,那么最终apk包里的清单文件合并出来的就会是2个文件的融合,因此,不能在这2个清单文件中分别注册入口。能够抽出2个不一样入口的AndroidManifest.xml存放到其余目录,main下的AndroidManifest.xml只注册通用组件便可。

5)操做:

a. 抽离MainActivity(oldapp)

在app目录下,建立一个support/entry目录(名字随意),用于存放入口相关功能的代码及资源文件,将com.lqr.oldapp.MainActivity放到support/entry/java目录下。

b. 抽离AndroidManifest.xml

在support目录下,建立manifest(名字随意),用于存放各渠道对应的AndroidManifest.xml,如:

其中newapp目录下的AndroidManifest.xml:

<application>
    <activity android:name="com.lqr.newapp.MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>

            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
</application>
复制代码

oldapp目录下的AndroidManifest.xml:

<application>
    <activity android:name="com.lqr.oldapp.MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
</application>
复制代码

c. 配置sourceSets

通过上面2步后,oldapp的MainActivity与各自的主入口注册清单文件就被抽离出去了,接下来就是使用sourceSets,根据不一样的渠道名,指定java与清单文件便可:

sourceSets {
    sourceSets.all { sourceSet ->
        // project.logger.log(LogLevel.ERROR, "sourceSet.name = " + sourceSet.name)
        if (sourceSet.name.contains('Debug') || sourceSet.name.contains('Release')) {
            if (sourceSet.name.contains("china")) {
                sourceSet.java.srcDirs = ['support/entry/java']
                sourceSet.manifest.srcFile 'support/manifest/oldapp/AndroidManifest.xml'
            } else {
                sourceSet.manifest.srcFile 'support/manifest/newapp/AndroidManifest.xml'
            }
        }
    }
}
复制代码

至此,最终打出的apk包中的AndroidManifest.xml中就只会保留一个主入口了,完美解决了局方要求。

三、解疑

Q:为何要把oldapp的MainActivity也抽出去?

A:由于oldapp的MainActivity不单只是china这个渠道须要用到,后续还会被其它渠道使用,为了后续复用考虑,因而就把MainActivity抽离出来。

Q:为何sourceSets中要判断sourceSet.name是否包含Debug或Release?

A:若是你有打印过sourceSet.name的话,你必定会发现输出的结果不仅仅只是那几个变体名,还有androidTest、test、main等等这些,但咱们仅仅只是想对工程变体(chinaDebug、chinaRelease、freeDebug、freeRelease)指定java目录和清单文件而已,若是对test、main这类“东西”也指定的话,结果并非咱们想要的,因此,必定要确保source配置的是咱们想要指定的变体,而非其余。

Q:sourceSets与变体合并的关系究竟如何?

A:以java源码目录为例,默认AS工程的java源码目录是【src/main/java】,在gradle中经过sourceSets指定了另外一个目录,好比【support/entry/java】,那么打包时,AS会认为这2个目录均是有效的java目录,因此,sourceSets指定的java目录仅仅只是对原来的扩充,而非替换。仍是以java源码目录为例,若是你的项目配置了多渠道,在不考虑sourceSets的状况下,项目在打包时,由于变体合并的特性,有效的java目录也是有2个,分别是【src/main/java】和【src/渠道名/java】,变体的合并规则不会由于sourceSets的配置而改变,若是将上述2种状况一块儿考虑上的话,那么最终打包时,有效的java目录则是3个,分别是【src/main/java】、【src/渠道名/java】、 【support/entry/java】。

5、渠道依赖

咱们知道,要在gradle中添加第三方库依赖的话,须要在dependencies领域进行配置,常见的configuration有provided(compileOnly)、compile(api)、implementation等等,它们的区别请自行百度查阅了解,针对【Build Type】、【Product Flavor】、【Build Variant】,这些configuration也会出现一些组合,如:

a. 构建类型组合

debugCompile	// 全部的debug变体都依赖
releaseCompile	// 全部的release变体都依赖
复制代码

b. 多渠道组合

chinaCompile	// china渠道依赖
americaCompile	// america渠道依赖
freeCompile		// free渠道依赖
复制代码

c. 变体组合

chinaDebugCompile		// chinaDebug变体依赖
chinaReleaseCompile		// chinaRelease变体依赖
americaDebugCompile		// americaDebug变体依赖
americaReleaseCompile	// americaRelease变体依赖
freeDebugCompile		// freeDebug变体依赖
freeReleaseCompile		// freeRelease变体依赖
复制代码

一、常规方式配置渠道依赖

经过上述组合就能够轻松配置好各类状况下的依赖了,如:

// autofittextview
compile 'me.grantland:autofittextview:0.2.+'
// leakcanary
debugCompile "com.squareup.leakcanary:leakcanary-android:1.6.1"
debugCompile "com.squareup.leakcanary:leakcanary-support-fragment:1.6.1"
releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:1.6.1"
// gson
chinaCompile 'com.google.code.gson:gson:2.6.2'
americaCompile 'com.google.code.gson:gson:2.6.2'
freeCompile 'com.google.code.gson:gson:2.5.2'
复制代码

二、代码方式配置渠道依赖

虽然官方给出的多种组合依赖能够解决几乎全部的依赖问题,但实际上,当渠道有不少不少时,整个gradle文件将变得冗长臃肿,你能想像20多个渠道中只有1个渠道依赖的gson版本不一样的状况吗?因此,这时候就须要考虑一下,充分利用好gradle做为脚本的特性,使用代码方式来进行渠道依赖:

dependencies {
    gradle.startParameter.getTaskNames().each { task ->
        // project.logger.log(LogLevel.ERROR, "lqr print task : " + task)
        if (task.contains('free')) {
            compile 'com.google.code.gson:gson:2.5.2'
        } else {
            compile 'com.google.code.gson:gson:2.6.2'
        }
    }
}
复制代码

另外,还有一种方式是我以前项目中使用过的,但这种方式不支持依赖远程仓库组件,这里也记录一下:

dependencies {
	// 配置 插件化库 依赖
    applicationVariants.all { variant ->
        if (variant.flavorName == 'china')
                ||variant.flavorName == 'america') {
            dependencies.add("${variant.flavorName}Compile", project(':DroidPluginFix'))
        } else {
            dependencies.add("${variant.flavorName}Compile", project(':DroidPlugin'))
        }
    }
}
复制代码

要知道,如下写法是正确的,但就是不生效:

dependencies.add("${variant.flavorName}Compile", 'com.google.code.gson:gson:2.6.2')
复制代码

DroidPluginFix是最新官方适配了Android七、8的DroidPlugin,而DroidPlugin则没有适配,由于历史缘由,须要对不一样的渠道依赖不一样的DroidPlugin版本。

6、结语

在公司待了有1年半了,这段时间里对本身的要求很严格,学习了不少新知识,并大胆的用于实践,收获颇多,由于公司业务的特殊性,对gradle以及多渠道的掌握要求比较高,因此,这也是我这段时间来重点学习的一部分,可是毕竟是单独学习这些知识,可能也会存在一些掌握不到位的状况,因此上面提到的知识若是有误或有更好的处理方式,欢迎各位指出和分享,thx~

相关文章
相关标签/搜索