gradle超詳細(xì)解析

*本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布

一、為什么要學(xué)gradle

Android studio已經(jīng)出來(lái)很久了,相信大部分公司都已經(jīng)從eclipse轉(zhuǎn)AS了,反正我都快忘記eclipse如何操作了。AS很多強(qiáng)大功能,其中很突出的一項(xiàng)就是gradle構(gòu)建。還記得第一次用依賴的時(shí)候,那感覺(jué)爽翻。但是因?yàn)閎uild代碼不熟悉,也遇到很多坑,經(jīng)常會(huì)莫名其妙報(bào)錯(cuò),當(dāng)時(shí)只能上網(wǎng)查,然后一板一眼的配置。作為程序猿這種不能完全掌握的感覺(jué)是最不爽的,很早就想徹底掌控它了。

其次,作為有夢(mèng)想的咸魚(yú),不能只做代碼的搬運(yùn)工,這種高階必備的知識(shí)點(diǎn)還是需要掌握的。比如國(guó)內(nèi)比較火熱的插件化、熱更新都會(huì)涉及到gradle插件知識(shí),所以想要進(jìn)階,必須掌握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"
//省略.....
}
}

相信這段代碼在座的各位都看吐了吧?

是的,筆者也一樣。剛開(kāi)始接觸AS的時(shí)候,看這段代碼感覺(jué)就感覺(jué)在看天書(shū),反正按規(guī)定配置就對(duì)了。今天我們就把他扒光了看看有什么特別。

仔細(xì)觀察其實(shí)也比較好理解,就是android的一些配置:

compileSdkVersion指的是當(dāng)前的androidSDK版本、applicationId指的是包名、versionCode指的是版本號(hào)....balabala...

雖然經(jīng)過(guò)幾年的百度、谷歌熏陶已經(jīng)很熟悉了,但是為什么這么配置呢?接下來(lái)就為各位看官解答。

我們可以把一個(gè)gradle項(xiàng)目看做一個(gè)Project,而這個(gè)Project就類(lèi)似JAVA的類(lèi),而一個(gè)類(lèi)自然就需要成員變量咯,所以我們首先就來(lái)創(chuàng)建一個(gè)pojo類(lèi),就命名為Person吧:

public class Person {

String name;

int age;

@Override
public String toString() {
    return "I am $name, $age years old"
}
}

然后就像上篇文章那樣創(chuàng)建一個(gè)自定義插件:

class MyPlugin implements Plugin<Project>{   
@Override   
void apply(Project project) {        
     project.extensions.add("person", Person)
     project.task('printPerson') {
        group 'atom'
        doLast{
           Person ext = project.person
           println ext 
      }
}  
}

調(diào)用了project的extensions,他可以視為變量的容器,只要往里加就OK了,然后就可以通過(guò)project調(diào)用到person了
很簡(jiǎn)單,我直接打印了該person,接下來(lái)就是重點(diǎn)了,我們需要在build.gradle里面配置person

person{
name  'atom'
age  18
}

是不是就像android的標(biāo)簽一樣?這下應(yīng)該很好理解gradle里為何這么寫(xiě)了吧。

其實(shí)這里就相當(dāng)于我們給person做了初始化,只是用有些像json的寫(xiě)法而已。

不過(guò)這樣我們就可以配置項(xiàng)目參數(shù)了,是不是很方便?

我們來(lái)驗(yàn)證一下,自行加上apply plugin: com.atom.MyPlugin,然后執(zhí)行一下gradle printPerson命令:

image

如我們所想,打印了我們配置的18歲的atom:)

三、task介紹

task,如其名:任務(wù),gradle就是由一個(gè)一個(gè)任務(wù)來(lái)完成的。他其實(shí)也是一個(gè)類(lèi),有自己的屬性,也可以"繼承",甚至他還有自己的生命周期。
他的定義方式有很多,下面我們來(lái)看一個(gè)最簡(jiǎn)單的實(shí)現(xiàn):

task myTask {
    println "myTask invoked!"
}

