×
广告

gradle超详细解析

96
very_mrq
2018.06.05 15:47* 字数 5332

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

一、为什么要学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命令:

image

如我们所想,打印了我们配置的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)

image

调用成功了,当然这是最简单的方式,不过理解这里就能继续看AndroidDSL了,具体步骤可以自行谷歌

四、Android Plugin源码解析

对于如何查看源码,其实很简单,只需要把全局build.gradle里的classpath的依赖加入项目build.gradle文件的dependencies里就好了,如下图:

image

这样就能在项目的依赖树里找到源码了,可以选择复制出来看,也可以直接在AS里看,个人感觉AS也挺方便的

image

打开第一个,就能看见很多plugin展现在我们眼前了,我们最熟悉的就是AppPlugin和LibraryPlugin了

前者就是主项目需要依赖的插件,后者就是组件化的module需要依赖的插件

我们拿最常用的AppPlugin来说把,根据上面定义插件的步骤,我们就直接看apply方法,由于Appplugin继承了basePlugin,所以又转到basePlugin:

public void apply(@NonNull Project project) {
    //省略一些初始化及错误检查代码

    //初始化线程信息记录者
    threadRecorder = ThreadRecorder.get();
    //保存一些基础信息
    ProcessProfileWriter.getProject(project.getPath())
            .setAndroidPluginVersion(Version.ANDROID_GRADLE_PLUGIN_VERSION)
            .setAndroidPlugin(getAnalyticsPluginType())
            .setPluginGeneration(GradleBuildProject.PluginGeneration.FIRST)
            .setOptions(AnalyticsUtil.toProto(projectOptions));

    BuildableArtifactImpl.Companion.disableResolution();
    //判断是不是新的API,这里我们只看最新实现,老的就不多说了
    if (!projectOptions.get(BooleanOption.ENABLE_NEW_DSL_AND_API)) {
        TaskInputHelper.enableBypass();

        threadRecorder.record(
                ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE,
                project.getPath(),
                null,
                this::configureProject);

        threadRecorder.record(
                ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION,
                project.getPath(),
                null,
                this::configureExtension);

        threadRecorder.record(
                ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION,
                project.getPath(),
                null,
                this::createTasks);
    } else {
        //省略以前的实现
    }
}

其实最重要的实现在于调用了三次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);
    }
}

以上代码做了如下事情:

1、创建GradleBuildProfileSpan.Builder

2、回调方法

3、写入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源码解析

接下来是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已经暴露了他们的作用:

1、BASE_PLUGIN_PROJECT_CONFIGURE:plugin的基础设置、初始化工作

2、BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION:EXTENSION的初始化工作

3、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(...)
    //省略监听代码...

}

这个方法主要做了以下几件事情:

1、利用project,初始化了sdkHandler、androidBuilder、dataBindingBuilder等几个必备的对象。

2、依赖了JavaBasePlugin,这个很重要,是JAVA构建项目需要的插件。

3、对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);
}

1、首先创建了四个NamedDomainObjectContainer,是由project的Container方法返回的,这些东东是什么有什么用,光靠百度谷歌基本上是很难找到了(笔者写这篇文章的时候gradle方面的资料还是相当匮乏的),所以我们得学会看官方文档咯~

image

也不算太难,笔者这二流英语水平都能看懂:创建一个容器,用来管理泛型中定义的类。而factory自然就是创建该类的工厂了。

所以根据上诉代码,不难知道创建了BuildType、ProductFlavor、SigningConfig、BaseVariantOutput这四个类的容器了。

稍微熟悉构建的童鞋应该很清楚这几个类的用处:

buildType

构建类型,在Android Gradle工程中,它已经帮我们内置了debug和release两个构建类型,可以分别设置不同包名等信息。

signingConfigs

签名配置,可以设置debug和release甚至自定义方式时的不同keystore,及其密码等信息。

ProductFlavor

多渠道打包必备,用处很多笔者也有推荐文章介绍

而最后一个BaseVariantOutput,“望文生义“不难才到就是输出文件咯~

2、把project、androidBuilder以及刚刚提到的几个类作为参数创建了extension,这里使用了策略模式,createExtension是一个抽象方法,真正实现是在AppPlugin

protected BaseExtension createExtension(//参数省略) {
    return project.getExtensions()
            .create();//参数省略
}

就如同我们之前介绍创建Extension的方式一样,通过project创建了名为“android”的Extension,类型为AppExtension,这个类就包含了我们平时用到的版本号、包名等等信息,为我们构建项目打下了基础。

3、用同样的方式创建了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);
        }
    }

1、解析variant渠道等信息
2、创建AssembleTask存入data里
3、给assemble添加依赖
4、创建该variant的专属AssembleTask
5、给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就是构建项目的基石,这里我就放出任玉刚大佬总结的主要构建方法:

常用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得多大。
其实主要就是这两个问题:
1、打包时资源文件无法自动更换
2、若手动更换,一个一个打成百上千的包那时间成本可不是盖的
那么今天,我们就来写一个自动替换资源文件的gradle插件,彻底解决这个问题。

首先我们需要写个自定义插件,具体步骤我在第一篇系列文章里提到过,也有推荐文章,这里我就放出插件结构就好(文末有DEMO,大家可以有需要的话可以查看)


自定义插件结构

首先我们需要写一个Plugin,并重写他的apply方法:

public class ResourceFlavorsPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        //这里写自定义内容
        
    }
}

首先我们理一下我们思路:
1、我们需要打很多渠道包
2、每个渠道包都需要修改包名、资源文件等
第一点我们利用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如何实现这一操作呢?我也不知道,这时候只能求助官方了,通过一些时间的查阅,我终于在官方文档中找到了这个方法,其实很简单:


copy

这是在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'
}

OK,大功告成!需要看Demo的童鞋请点击下面的传送们:
github地址
如果对您有帮助的话,希望给个star鼓励一下~谢谢

最最最后:我也把该项目上传到了jcenter,可以直接使用哦~具体参见github说明

Android进阶
Web note ad 1