为什么我们需要掌握Gradle的这些知识?
Posted 郭霖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么我们需要掌握Gradle的这些知识?相关的知识,希望对你有一定的参考价值。
6月27日,京东和中国联通签约,就"京东便利店"项目进行战略合作。消费者将从京东便利店、"掌柜宝"商户等线下门店快捷办理或订购联通业务,相应获得联通释放的专享电子券,用于门店消费。中国联通将成为"京东便利店"的首家电信运营商全国级战略合作伙伴,并在"掌柜宝"APP上线提供通信服务。
本篇来自投稿老司机 very_mrq的投稿,分享了gradle超详细解析,一起来看看!希望大家喜欢。
https://www.jianshu.com/u/1bcfdb6fad58
为什么要学gradle
android studio已经出来很久了,相信大部分公司都已经从eclipse转AS了,反正我都快忘记eclipse如何操作了。AS很多强大功能,其中很突出的一项就是gradle构建。还记得第一次用依赖的时候,那感觉爽翻。但是因为build代码不熟悉,也遇到很多坑,经常会莫名其妙报错,当时只能上网查,然后一板一眼的配置。作为程序猿这种不能完全掌握的感觉是最不爽的,很早就想彻底掌控它了。
其次,作为有梦想的咸鱼,不能只做代码的搬运工,这种高阶必备的知识点还是需要掌握的。比如国内比较火热的插件化、热更新都会涉及到gradle插件知识,所以想要进阶,必须掌握gradle。
Extension介绍
讲解之前我们先看一段熟悉的代码:
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.example.gradletest"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//省略.....
}
}
相信这段代码在座的各位都看吐了吧?是的,笔者也一样。刚开始接触AS的时候,看这段代码感觉就感觉在看天书,反正按规定配置就对了。今天我们就把他扒光了看看有什么特别。仔细观察其实也比较好理解,就是android的一些配置:
compileSdkVersion指的是当前的androidSDK版本、applicationId指的是包名、versionCode指的是版本号....balabala...
虽然经过几年的百度、谷歌熏陶已经很熟悉了,但是为什么这么配置呢?接下来就为各位看官解答。我们可以把一个gradle项目看做一个Project,而这个Project就类似JAVA的类,而一个类自然就需要成员变量咯,所以我们首先就来创建一个pojo类,就命名为Person吧:
public class Person {
String name;
int age;
@Override
public String toString() {
return "I am $name, $age years old"
}
}
然后就像上篇文章那样创建一个自定义插件:
class MyPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
project.extensions.add("person", Person)
Person ext = project.person
println ext
}
}
调用了project的extensions,他可以视为变量的容器,只要往里加就OK了,然后就可以通过project调用到person了。很简单,我直接打印了该person,接下来就是重点了,我们需要在build.gradle里面配置person。
person{
name 'atom'
age 18
}
是不是就像android的标签一样?这下应该很好理解gradle里为何这么写了吧。其实这里就相当于我们给person做了初始化,只是用有些像json的写法而已。不过这样我们就可以配置项目参数了,是不是很方便?我们来验证一下,执行一下gradle printPerson命令:
如我们所想,打印了我们配置的18岁的atom:)
task介绍
task,如其名:任务,gradle就是由一个一个任务来完成的。他其实也是一个类,有自己的属性,也可以"继承",甚至他还有自己的生命周期。它的定义方式有很多,下面我们来看一个最简单的实现:
task myTask {
println "myTask invoked!"
}
gradle就是一个一个task组成的,我们平时遇到莫名其妙的报错,最常用的就是clean(斜眼笑),其实也是一个task而已,包括我们debug运行、打包签名等等等等,都是Android studio给我们视图化了而已,本质也是执行task。所以我们执行一下clean命令:gradle clean。
myTask invoked!还是被打出来了,为啥?其实上面我也提到了,task有自己的生命周期。初始化---配置期---执行期,我从实战gradle里偷了一张图:
其实上面代码就是在配置阶段而已,配置阶段的代码只要在执行任何task都会跟着执行,如果我们希望不被执行的话,就只能放到执行阶段了,最直接的方法就是加到doLast、doFirst里,当然实现方式也挺多的,我就列两种吧:
project.task('printPerson') {
group 'atom'
//定义时
doLast {
println "this is doLast1"
}
}
Task printPerson= project.tasks["printPerson"]
//后来加
printPerson.doFirst {
println "this is doFirst1"
}
printPerson.doFirst {
println "this is doFirst2"
}
printPerson.doLast {
println "this is doLast2"
}
刚开始可能不好理解这种方式,其实可以理解为task里有一个队列,队列中是task将会执行的action,当doFirst 时,就会在队列头部插入一个action,而doLast则在队列尾部添加,当执行该任务时就会从队列中取出action依次执行,就如同我们上述代码,执行gradle printPerson时,打印结果如下:
> Task :app:printPerson
this is doFirst2
this is doFirst1
this is doLast1
this is doLast2
注意,此时必须要执行gradle printPerson时才会打印了,clean之流就没用了。
刚刚提到过,task其实也是一个类,没错,就如同object一样,task的基类是DefaultTask ,我们也可以自定义一个task,必须继承DefaultTask,如下:
class MyTask extends DefaultTask {
String message = 'This is MyTask'
// @TaskAction 表示该Task要执行的动作,即在调用该Task时,hello()方法将被执行
@TaskAction
def hello(){
println "Hello gradle. $message"
}
}
其实task还有许多内容,比如输入输出文件outputFile、Input。but,对于android开发目前来说,这就够了,但是了解一下也是很有好处的,比如我们构建速度就和输入输出有关,是不是被这个坑爹的构建速度郁闷到很多次!我推荐大家去看看《Android+Gradle权威指南》之类的书,目前网上资料不全不够系统,当然,官方文档还是值得好好看的~
闲话少叙,继续主题。task还有一个比较重要的内容,就是“继承”。没错,task之间也是可以‘继承’的,不过此继承非彼继承,而是通过dependsOn关键字实现的,我们先来看看实现:
task task1 << {
println "this is task1"
}
task task2 << {
println "this is task2"
}
task task3 << {
println "this is task3"
}
task task4 << {
println "this is task4"
}
task1.dependsOn('task2')
task2.dependsOn('task3')
task1.dependsOn('task4')
‘继承’关系为:task1-->task2/task4和task2-->task3。我们先打印出来:
> Task :app:task3
this is task3
> Task :app:task2
this is task2
> Task :app:task4
this is task4
> Task :app:task1
this is task1
可以看到,task3是最先执行的,这是因为dependsOn的逻辑就是首先执行‘最高’辈分的,最后执行‘最低’辈分的。什么意思呢,拿代码来说就是task1‘继承’了task2,task4,而task2‘继承’了task3,意思就是task3是task1的爷爷辈,所以最先执行,这样相信大家能够理解了吧。
自定义gradle插件
gradle就是构建工具,他使用的语言是groovy,我们可以在build.gradle里面写代码来控制,当然,如果代码很多,希望单独提取出来,那么可以使用自定义gradle插件来实现,没错,AndroidDSL(plugin)就是一个自定义插件而已,所以学习它之前需要了解如何自定义gradle插件。
首先,我们新建一个项目,会得到两个build.gradle,一个是主项目的,一个是全局的。我们先只看项目里的build文件,其中自定义插件的重点:
apply plugin: 'com.android.application'
这就表示我们引入了Android的插件了,下面来演示一下最简单的自定义插件步骤。
事实上所有的自定义插件都需要继承一个plugin类,然后重写apply方法,如下:
apply plugin: com.atom.MyPlugin
class MyPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println "myPlugin invoked!"
}
}
把上述代码加到build.gradle下面,在命令行运行随意的命令:gradlew clean(windows)
调用成功了,当然这是最简单的方式,不过理解这里就能继续看AndroidDSL了,具体步骤可以自行谷歌
Android Plugin源码解析
对于如何查看源码,其实很简单,只需要把全局build.gradle里的classpath的依赖加入项目build.gradle文件的dependencies里就好了,如下图:
这样就能在项目的依赖树里找到源码了,可以选择复制出来看,也可以直接在AS里看,个人感觉AS也挺方便的
打开第一个,就能看见很多plugin展现在我们眼前了,我们最熟悉的就是AppPlugin和LibraryPlugin了。前者就是主项目需要依赖的插件,后者就是组件化的module需要依赖的插件。我们拿最常用的AppPlugin来说把,根据上面定义插件的步骤,我们就直接看apply方法,由于Appplugin继承了basePlugin,所以又转到basePlugin:
其实最重要的实现在于调用了三次threadRecorder.record,值得一说的是:this::configureProject这种写法。这是 JAVA8 里 lambda 语法,等于:()->this.configureProject(),匿名内部类的简写方式,后面会回调这里。J8已经出来很久了,相信大家有了一定的了解,这里就不多说。我们就来看看这个record方法:
@Override
public void record(
@NonNull ExecutionType executionType,
@NonNull String projectPath,
@Nullable String variant,
@NonNull VoidBlock block) {
//刚刚初始化过的单例
ProfileRecordWriter profileRecordWriter = ProcessProfileWriter.get();
//创建GradleBuildProfileSpan的建造者
GradleBuildProfileSpan.Builder currentRecord =
create(profileRecordWriter, executionType, null);
try {
//刚刚提到的回调
block.call();
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
//写入GradleBuildProfileSpan并保存
write(profileRecordWriter, currentRecord, projectPath, variant);
}
}
以上代码做了如下事情:
创建GradleBuildProfileSpan.Builder
回调方法
写入GradleBuildProfileSpan并保存到spans中
我们先不管回调,看1、3的代码,首先create:
private GradleBuildProfileSpan.Builder create(
@NonNull ProfileRecordWriter profileRecordWriter,
@NonNull ExecutionType executionType,
@Nullable GradleTransformExecution transform) {
long thisRecordId = profileRecordWriter.allocateRecordId();
// am I a child ?
@Nullable
Long parentId = recordStacks.get().peek();
long startTimeInMs = System.currentTimeMillis();
final GradleBuildProfileSpan.Builder currentRecord =
GradleBuildProfileSpan.newBuilder()
.setId(thisRecordId)
.setType(executionType)
.setStartTimeInMs(startTimeInMs);
if (transform != null) {
currentRecord.setTransform(transform);
}
if (parentId != null) {
currentRecord.setParentId(parentId);
}
currentRecord.setThreadId(threadId.get());
recordStacks.get().push(thisRecordId);
return currentRecord;
}
代码不少,但是做的事情很简单,就是创建了一个GradleBuildProfileSpan.Builder,并设置了它的threadId、Id、parentId...等等一系列线程相关的东西,并保存在一个双向队列里,并放入threadLocal里解决多线程并发问题。这个threadLocal若不理解的可以移步我的另一篇文章:消息机制:Handler源码解
https://blog.csdn.net/verymrq/article/details/78681901
接下来是write
private void write(
@NonNull ProfileRecordWriter profileRecordWriter,
@NonNull GradleBuildProfileSpan.Builder currentRecord,
@NonNull String projectPath,
@Nullable String variant) {
// pop this record from the stack.
if (recordStacks.get().pop() != currentRecord.getId()) {
Logger.getLogger(ThreadRecorder.class.getName())
.log(Level.SEVERE, "Profiler stack corrupted");
}
currentRecord.setDurationInMs(
System.currentTimeMillis() - currentRecord.getStartTimeInMs());
profileRecordWriter.writeRecord(projectPath, variant, currentRecord);
}
调用了profileRecordWriter.writeRecord,继续:
/** Append a span record to the build profile. Thread safe. */
@Override
public void writeRecord(
@NonNull String project,
@Nullable String variant,
@NonNull final GradleBuildProfileSpan.Builder executionRecord) {
executionRecord.setProject(mNameAnonymizer.anonymizeProjectPath(project));
executionRecord.setVariant(mNameAnonymizer.anonymizeVariant(project, variant));
spans.add(executionRecord.build());
}
这里使用建造者模式创建了GradleBuildProfileSpan,并保存到了spans里。关于1、3步骤说了这么多,其实也就是做了这点事情,接下来才是重点了,关于回调:回头看basePlugin里的3个回调方法configureProject、configureExtension、createTasks,方法里传的type已经暴露了他们的作用:
BASE_PLUGIN_PROJECT_CONFIGURE:plugin的基础设置、初始化工作
BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION:EXTENSION的初始化工作
BASE_PLUGIN_PROJECT_TASKS_CREATION:plugin的task创建
这三步基本囊括了自定义插件的所有内容,我这里简单先介绍一下第一步,后面再详细解析很重要的后面两步
private void configureProject() {
final Gradle gradle = project.getGradle();
extraModelInfo = new ExtraModelInfo(project.getPath(), projectOptions, project.getLogger());
checkGradleVersion(project, getLogger(), projectOptions);
sdkHandler = new SdkHandler(project, getLogger());
if (!gradle.getStartParameter().isOffline()
&& projectOptions.get(BooleanOption.ENABLE_SDK_DOWNLOAD)) {
SdkLibData sdkLibData = SdkLibData.download(getDownloader(), getSettingsController());
sdkHandler.setSdkLibData(sdkLibData);
}
androidBuilder =
new AndroidBuilder(
project == project.getRootProject() ? project.getName() : project.getPath(),
creator,
new GradleProcessExecutor(project),
new GradleJavaProcessExecutor(project),
extraModelInfo.getSyncIssueHandler(),
extraModelInfo.getMessageReceiver(),
getLogger(),
isVerbose());
dataBindingBuilder = new DataBindingBuilder();
dataBindingBuilder.setPrintMachineReadableOutput(
SyncOptions.getErrorFormatMode(projectOptions) == ErrorFormatMode.MACHINE_PARSABLE);
if (projectOptions.hasRemovedOptions()) {
androidBuilder
.getIssueReporter()
.reportWarning(Type.GENERIC, projectOptions.getRemovedOptionsErrorMessage());
}
if (projectOptions.hasDeprecatedOptions()) {
extraModelInfo
.getDeprecationReporter()
.reportDeprecatedOptions(projectOptions.getDeprecatedOptions());
}
// Apply the Java plugin
project.getPlugins().apply(JavaBasePlugin.class);
project.getTasks()
.getByName("assemble")
.setDescription(
"Assembles all variants of all applications and secondary packages.");
gradle.addBuildListener(...)
//省略监听代码...
}
这个方法主要做了以下几件事情:
利用project,初始化了sdkHandler、androidBuilder、dataBindingBuilder等几个必备的对象。
依赖了JavaBasePlugin,这个很重要,是JAVA构建项目需要的插件。
对gradle创建做了监听,做了内存、磁盘缓存的工作,你可以在build\intermediates\dex-cache\cache.xml文件下找到JAR包等内容的缓存。
接下来让我们看看第二步configureExtension方法,由于篇幅原因省略了许多参数信息但不影响阅读:
private void configureExtension() {
ObjectFactory objectFactory = project.getObjects();
//1==============
final NamedDomainObjectContainer<BuildType> buildTypeContainer =
project.container(
BuildType.class,
new BuildTypeFactory(
objectFactory,
project,
extraModelInfo.getSyncIssueHandler(),
extraModelInfo.getDeprecationReporter()));
final NamedDomainObjectContainer<ProductFlavor> productFlavorContainer =
project.container(
ProductFlavor.class,
new ProductFlavorFactory(
objectFactory,
project,
extraModelInfo.getDeprecationReporter(),
project.getLogger()));
final NamedDomainObjectContainer<SigningConfig> signingConfigContainer =
project.container(
SigningConfig.class,
new SigningConfigFactory(
objectFactory,
GradleKeystoreHelper.getDefaultDebugKeystoreLocation()));
final NamedDomainObjectContainer<BaseVariantOutput> buildOutputs =
project.container(BaseVariantOutput.class);
project.getExtensions().add("buildOutputs", buildOutputs);
sourceSetManager = createSourceSetManager();
//2==============
extension =createExtension();
ndkHandler =new NdkHandler();
@Nullable
FileCache buildCache = BuildCacheUtils.createBuildCacheIfEnabled(project, projectOptions);
GlobalScope globalScope = new GlobalScope();
//3===============================
variantFactory = createVariantFactory(globalScope, androidBuilder, extension);
taskManager =createTaskManager();
variantManager = new VariantManager();
//省略部分代码
// create default Objects, signingConfig first as its used by the BuildTypes.
variantFactory.createDefaultComponents(
buildTypeContainer, productFlavorContainer, signingConfigContainer);
}
首先创建了四个NamedDomainObjectContainer,是由project的Container方法返回的,这些东东是什么有什么用,光靠百度谷歌基本上是很难找到了(笔者写这篇文章的时候gradle方面的资料还是相当匮乏的),所以我们得学会看官方文档咯~
也不算太难,笔者这二流英语水平都能看懂:创建一个容器,用来管理泛型中定义的类。而factory自然就是创建该类的工厂了。所以根据上诉代码,不难知道创建了BuildType、ProductFlavor、SigningConfig、BaseVariantOutput这四个类的容器了。
稍微熟悉构建的童鞋应该很清楚这几个类的用处:
buildType
构建类型,在Android Gradle工程中,它已经帮我们内置了debug和release两个构建类型,可以分别设置不同包名等信息。
signingConfigs
签名配置,可以设置debug和release甚至自定义方式时的不同keystore,及其密码等信息。
ProductFlavor
多渠道打包必备,用处很多笔者也有推荐文章介绍
而最后一个BaseVariantOutput,“望文生义“不难才到就是输出文件咯~
把project、androidBuilder以及刚刚提到的几个类作为参数创建了extension,这里使用了策略模式,createExtension是一个抽象方法,真正实现是在AppPlugin
protected BaseExtension createExtension(//参数省略) {
return project.getExtensions()
.create();//参数省略
}
就如同我们之前介绍创建Extension的方式一样,通过project创建了名为“android”的Extension,类型为AppExtension,这个类就包含了我们平时用到的版本号、包名等等信息,为我们构建项目打下了基础。
用同样的方式创建了variantFactory、taskManager、variantManager,最后设置了默认的构建信息。
关于这几个类的作用分别是:
variantFactory构建信息的工厂、taskManager构建任务、variantManager各种不同构建方式及多渠道构建的管理
这就涉及到gradle核心:task了
在继续讲解之前,我先讲解一下assemble,assemble是一个task,用于构建、打包项目,平时我们打包签名APK就是调用了该方法,由于我们有不同buildTypes,以及不同productFlavors,所以我们还需要生成各种不同的assemble系列方法:assemble{productFlavor}{BuildVariant},比如
assembleRelease:打所有的渠道Release包
assemblexiaomiRelease:打小米Release包
assemblehuaweiRelease:打华为Release包
AndroidDSL负责生成我们在build.gradle里配置的多渠道等各种assemble系列方法。然后assemble方法会依赖很多方法,就如同我们上文所叙述的,依次执行assemble依赖的方法完成构建,好了,我们还是来看源码理解吧!
第三步就是Android的创建task部分,该方法其实就是调用了createTasksBeforeEvaluate和createAndroidTasks两个方法,其中createAndroidTasks才是重点,该方法中又调用了variantManager的createAndroidTasks方法,跳过与本文无关的细节,看下面重要的地方:
/**
* Variant/Task creation entry point.
*/
public void createAndroidTasks() {
//省略部分代码...
for (final VariantScope variantScope : variantScopes) {
recorder.record(
ExecutionType.VARIANT_MANAGER_CREATE_TASKS_FOR_VARIANT,
project.getPath(),
variantScope.getFullVariantName(),
() -> createTasksForVariantData(variantScope));
}
}
循环调用createTasksForVariantData方法,该方法就是为所有的渠道创建相关方法了,而variantScopes则存放了各种渠道、buildType信息,继续查看该方法:
/** Create tasks for the specified variant. */
public void createTasksForVariantData(final VariantScope variantScope) {
//1======
final BaseVariantData variantData = variantScope.getVariantData();
final VariantType variantType = variantData.getType();
final GradleVariantConfiguration variantConfig = variantScope.getVariantConfiguration();
final BuildTypeData buildTypeData = buildTypes.get(variantConfig.getBuildType().getName());
if (buildTypeData.getAssembleTask() == null) {
//2======
buildTypeData.setAssembleTask(taskManager.createAssembleTask(buildTypeData));
}
// Add dependency of assemble task on assemble build type task.
//3======
taskManager
.getTaskFactory()
.configure(
"assemble",
task -> {
assert buildTypeData.getAssembleTask() != null;
task.dependsOn(buildTypeData.getAssembleTask().getName());
});
//4======
createAssembleTaskForVariantData(variantData);
if (variantType.isForTesting()) {
//省略测试相关代码...
} else {
//5======
taskManager.createTasksForVariantScope(variantScope);
}
}
解析variant渠道等信息
创建AssembleTask存入data里
给assemble添加依赖
创建该variant的专属AssembleTask
给AssembleTask添加构建项目所需task依赖(dependsOn)
看一下4、5步骤详细代码,首先是第四步,给每个渠道和buildtype创建对应的方法:
/** Create assemble task for VariantData. */
private void createAssembleTaskForVariantData(final BaseVariantData variantData) {
final VariantScope variantScope = variantData.getScope();
if (variantData.getType().isForTesting()) {
//测试
} else {
BuildTypeData buildTypeData =
buildTypes.get(variantData.getVariantConfiguration().getBuildType().getName());
Preconditions.checkNotNull(buildTypeData.getAssembleTask());
if (productFlavors.isEmpty()) {
//如果没有设置渠道
} else {
//省略部分代码...
// assembleTask for this flavor(dimension), created on demand if needed.
if (variantConfig.getProductFlavors().size() > 1) {
//获取渠道名
final String name = StringHelper.capitalize(variantConfig.getFlavorName());
final String variantAssembleTaskName =
//组装名字
StringHelper.appendCapitalized("assemble", name);
if (!taskManager.getTaskFactory().containsKey(variantAssembleTaskName)) {
//创建相应渠道方法
Task task = taskManager.getTaskFactory().create(variantAssembleTaskName);
task.setDescription("Assembles all builds for flavor combination: " + name);
task.setGroup("Build");
//渠道方法依赖AssembleTask
task.dependsOn(variantScope.getAssembleTask().getName());
}
taskManager
.getTaskFactory()
.configure(
"assemble", task1 -> task1.dependsOn(variantAssembleTaskName));
}
}
}
}
注释已经很清晰了,最重要的就是组装名字,创建相应的渠道打包方法。这里我们又学到一种定义task的方式:TaskFactory.create。这是AndroidDSL自定义的类,他的实现类是TaskFactoryImpl,由kotlin语言实现:
class TaskFactoryImpl(private val taskContainer: TaskContainer): TaskFactory {
//省略大部分方法....
override fun configure(name: String, configAction: Action<in Task>) {
val task = taskContainer.getByName(name)
configAction.execute(task)
}
}
省略了大部分方法,但也很简单了,使用代理模式代理了taskContainer,而这个taskContainer就是gradle的类了,查看官方文档:
<T extends Task> T create(String name,
Class<T> type,
Action<? super T> configuration)
throws InvalidUserDataException
Creates a Task with the given name and type, configures it with the given action, and adds it to this container.
After the task is added, it is made available as a property of the project, so that you can reference the task by name in your build file. See here for more details.
//....
就是创建一个task并放入容器里。参数只有第三个比较难猜一点点,看了文档也就很清楚:给task设置一个action而已。当然,这里并没有调用这个重载方法,不过我这里是为了第5步介绍,好的,让我们回到第5步操作:
taskManager.createTasksForVariantScope(variantScope);
这里taskManager由BasePlugin的子类实现,实现类为ApplicationTaskManager,我们看一下他的createTasksForVariantScope方法:
@Override
public void createTasksForVariantScope(@NonNull final VariantScope variantScope) {
BaseVariantData variantData = variantScope.getVariantData();
assert variantData instanceof ApplicationVariantData;
createAnchorTasks(variantScope);
createCheckManifestTask(variantScope);
//....
// Add a task to create the res values
//创建资源文件相关
recorder.record(
ExecutionType.APP_TASK_MANAGER_CREATE_GENERATE_RES_VALUES_TASK,
project.getPath(),
variantScope.getFullVariantName(),
() -> createGenerateResValuesTask(variantScope));
// Add a task to merge the resource folders
//创建资源文件相关
recorder.record(
ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_RESOURCES_TASK,
project.getPath(),
variantScope.getFullVariantName(),
(Recorder.VoidBlock) () -> createMergeResourcesTask(variantScope, true));
//省略类似方法
}
这个方法就是构建精髓所在,他创建了我们构建项目所需要的大部分task,比如创建manifest文件,合并manifest文件,处理resource文件...等等task,这些task就是构建项目的基石,这里我就放出任玉刚大佬总结的主要构建方法:
具体每个方法做了什么,就是需要大家阅读源码参透了,这里我只负责梳理大致流程,嘿嘿...
下面我们就看看创建的第一个方法createAnchorTasks,在这个方法里面调用了createCompileAnchorTask,他的实现是:
private void createCompileAnchorTask(@NonNull final VariantScope scope) {
final BaseVariantData variantData = scope.getVariantData();
//....
scope.getAssembleTask().dependsOn(scope.getCompileTask());
}
为什么我要专门说一下这个task,就是因为最后一句代码,AssembleTask依赖的该task,也就是说当我们执行AssembleTask的时候,该task会提前执行,而构建原理也在于此,该task也会依赖其他task,就这样一层层依赖,构建时就会调用所有的相关task,这样就完成了我们Android项目的构建。
自定义插件实战
不知道大家有没有遇到过这样的需求:公司有一款产品,而客户需要将公司产品做制定化操作,如:修改app包名、appIcon、appName、以及引导页等一些资源文件,以便客户展示他自己的广告。这样可能会有很多定制化产品需要去打包,以前采用一个一个手动更改并打包,一打就是一下午,随着定制化越来越多,每次更新都要这样打,估计都快疯了吧。这个时候大家首先会想到利用Android系统的多渠道打包方法productFlavors,比如这样:
android {
productFlavors {
xiaomi{
applicationId "com.xiaomi.cn"
}
google{
applicationId "com.google.cn"
}
huawei{
applicationId "com.huawei.cn"
}
}
}
这样就可以修改包名,appName等一些需求,但是资源文件可能就不太方便了,可能有童鞋会说,在res下面多放几张图片,然后利用manifestPlaceholders来修改,没错,这样的方式也可以实现,不过万一你公司的渠道定制包很很多呢?100个、500个,难道需要放那么多张没用的图片进去?那app得多大。
其实主要就是这两个问题:
打包时资源文件无法自动更换
若手动更换,一个一个打成百上千的包那时间成本可不是盖的
那么今天,我们就来写一个自动替换资源文件的gradle插件,彻底解决这个问题。
首先我们需要写个自定义插件,具体步骤我在第一篇系列文章里提到过,也有推荐文章,这里我就放出插件结构就好(文末有DEMO,大家可以有需要的话可以查看)
首先我们需要写一个Plugin,并重写他的apply方法:
public class ResourceFlavorsPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//这里写自定义内容
}
}
首先我们理一下我们思路:
我们需要打很多渠道包
每个渠道包都需要修改包名、资源文件等
第一点我们利用AndroidDSL来实现:
android {
flavorDimensions "define"
productFlavors {
"define1" {
dimension "define"
applicationId "com.atom.define1"
manifestPlaceholders.put("appName", "定制1")
}
"define2" {
dimension "define"
applicationId "com.atom.define2"
manifestPlaceholders.put("appName", "定制2")
}
//其他渠道省略....
}
}
这个很简单就不多说了,下面就到我们需要做的事情:修改资源。我们需要在打每个渠道包之前把对应资源文件修改成相应的,而我们每次打包都需要运行assemble系列方法,比如
打所有发布版的包:assembleRelease
打define1的发布版的包:assembleDefine1Release
以此类推,而根据上一篇文章我们又知道assemble依赖了许多task,这样的话我们就好办了,只需要在执行资源合并task之前就修改资源文件就好了。查看源码发现preBuild这个task就是最先执行的几个task之一,所以我们需要获取到该task,执行他的doFirst即可
project.android.applicationVariants.all { variant ->
String variantName = variant.name.capitalize()
def variantFlavorName = variant.flavorName
Task preBuild = project.tasks["pre${variantName}Build"]
if (variantFlavorName == null || "" == variantFlavorName) {
return
}
preBuild.doFirst {
//在这里替换资源文件
println "${variantFlavorName} resource is changed!"
}
}
利用applicationVariants获取variant,然后就能获取到variantName也就是渠道包的打包方式,然后做一些字符串拼接,就获取到相应task名称,然后再从Project的taskContainer里取出就好。下一步就需要修改资源文件了,而gradle如何实现这一操作呢?我也不知道,这时候只能求助官方了,通过一些时间的查阅,我终于在官方文档中找到了这个方法,其实很简单:
这是在project下的一个方法,官方文档介绍的很详细了,连demo都有,可以说相当良心了。from和into后面分别跟源文件和被替换的文件就可以了,当然,文件夹也行,所以我们的代码就变成了这样
project.android.applicationVariants.all { variant ->
//...
preBuild.doFirst {
project.copy {
from "../resourceDir/${variantFlavorName}"
into "../app/src/main/res"
}
println "${variantFlavorName} resource is changed!"
}
}
为了更好的扩展性,能够在build文件中设置源文件位置、名称等,我们可以用extension来操作,首先创建一个pojo类
public class FlavorType {
/**
* 存放渠道包图片的路径
*/
String resourceDir
/**
* 主项目名
*/
String appName
}
再稍微修改一下我们的代码:
project.extensions.add("rfp", FlavorType)
project.afterEvaluate {
FlavorType ext = project.rfp
def resourceDir = ext.resourceDir
def appName = ext.appName
project.android.applicationVariants.all { variant ->
//...
preBuild.doFirst {
project.copy {
from "../${resourceDir}/${variantFlavorName}"
into "../${appName}/src/main/res"
}
println "${variantFlavorName} resource is changed!"
}
}
}
这样就可以在build文件中自定义了,就像这样:
rfp{
resourceDir 'definepic'
appName 'app'
}
https://github.com/mrqatom/ChangeResourceDemo
如果对您有帮助的话,希望给个star鼓励一下~谢谢!
最最最后:我也把该项目上传到了jcenter,可以直接使用哦~具体参见github说明
欢迎长按下图 -> 识别图中二维码
以上是关于为什么我们需要掌握Gradle的这些知识?的主要内容,如果未能解决你的问题,请参考以下文章
掌握Gradle,还需要掌握这些知识--Groovy MOP
掌握Gradle,还需要掌握这些知识--Groovy MOP