gradle就是一個(gè)一個(gè)task組成的,我們平時(shí)遇到莫名其妙的報(bào)錯(cuò),最常用的就是clean(斜眼笑),其實(shí)也是一個(gè)task而已,包括我們debug運(yùn)行、打包簽名等等等等,都是Android studio給我們視圖化了而已,本質(zhì)也是執(zhí)行task。所以我們執(zhí)行一下clean命令:gradle clean
myTask invoked!還是被打出來(lái)了,為啥?其實(shí)上面我也提到了,task有自己的生命周期。
初始化---配置期---執(zhí)行期,我從實(shí)戰(zhàn)gradle里偷了一張圖:


這里寫(xiě)圖片描述

其實(shí)上面代碼就是在配置階段而已,配置階段的代碼只要在執(zhí)行任何task都會(huì)跟著執(zhí)行,如果我們希望不被執(zhí)行的話,就只能放到執(zhí)行階段了,最直接的方法就是加到doLast、doFirst里,當(dāng)然實(shí)現(xiàn)方式也挺多的,我就列兩種吧:

project.task('printPerson') {
            group 'atom'
            //定義時(shí)
            doLast {
                println "this is doLast1"
            }
        }
Task printPerson= project.tasks["printPerson"]
//后來(lái)加
printPerson.doFirst {
        println "this is doFirst1"
   }
printPerson.doFirst {
        println "this is doFirst2"
   }
printPerson.doLast {
        println "this is doLast2"
   }

剛開(kāi)始可能不好理解這種方式,其實(shí)可以理解為task里有一個(gè)隊(duì)列,隊(duì)列中是task將會(huì)執(zhí)行的action,當(dāng)doFirst 時(shí),就會(huì)在隊(duì)列頭部插入一個(gè)action,而doLast則在隊(duì)列尾部添加,當(dāng)執(zhí)行該任務(wù)時(shí)就會(huì)從隊(duì)列中取出action依次執(zhí)行,就如同我們上述代碼,執(zhí)行g(shù)radle printPerson時(shí),打印結(jié)果如下:

> Task :app:printPerson 
this is doFirst2
this is doFirst1
this is doLast1
this is doLast2

注意,此時(shí)必須要執(zhí)行g(shù)radle printPerson時(shí)才會(huì)打印了,clean之流就沒(méi)用了。

剛剛提到過(guò),task其實(shí)也是一個(gè)類(lèi),沒(méi)錯(cuò),就如同object一樣,task的基類(lèi)是DefaultTask ,我們也可以自定義一個(gè)task,必須繼承DefaultTask,如下:

class MyTask extends DefaultTask {

    String message = 'This is MyTask'

    // @TaskAction 表示該Task要執(zhí)行的動(dòng)作,即在調(diào)用該Task時(shí),hello()方法將被執(zhí)行
    @TaskAction
    def hello(){
        println "Hello gradle. $message"
    }
}

其實(shí)task還有許多內(nèi)容,比如輸入輸出文件outputFile、Input
but,對(duì)于android開(kāi)發(fā)目前來(lái)說(shuō),這就夠了,但是了解一下也是很有好處的,比如我們構(gòu)建速度就和輸入輸出有關(guān),是不是被這個(gè)坑爹的構(gòu)建速度郁悶到很多次!我推薦大家去看看《Android+Gradle權(quán)威指南》之類(lèi)的書(shū),目前網(wǎng)上資料不全不夠系統(tǒng),當(dāng)然,官方文檔還是值得好好看的~
閑話少敘,繼續(xù)主題。task還有一個(gè)比較重要的內(nèi)容,就是“繼承”。
沒(méi)錯(cuò),task之間也是可以‘繼承’的,不過(guò)此繼承非彼繼承,而是通過(guò)dependsOn關(guān)鍵字實(shí)現(xiàn)的,我們先來(lái)看看實(shí)現(xiàn):

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')

‘繼承’關(guān)系為:task1-->task2/task4和task2-->task3
我們先打印出來(lái):


> 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是最先執(zhí)行的,這是因?yàn)閐ependsOn的邏輯就是首先執(zhí)行‘最高’輩分的,最后執(zhí)行‘最低’輩分的。什么意思呢,拿代碼來(lái)說(shuō)就是task1‘繼承’了task2,task4,而task2‘繼承’了task3,意思就是task3是task1的爺爺輩,所以最先執(zhí)行,這樣相信大家能夠理解了吧。

四、自定義gradle插件

gradle就是構(gòu)建工具,他使用的語(yǔ)言是groovy,我們可以在build.gradle里面寫(xiě)代碼來(lái)控制,當(dāng)然,如果代碼很多,希望單獨(dú)提取出來(lái),那么可以使用自定義gradle插件來(lái)實(shí)現(xiàn),沒(méi)錯(cuò),AndroidDSL(plugin)就是一個(gè)自定義插件而已,所以學(xué)習(xí)它之前需要了解如何自定義gradle插件。

首先,我們新建一個(gè)項(xiàng)目,會(huì)得到兩個(gè)build.gradle,一個(gè)是主項(xiàng)目的,一個(gè)是全局的。我們先只看項(xiàng)目里的build文件,其中自定義插件的重點(diǎn):

apply plugin: 'com.android.application'

這就表示我們引入了Android的插件了,下面來(lái)演示一下最簡(jiǎn)單的自定義插件步驟。

事實(shí)上所有的自定義插件都需要繼承一個(gè)plugin類(lèi),然后重寫(xiě)apply方法,如下:

apply plugin: com.atom.MyPlugin 

class MyPlugin implements Plugin<Project>{ 
@Override 
void apply(Project project) { 
    println "myPlugin invoked!" 
}
}

把上述代碼加到build.gradle下面,在命令行運(yùn)行隨意的命令:gradlew clean(windows)

image

調(diào)用成功了,當(dāng)然這是最簡(jiǎn)單的方式,不過(guò)理解這里就能繼續(xù)看AndroidDSL了,具體步驟可以自行谷歌

四、Android Plugin源碼解析

對(duì)于如何查看源碼,其實(shí)很簡(jiǎn)單,只需要把全局build.gradle里的classpath的依賴加入項(xiàng)目build.gradle文件的dependencies里就好了,如下圖:

image

這樣就能在項(xiàng)目的依賴樹(shù)里找到源碼了,可以選擇復(fù)制出來(lái)看,也可以直接在AS里看,個(gè)人感覺(jué)AS也挺方便的

image

打開(kāi)第一個(gè),就能看見(jiàn)很多plugin展現(xiàn)在我們眼前了,我們最熟悉的就是AppPlugin和LibraryPlugin了

前者就是主項(xiàng)目需要依賴的插件,后者就是組件化的module需要依賴的插件

我們拿最常用的AppPlugin來(lái)說(shuō)把,根據(jù)上面定義插件的步驟,我們就直接看apply方法,由于Appplugin繼承了basePlugin,所以又轉(zhuǎn)到basePlugin:

public void apply(@NonNull Project project) {
    //省略一些初始化及錯(cuò)誤檢查代碼

    //初始化線程信息記錄者
    threadRecorder = ThreadRecorder.get();
    //保存一些基礎(chǔ)信息
    ProcessProfileWriter.getProject(project.getPath())
            .setAndroidPluginVersion(Version.ANDROID_GRADLE_PLUGIN_VERSION)
            .setAndroidPlugin(getAnalyticsPluginType())
            .setPluginGeneration(GradleBuildProject.PluginGeneration.FIRST)
            .setOptions(AnalyticsUtil.toProto(projectOptions));

    BuildableArtifactImpl.Companion.disableResolution();
    //判斷是不是新的API,這里我們只看最新實(shí)現(xiàn),老的就不多說(shuō)了
    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 {
        //省略以前的實(shí)現(xiàn)
    }
}

其實(shí)最重要的實(shí)現(xiàn)在于調(diào)用了三次threadRecorder.record,值得一說(shuō)的是:this::configureProject這種寫(xiě)法

這是JAVA8里lambda語(yǔ)法,等于:()-> this.configureProject(),匿名內(nèi)部類(lèi)的簡(jiǎn)寫(xiě)方式,后面會(huì)回調(diào)這里。

J8已經(jīng)出來(lái)很久了,相信大家有了一定的了解,這里就不多說(shuō)。

我們就來(lái)看看這個(gè)record方法:

@Override
public void record(
        @NonNull ExecutionType executionType,
        @NonNull String projectPath,
        @Nullable String variant,
        @NonNull VoidBlock block) {
    //剛剛初始化過(guò)的單例
    ProfileRecordWriter profileRecordWriter = ProcessProfileWriter.get();
    //創(chuàng)建GradleBuildProfileSpan的建造者
    GradleBuildProfileSpan.Builder currentRecord =
            create(profileRecordWriter, executionType, null);
    try {
        //剛剛提到的回調(diào)
        block.call();
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    } finally {
        //寫(xiě)入GradleBuildProfileSpan并保存
        write(profileRecordWriter, currentRecord, projectPath, variant);
    }
}

以上代碼做了如下事情:

1、創(chuàng)建GradleBuildProfileSpan.Builder

2、回調(diào)方法

3、寫(xiě)入GradleBuildProfileSpan并保存到spans中

我們先不管回調(diào),看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;
}

代碼不少,但是做的事情很簡(jiǎn)單,就是創(chuàng)建了一個(gè)GradleBuildProfileSpan.Builder,并設(shè)置了它的threadId、Id、parentId...等等一系列線程相關(guān)的東西,并保存在一個(gè)雙向隊(duì)列里,并放入threadLocal里解決多線程并發(fā)問(wèn)題。這個(gè)threadLocal若不理解的可以移步我的另一篇文章:消息機(jī)制:Handler源碼解析

接下來(lái)是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);
}

調(diào)用了profileRecordWriter.writeRecord,繼續(xù):

 /** 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());
}

這里使用建造者模式創(chuàng)建了GradleBuildProfileSpan,并保存到了spans里。

關(guān)于1、3步驟說(shuō)了這么多,其實(shí)也就是做了這點(diǎn)事情,接下來(lái)才是重點(diǎn)了,關(guān)于回調(diào):

回頭看basePlugin里的3個(gè)回調(diào)方法configureProject、configureExtension、

createTasks,方法里傳的type已經(jīng)暴露了他們的作用:

1、BASE_PLUGIN_PROJECT_CONFIGURE:plugin的基礎(chǔ)設(shè)置、初始化工作

2、BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION:EXTENSION的初始化工作

3、BASE_PLUGIN_PROJECT_TASKS_CREATION:plugin的task創(chuàng)建

這三步基本囊括了自定義插件的所有內(nèi)容,我這里簡(jiǎn)單先介紹一下第一步,后面再詳細(xì)解析很重要的后面兩步

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(...)
    //省略監(jiān)聽(tīng)代碼...

}

這個(gè)方法主要做了以下幾件事情:

1、利用project,初始化了sdkHandler、androidBuilder、dataBindingBuilder等幾個(gè)必備的對(duì)象。

2、依賴了JavaBasePlugin,這個(gè)很重要,是JAVA構(gòu)建項(xiàng)目需要的插件。

3、對(duì)gradle創(chuàng)建做了監(jiān)聽(tīng),做了內(nèi)存、磁盤(pán)緩存的工作,你可以在build\intermediates\dex-cache\cache.xml文件下找到JAR包等內(nèi)容的緩存。

接下來(lái)讓我們看看第二步configureExtension方法,由于篇幅原因省略了許多參數(shù)信息但不影響閱讀:

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、首先創(chuàng)建了四個(gè)NamedDomainObjectContainer,是由project的Container方法返回的,這些東東是什么有什么用,光靠百度谷歌基本上是很難找到了(筆者寫(xiě)這篇文章的時(shí)候gradle方面的資料還是相當(dāng)匱乏的),所以我們得學(xué)會(huì)看官方文檔咯~

image

也不算太難,筆者這二流英語(yǔ)水平都能看懂:創(chuàng)建一個(gè)容器,用來(lái)管理泛型中定義的類(lèi)。而factory自然就是創(chuàng)建該類(lèi)的工廠了。

所以根據(jù)上訴代碼,不難知道創(chuàng)建了BuildType、ProductFlavor、SigningConfig、BaseVariantOutput這四個(gè)類(lèi)的容器了。

稍微熟悉構(gòu)建的童鞋應(yīng)該很清楚這幾個(gè)類(lèi)的用處:

buildType

構(gòu)建類(lèi)型,在Android Gradle工程中,它已經(jīng)幫我們內(nèi)置了debug和release兩個(gè)構(gòu)建類(lèi)型,可以分別設(shè)置不同包名等信息。

signingConfigs

簽名配置,可以設(shè)置debug和release甚至自定義方式時(shí)的不同keystore,及其密碼等信息。

ProductFlavor

多渠道打包必備,用處很多筆者也有推薦文章介紹

而最后一個(gè)BaseVariantOutput,“望文生義“不難才到就是輸出文件咯~

2、把project、androidBuilder以及剛剛提到的幾個(gè)類(lèi)作為參數(shù)創(chuàng)建了extension,這里使用了策略模式,createExtension是一個(gè)抽象方法,真正實(shí)現(xiàn)是在AppPlugin

protected BaseExtension createExtension(//參數(shù)省略) {
    return project.getExtensions()
            .create();//參數(shù)省略
}

就如同我們之前介紹創(chuàng)建Extension的方式一樣,通過(guò)project創(chuàng)建了名為“android”的Extension,類(lèi)型為AppExtension,這個(gè)類(lèi)就包含了我們平時(shí)用到的版本號(hào)、包名等等信息,為我們構(gòu)建項(xiàng)目打下了基礎(chǔ)。

3、用同樣的方式創(chuàng)建了variantFactory、taskManager、variantManager,最后設(shè)置了默認(rèn)的構(gòu)建信息。

關(guān)于這幾個(gè)類(lèi)的作用分別是:

variantFactory構(gòu)建信息的工廠、taskManager構(gòu)建任務(wù)、variantManager各種不同構(gòu)建方式及多渠道構(gòu)建的管理

這就涉及到gradle核心:task了

在繼續(xù)講解之前,我先講解一下assemble,assemble是一個(gè)task,用于構(gòu)建、打包項(xiàng)目,平時(shí)我們打包簽名APK就是調(diào)用了該方法,由于我們有不同buildTypes,以及不同productFlavors,所以我們還需要生成各種不同的assemble系列方法:assemble{productFlavor}{BuildVariant},比如
assembleRelease:打所有的渠道Release包
assemblexiaomiRelease:打小米R(shí)elease包
assemblehuaweiRelease:打華為Release包
AndroidDSL負(fù)責(zé)生成我們?cè)赽uild.gradle里配置的多渠道等各種assemble系列方法。
然后assemble方法會(huì)依賴很多方法,就如同我們上文所敘述的,依次執(zhí)行assemble依賴的方法完成構(gòu)建,好了,我們還是來(lái)看源碼理解吧!

第三步就是Android的創(chuàng)建task部分,該方法其實(shí)就是調(diào)用了createTasksBeforeEvaluate和createAndroidTasks兩個(gè)方法,其中createAndroidTasks才是重點(diǎn),該方法中又調(diào)用了variantManager的createAndroidTasks方法,跳過(guò)與本文無(wú)關(guān)的細(xì)節(jié),看下面重要的地方:

    /**
     * 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));
        }

    }

循環(huán)調(diào)用createTasksForVariantData方法,該方法就是為所有的渠道創(chuàng)建相關(guān)方法了,而variantScopes則存放了各種渠道、buildType信息,繼續(xù)查看該方法:

    /** 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()) {
            //省略測(cè)試相關(guān)代碼...
        } else {
            //5======
            taskManager.createTasksForVariantScope(variantScope);
        }
    }

1、解析variant渠道等信息
2、創(chuàng)建AssembleTask存入data里
3、給assemble添加依賴
4、創(chuàng)建該variant的專(zhuān)屬AssembleTask
5、給AssembleTask添加構(gòu)建項(xiàng)目所需task依賴(dependsOn)

看一下4、5步驟詳細(xì)代碼,首先是第四步,給每個(gè)渠道和buildtype創(chuàng)建對(duì)應(yīng)的方法:

    /** Create assemble task for VariantData. */
    private void createAssembleTaskForVariantData(final BaseVariantData variantData) {
        final VariantScope variantScope = variantData.getScope();
        if (variantData.getType().isForTesting()) {
            //測(cè)試
        } else {
            BuildTypeData buildTypeData =
                    buildTypes.get(variantData.getVariantConfiguration().getBuildType().getName());

            Preconditions.checkNotNull(buildTypeData.getAssembleTask());

            if (productFlavors.isEmpty()) {
                //如果沒(méi)有設(shè)置渠道
            } 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)) {
                        //創(chuàng)建相應(yīng)渠道方法
                        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));
                }
            }
        }
    }

注釋已經(jīng)很清晰了,最重要的就是組裝名字,創(chuàng)建相應(yīng)的渠道打包方法。這里我們又學(xué)到一種定義task的方式:TaskFactory.create
這是AndroidDSL自定義的類(lèi),他的實(shí)現(xiàn)類(lèi)是TaskFactoryImpl,由kotlin語(yǔ)言實(shí)現(xiàn):

class TaskFactoryImpl(private val taskContainer: TaskContainer): TaskFactory {

    //省略大部分方法....
    override fun configure(name: String, configAction: Action<in Task>) {
        val task = taskContainer.getByName(name)
        configAction.execute(task)
    }

}

省略了大部分方法,但也很簡(jiǎn)單了,使用代理模式代理了taskContainer,而這個(gè)taskContainer就是gradle的類(lèi)了,查看官方文檔:

<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.
//....

就是創(chuàng)建一個(gè)task并放入容器里
參數(shù)只有第三個(gè)比較難猜一點(diǎn)點(diǎn),看了文檔也就很清楚:給task設(shè)置一個(gè)action而已。當(dāng)然,這里并沒(méi)有調(diào)用這個(gè)重載方法,不過(guò)我這里是為了第5步介紹,好的,讓我們回到第5步操作:

taskManager.createTasksForVariantScope(variantScope);

這里taskManager由BasePlugin的子類(lèi)實(shí)現(xiàn),實(shí)現(xiàn)類(lèi)為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
        //創(chuàng)建資源文件相關(guān)
        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
        //創(chuàng)建資源文件相關(guān)
        recorder.record(
                ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_RESOURCES_TASK,
                project.getPath(),
                variantScope.getFullVariantName(),
                (Recorder.VoidBlock) () -> createMergeResourcesTask(variantScope, true));

                //省略類(lèi)似方法
}

這個(gè)方法就是構(gòu)建精髓所在,他創(chuàng)建了我們構(gòu)建項(xiàng)目所需要的大部分task,比如創(chuàng)建manifest文件,合并manifest文件,處理resource文件...等等task,這些task就是構(gòu)建項(xiàng)目的基石,這里我就放出任玉剛大佬總結(jié)的主要構(gòu)建方法:

常用Task

具體每個(gè)方法做了什么,就是需要大家閱讀源碼參透了,這里我只負(fù)責(zé)梳理大致流程,嘿嘿...
下面我們就看看創(chuàng)建的第一個(gè)方法createAnchorTasks,在這個(gè)方法里面調(diào)用了createCompileAnchorTask,他的實(shí)現(xiàn)是:

    private void createCompileAnchorTask(@NonNull final VariantScope scope) {
        final BaseVariantData variantData = scope.getVariantData();
        //....
        scope.getAssembleTask().dependsOn(scope.getCompileTask());
    }

為什么我要專(zhuān)門(mén)說(shuō)一下這個(gè)task,就是因?yàn)樽詈笠痪浯a,AssembleTask依賴的該task,也就是說(shuō)當(dāng)我們執(zhí)行AssembleTask的時(shí)候,該task會(huì)提前執(zhí)行,而構(gòu)建原理也在于此,該task也會(huì)依賴其他task,就這樣一層層依賴,構(gòu)建時(shí)就會(huì)調(diào)用所有的相關(guān)task,這樣就完成了我們Android項(xiàng)目的構(gòu)建。

五、自定義插件實(shí)戰(zhàn)

不知道大家有沒(méi)有遇到過(guò)這樣的需求:公司有一款產(chǎn)品,而客戶需要將公司產(chǎn)品做制定化操作,如:修改app包名、appIcon、appName、以及引導(dǎo)頁(yè)等一些資源文件,以便客戶展示他自己的廣告。這樣可能會(huì)有很多定制化產(chǎn)品需要去打包,以前采用一個(gè)一個(gè)手動(dòng)更改并打包,一打就是一下午,隨著定制化越來(lái)越多,每次更新都要這樣打,估計(jì)都快瘋了吧。
這個(gè)時(shí)候大家首先會(huì)想到利用Android系統(tǒng)的多渠道打包方法productFlavors,比如這樣:

android  {
    productFlavors {
        xiaomi{
        applicationId  "com.xiaomi.cn"
        }
        google{
        applicationId  "com.google.cn"
        }
        huawei{
        applicationId  "com.huawei.cn"
        }
    }
}

這樣就可以修改包名,appName等一些需求,但是資源文件可能就不太方便了,可能有童鞋會(huì)說(shuō),在res下面多放幾張圖片,然后利用manifestPlaceholders來(lái)修改,沒(méi)錯(cuò),這樣的方式也可以實(shí)現(xiàn),不過(guò)萬(wàn)一你公司的渠道定制包很很多呢?100個(gè)、500個(gè),難道需要放那么多張沒(méi)用的圖片進(jìn)去?那app得多大。
其實(shí)主要就是這兩個(gè)問(wèn)題:
1、打包時(shí)資源文件無(wú)法自動(dòng)更換
2、若手動(dòng)更換,一個(gè)一個(gè)打成百上千的包那時(shí)間成本可不是蓋的
那么今天,我們就來(lái)寫(xiě)一個(gè)自動(dòng)替換資源文件的gradle插件,徹底解決這個(gè)問(wèn)題。

首先我們需要寫(xiě)個(gè)自定義插件,具體步驟我在第一篇系列文章里提到過(guò),也有推薦文章,這里我就放出插件結(jié)構(gòu)就好(文末有DEMO,大家可以有需要的話可以查看)


自定義插件結(jié)構(gòu)

首先我們需要寫(xiě)一個(gè)Plugin,并重寫(xiě)他的apply方法:

public class ResourceFlavorsPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        //這里寫(xiě)自定義內(nèi)容
        
    }
}

首先我們理一下我們思路:
1、我們需要打很多渠道包
2、每個(gè)渠道包都需要修改包名、資源文件等
第一點(diǎn)我們利用AndroidDSL來(lái)實(shí)現(xiàn):

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")
        }
        //其他渠道省略....
    }
}

這個(gè)很簡(jiǎn)單就不多說(shuō)了,下面就到我們需要做的事情:修改資源。
我們需要在打每個(gè)渠道包之前把對(duì)應(yīng)資源文件修改成相應(yīng)的,而我們每次打包都需要運(yùn)行assemble系列方法,比如
打所有發(fā)布版的包:assembleRelease
打define1的發(fā)布版的包:assembleDefine1Release
以此類(lèi)推,而根據(jù)上一篇文章我們又知道assemble依賴了許多task,這樣的話我們就好辦了,只需要在執(zhí)行資源合并task之前就修改資源文件就好了。
查看源碼發(fā)現(xiàn)preBuild這個(gè)task就是最先執(zhí)行的幾個(gè)task之一,所以我們需要獲取到該task,執(zhí)行他的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也就是渠道包的打包方式,然后做一些字符串拼接,就獲取到相應(yīng)task名稱(chēng),然后再?gòu)腜roject的taskContainer里取出就好。

下一步就需要修改資源文件了,而gradle如何實(shí)現(xiàn)這一操作呢?我也不知道,這時(shí)候只能求助官方了,通過(guò)一些時(shí)間的查閱,我終于在官方文檔中找到了這個(gè)方法,其實(shí)很簡(jiǎn)單:


copy

這是在project下的一個(gè)方法,官方文檔介紹的很詳細(xì)了,連demo都有,可以說(shuō)相當(dāng)良心了。
from和into后面分別跟源文件和被替換的文件就可以了,當(dāng)然,文件夾也行,所以我們的代碼就變成了這樣

project.android.applicationVariants.all { variant ->
                //...
                preBuild.doFirst {
                    project.copy {
                        from "../resourceDir/${variantFlavorName}"
                        into "../app/src/main/res"
                    }
                    println "${variantFlavorName} resource is changed!"
                }
            }

為了更好的擴(kuò)展性,能夠在build文件中設(shè)置源文件位置、名稱(chēng)等,我們可以用extension來(lái)操作,首先創(chuàng)建一個(gè)pojo類(lèi)

public class FlavorType {
    /**
     * 存放渠道包圖片的路徑
     */
    String resourceDir
    /**
     * 主項(xiàng)目名
     */
    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的童鞋請(qǐng)點(diǎn)擊下面的傳送們:
github地址
如果對(duì)您有幫助的話,希望給個(gè)star鼓勵(lì)一下~謝謝

最最最后:我也把該項(xiàng)目上傳到了jcenter,可以直接使用哦~具體參見(jiàn)github說(shuō)明

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容