一篇文章深刻gradle(上篇):依賴機制

Hello,各位朋友們,小笨鳥又和大家見面啦。不一樣於網上泛泛而談的入門文章只停留在「怎麼用」的層次,本篇文章從源碼角度去理解gradle。固然,若是你沒有看過個人前一篇文章《一篇文章基本看懂gradle》,仍是建議你先看一下,本篇文章的不少知識點會在上篇文章的基礎上展開。html

本篇文章會比較深刻,須要費一些腦筋和精力才能理解。建議跟着個人行文思路一步一步理解。本文的篇幅會比較長。閱讀大概須要花費半個小時。閱讀本文將會了解:java

  • gradle構建Lifecycle
  • gradle Extension和Plugin
  • gradle依賴實現,Configuration

其中gradle依賴將會是本文的重點。原本以前還想把artifact也講解到,可是發現篇幅已經很長了,因此就一篇文章拆成兩篇吧。android

gradle構建Lifecycle

Lifecycle的概念咱們在上一篇文章講Project的時候也講到過。在這篇文章中,咱們會再簡單講一講做爲引入。api

正如官網所說,gradle構建流程總共爲三個階段:bash

  • 初始化階段:在這個階段中settings.gradle是主角,gradle會把settings.gradle中的配置代理給Settings類。主要負責的是判斷哪些module須要參與到構建中,而後根據這些module的設置初始化他們的delegate對象Project。注意,Project對象是在初始化階段生成的。
  • 配置階段:配置階段的任務是執行各項目下的build.gradle,完成Project對象的配置,而且構造Task任務依賴關係圖TaskExectionGraph以便在執行階段按照依賴關係執行Task。
  • 執行階段:執行階段會根據你命令行輸入的Task,按照依賴關係經過調用gradle taskname進行執行。

值得一提的是,咱們能夠經過多種方式對project構建階段進行監控和處理。如閉包

// 如下爲Project的方法
afterEvaluate(closure),afterEvaluate(action)
beforeEvaluate(closure),beforeEvaluate(action)

// 如下爲gradle提供的生命週期回調
afterProject(closure),afterProject(action)
beforeProject(closure),beforeProject(action)
buildFinished(closure),buildFinished(action)
projectsEvaluated(closure),projectsEvaluated(action)
projectsLoaded(closure),projectsLoaded(action)
settingsEvaluated(closure),settingsEvaluated(action)
addBuildListener(buildListener)
addListener(listener)
addProjectEvaluationListener(listener)

// Task也有這種方法
afterTask​(Closure closure)
beforeTask(Closure closure)
//任務準備好後調用
whenReady(Closure closure)
複製代碼

那麼知道這樣的構建流程咱們能夠怎麼使用呢?咱們能夠進行監控,或者是動態的根據須要去控制project和task的構建執行。好比爲了加快編譯速度,咱們去掉一些測試的task。就能夠這樣寫app

gradle.taskGraph.whenReady {
        tasks.each { task ->
            if (task.name.contains("Test")) {
                task.enabled = false
            } else if (task.name == "mockableAndroidJar") {
                task.enabled = false
            }
        }
    }
複製代碼

Extension和Plugin

Extension和Plugin都是咱們平常開發中常常有講到的東西。上一篇文章中咱們講到了build.gradle中的閉包都是有一個Delegate代理的,這個代理對象能夠接受閉包中的參數傳遞給本身來使用,那麼這個代理是啥呢?其實就是咱們這裏要說的Extension。ide

1.Extension通俗來說其實就是一個普通的java bean。裏面存放一些參數。使用須要藉助於ExtensionContainer來進行建立。舉個栗子工具

// 先定義一個實體類
public class Message {
    String message = "empty"
    String greeter = "none"
}

// 接下來在gradle文件中去使用project.extensions(也就是ExtensionContainer)來進行建立。
def extension = project.extensions.create("cyMessage", Message)

// 再寫個task來進行驗證
project.task('sendMessage') {
    doLast {
        println "${extension.message} from ${extension.greeter}"
        println project.cyMessage.message + "from ${extension.greeter}"
    }
}
複製代碼

2.Plugin插件 講完了Extension,咱們可能就會疑惑了。由於咱們在build.gradle中並無看到這些javabean和extension添加的操做啊,那麼這些代碼是在哪裏寫的呢?這時候咱們就要引入Plugin的概念了,也就是你看到的源碼分析

apply plugin 'com.android.application'
複製代碼

這個玩意了。這個就是Android Application的插件。android相關的Extension都是定義在這個插件中的。 有些同窗可能對插件不是很理解,爲何須要這個東西。其實你們能夠思考一下,gradle只是一個通用的構建工具。在他上層可能有各類應用,好比java,好比Android,甚至多是將來的鴻蒙。那麼這些應用對gradle確定會有不一樣的擴展,又確定不能把這些擴展直接放在gradle中。因此在上層添加插件就是個好選擇,須要什麼擴展就選什麼插件。 Android開發中常見的插件有三種:

apply plugin 'com.android.application'   // AppPlugin,  主module才能引用
apply plugin 'com.android.library'    // LibraryPlugin, 普通的android module引用
apply plugin 'java'                           // java module的引用
複製代碼

插件的其它知識不是本篇文章的重點,網上這方面的文章不少,你們能夠自行學習。咱們根據如今掌握的知識就要去探索gradle更深層次的知識啦。

gradle依賴實現

這兩篇文章截止到如今,gradle構建相關的流程咱們基本都走通了。可是有個很重要的組件咱們沒有去分析。就是依賴管理。咱們前一篇文章也講到了,依賴管理是gradle的一個很重要的特性,方便咱們進行代碼複用。那麼咱們這一部分就專門講一講依賴管理究竟是怎麼實現的。

首先咱們仍是看看基本的代碼

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:26.1.0'
    api  'com.android.support:appcompat-v7:26.1.0'
    implementation project(':test')
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
}
複製代碼

咱們能夠看到,通常有三種三方庫類型,第一種是二進制文件,fileTree指向這個文件夾下的全部jar文件,第二種是三方庫的string座標形式,第三種是project的形式。 固然,咱們也會知道,代碼中的implementation和api兩種引用方式是有區別的。implement的依賴是不能傳遞的,可是api是能夠的。

咱們在以前的研發中,可能沒有去考慮深層次的這三種三方庫類型,兩種引用方式的緣由。當咱們按照上一篇中所說的代理模式去DependencyHandler中找implement, api等相關方法的時候,卻發現,好像並無定義這些方法,那麼這是怎麼回事呢?若是沒有定義,是否是能夠隨便定義一種引用方式呢?帶着問題咱們往下面看

1.源碼閱讀姿式

首先咱們仍是先介紹一下閱讀gradle源碼的方式。我嘗試了不少種方式,發現仍是Android studio閱讀起來最舒服,而後找到了一種方法。就是能夠建一個gradle的demo,而後建一個module,把除了build.gradle之外的東西所有刪掉。而後拷貝下面的代碼進去。這樣就能看到源碼了

apply plugin 'java'

dependencies {
     compile gradleApi()
     compile 'xxxx'  // 填入你gradle的版本
}
複製代碼

2.源碼分析

2.1 methodmissing

首先咱們先介紹一個groovy語言的特性:methodmissing。你們能夠參考官網 簡單來講就是當咱們預先在一個類中定義一個methodmissing方法。而後在這個類的對象上調用以前沒有定義過的方法時,這個方法就會降級(fallback)到它所定義的methodmissing方法上。

class GORM {

   def dynamicMethods = [...] // an array of dynamic methods that use regex

   def methodMissing(String name, args) {
       def method = dynamicMethods.find { it.match(name) }
       if(method) {
          GORM.metaClass."$name" = { Object[] varArgs ->
             method.invoke(delegate, name, varArgs)
          }
          return method.invoke(delegate,name, args)
       }
       else throw new MissingMethodException(name, delegate, args)
   }
}

assert new GORM().methodA(1) == resultA
複製代碼

如圖,當咱們調用methodA時,由於這個方法沒有定義,就會轉到methodmissing方法上,而且會把這個方法的名字methodA和它的參數一塊兒傳到methodmissing,這樣若是dynamicMethod裏面有定義methodA的話,這個方法就能執行了。這就是methodmissing的妙用。

爲何須要這種機制呢?我理解這仍是爲了擴展性。dependencies是gradle自身的功能。它不能徹底的總括全部上層應用可能會有的引用方式。每種插件均可能增長引用的方式,爲了擴展性考慮,必須採用這種methodmissing的特性,把這些引用交給插件處理。好比Android的implement, api, annotationProcessor等。

2.2 Configuration

在往下面講解前,咱們先了解一下Configuration的一些知識。
按照官網所說:

Every dependency declared for a Gradle project applies to a specific scope. For example some dependencies should be used for compiling source code whereas others only need to be available at runtime. Gradle represents the scope of a dependency with the help of a Configuration. Every configuration can be identified by a unique name.

也就是說,Configuration定義了依賴在編譯和運行時候的不一樣範圍, 每一個Configuration都有name來區分。好比android常見的兩種依賴方式implementation和api。

2.3 依賴的識別

gradle中使用MethodMixIn這個接口來實現methodmissing的能力。

// MethodMixIn
public interface MethodMixIn {
    MethodAccess getAdditionalMethods();
}
複製代碼
public interface MethodAccess {
    /** * Returns true when this object is known to have a method with the given name that accepts the given arguments. * * <p>Note that not every method is known. Some methods may require an attempt invoke it in order for them to be discovered.</p> */
    boolean hasMethod(String name, Object... arguments);

    /** * Invokes the method with the given name and arguments. */
    DynamicInvokeResult tryInvokeMethod(String name, Object... arguments);
}
複製代碼

能夠看到這裏的methodmissing主要是在找不到這個方法的時候去返回一個MethodAccess,MethodAccess中去判斷是否存在以及動態執行這個method。

接下來咱們看DependencyHandler的實現類DefaultDependencyHandler。這個類實現了MethodMixIn接口,返回的是一個DynamicAddDependencyMethods對象。

// DefaultDependencyHandler.java
public DefaultDependencyHandler(...) {
    ...
    this.dynamicMethods = new DynamicAddDependencyMethods(configurationContainer, new DefaultDependencyHandler.DirectDependencyAdder());
}

public MethodAccess getAdditionalMethods() {
    return this.dynamicMethods;
}
複製代碼

因此其實就是返回了一個DynamicAddDependencyMethods去加以判斷。那麼毫無疑問要在這個類中進行判斷和執行具體方法。接下來咱們看看這個類中是怎麼處理的。

DynamicAddDependencyMethods(ConfigurationContainer configurationContainer, DynamicAddDependencyMethods.DependencyAdder dependencyAdder) {
        this.configurationContainer = configurationContainer;
        this.dependencyAdder = dependencyAdder;
    }

    public boolean hasMethod(String name, Object... arguments) {
        return arguments.length != 0 && this.configurationContainer.findByName(name) != null;
    }

    public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {
        if (arguments.length == 0) {
            return DynamicInvokeResult.notFound();
        } else {
            Configuration configuration = (Configuration)this.configurationContainer.findByName(name);
            if (configuration == null) {
                return DynamicInvokeResult.notFound();
            } else {
                List<?> normalizedArgs = CollectionUtils.flattenCollections(arguments);
                if (normalizedArgs.size() == 2 && normalizedArgs.get(1) instanceof Closure) {
                    return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)normalizedArgs.get(1)));
                } else if (normalizedArgs.size() == 1) {
                    return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)null));
                } else {
                    Iterator var5 = normalizedArgs.iterator();

                    while(var5.hasNext()) {
                        Object arg = var5.next();
                        this.dependencyAdder.add(configuration, arg, (Closure)null);
                    }

                    return DynamicInvokeResult.found();
                }
            }
        }
    }
複製代碼

能夠看到這個類的兩個要點:
1.判斷Configuration有無:經過外部傳入的ConfigurationContainer來判斷是否存在這個方法。這樣咱們能夠聯想到,這個ConfigurationContainer確定是每一個平臺Plugin本身傳入的,必須是已定義的才能使用。好比android就添加了implementation, api等。若是你想查看Configuration在gradle源碼中的初始化和配置,能夠查看VariantDependencies這個類。

2.執行方法:真正的執行方法會根據參數來判斷,好比咱們常見的一個參數的引用形式,還有一個參數+一個閉包的形式,好比

compile('com.zhyea:ar4j:1.0') {
		exclude module: 'cglib' //by artifact name
}
複製代碼

這種類型的引用。當在ConfigurationContainer中找到了這個引用方式(如下都稱Configuration)時,就會返回一個DynamicInvokeResult。具體這個類的做用咱們後面再看,咱們先看他們都作了一個

this.dependencyAdder.add(configuration, arg, (Closure)null);
複製代碼

的操做,這個操做是作了些什麼呢,咱們繼續往下跟就會發現,其實仍是調用了DefaultDependencyHandler的doAdd方法。

// DefaultDependencyHandler.java
    private class DirectDependencyAdder implements DependencyAdder<Dependency> {
        private DirectDependencyAdder() {
        }

        public Dependency add(Configuration configuration, Object dependencyNotation, @Nullable Closure configureAction) {
            return DefaultDependencyHandler.this.doAdd(configuration, dependencyNotation, configureAction);
        }
    }

    private Dependency doAdd(Configuration configuration, Object dependencyNotation, Closure configureClosure) {
        if (dependencyNotation instanceof Configuration) {
            Configuration other = (Configuration)dependencyNotation;
            if (!this.configurationContainer.contains(other)) {
                throw new UnsupportedOperationException("Currently you can only declare dependencies on configurations from the same project.");
            } else {
                configuration.extendsFrom(new Configuration[]{other});
                return null;
            }
        } else {
            Dependency dependency = this.create(dependencyNotation, configureClosure);
            configuration.getDependencies().add(dependency);
            return dependency;
        }
    }
複製代碼

能夠看到,這裏會先判斷dependencyNotation是不是Configuration,若是存在的話,就讓當前的configuration繼承dependencyNotation,也就是全部添加到dependencyNotation的依賴都會添加到configuration中。

這裏可能有些朋友就會疑惑了,爲啥還要對dependencyNotation判斷呢?這個主要是爲了處理嵌套的狀況。好比implementation project(path: ':projectA', configuration: 'configA')這種類型的引用。有興趣能夠看看上面的CollectionUtils.flattenCollections(arguments)方法。

總結一下,這個過程就是藉助gradle的MethodMixIn接口,將全部未定義的引用方法轉到getAdditionalMethods方法上來,在這個方法裏面判斷Configuration是否存在,若是存在的話就生成Dependency。

2.4 依賴的建立

能夠看到上面過程的最後,是DefaultDependencyHandler調用了create方法建立出了一個Dependency。咱們繼續來分析建立Dependency的過程。

// DefaultDependencyHandler.java
    public Dependency create(Object dependencyNotation, Closure configureClosure) {
        Dependency dependency = this.dependencyFactory.createDependency(dependencyNotation);
        return (Dependency)ConfigureUtil.configure(configureClosure, dependency);
    }

    // DefaultDependencyFactory.java
    public Dependency createDependency(Object dependencyNotation) {
        Dependency dependency = (Dependency)this.dependencyNotationParser.parseNotation(dependencyNotation);
        this.injectServices(dependency);
        return dependency;
    }
複製代碼

能夠看到最終是調用了dependencyNotationParser來parse這個dependencyNotation。而這裏的dependencyNotationParser其實就是DependencyNotationParser這個類。

public class DependencyNotationParser {
    public static NotationParser<Object, Dependency> parser(Instantiator instantiator, DefaultProjectDependencyFactory dependencyFactory, ClassPathRegistry classPathRegistry, FileLookup fileLookup, RuntimeShadedJarFactory runtimeShadedJarFactory, CurrentGradleInstallation currentGradleInstallation, Interner<String> stringInterner) {
        return NotationParserBuilder.toType(Dependency.class)
        .fromCharSequence(new DependencyStringNotationConverter(instantiator, DefaultExternalModuleDependency.class, stringInterner))
        .converter(new DependencyMapNotationConverter(instantiator, DefaultExternalModuleDependency.class))
        .fromType(FileCollection.class, new DependencyFilesNotationConverter(instantiator))
        .fromType(Project.class, new DependencyProjectNotationConverter(dependencyFactory))
        .fromType(ClassPathNotation.class, new DependencyClassPathNotationConverter(instantiator, classPathRegistry, fileLookup.getFileResolver(), runtimeShadedJarFactory, currentGradleInstallation))
        .invalidNotationMessage("Comprehensive documentation on dependency notations is available in DSL reference for DependencyHandler type.").toComposite();
    }
}
複製代碼

從裏面咱們看到了FileCollection,Project,ClassPathNotation三個類,是否是感受和咱們的三種三方庫資源形式很對應?其實這三種資源形式的解析就是用這三個類進行的。DependencyNotationParser就是整合了這些轉換器,成爲一個綜合的轉換器。其中,

  • DependencyFilesNotationConverter將FileCollection解析爲SelfResolvingDependency,也就是implementation fileTree(include: ['*.jar'], dir: 'libs')這種形式。
  • DependencyProjectNotationConverter將Project解析爲ProjectDependency。也就是implementation project(‘:projectA’)
  • DependencyClassPathNotationConverter將ClassPathNotation轉成SelfResolvingDependency。也就是implementation ‘xxx’這種。

這三種方式具體的解析方法你們能夠自行閱讀源碼,不是本文重點。因此除了Project會被解析爲ProjectDependency之外,其餘的都是SelfResolvingDependency。其實ProjectDependency是SelfResolvingDependency的子類。他們的關係能夠從SelfResolvingDependency的代碼註釋中看出。

2.5 ProjectDependency

接下來說講ProjectDependency.一個常見的Project引用以下:

implementation project(‘:projectA’)
複製代碼

這裏的implementation咱們已經知道是插件添加的擴展,不是gradle自帶的。那project呢?這個就是gradle自帶的了。delegate是DependencyHandler的project方法。

// DefaultDependencyHandler.java
    public Dependency project(Map<String, ?> notation) {
        return this.dependencyFactory.createProjectDependencyFromMap(this.projectFinder, notation);
    }

    // DefaultDependencyFactory.java
    public ProjectDependency createProjectDependencyFromMap(ProjectFinder projectFinder, Map<? extends String, ? extends Object> map) {
        return this.projectDependencyFactory.createFromMap(projectFinder, map);
    }

    // ProjectDependencyFactory.java
    public ProjectDependency createFromMap(ProjectFinder projectFinder, Map<? extends String, ?> map) {
        return (ProjectDependency)NotationParserBuilder.toType(ProjectDependency.class).converter(new ProjectDependencyFactory.ProjectDependencyMapNotationConverter(projectFinder, this.factory)).toComposite().parseNotation(map);
    }

    // ProjectDependencyMapNotationConverter.java
    protected ProjectDependency parseMap(@MapKey("path") String path, @Optional @MapKey("configuration") String configuration) {
            return this.factory.create(this.projectFinder.getProject(path), configuration);
    }

    // DefaultProjectDependencyFactory.java
    public ProjectDependency create(ProjectInternal project, String configuration) {
        DefaultProjectDependency projectDependency = (DefaultProjectDependency)this.instantiator.newInstance(DefaultProjectDependency.class, new Object[]{project, configuration, this.projectAccessListener, this.buildProjectDependencies});
        projectDependency.setAttributesFactory(this.attributesFactory);
        projectDependency.setCapabilityNotationParser(this.capabilityNotationParser);
        return projectDependency;
    }
複製代碼

咱們能夠看到,傳入的project最終傳遞給了ProjectDependencyMapNotationConverter。先去查找這個project,而後經過factory去create ProjectDependency對象,固然這裏也有考慮到Configuration的影響, 最終是產生了一個DefaultProjectDependency。這就是ProjectDependency的產生過程。

2.6 依賴的體現

看到這裏,你們能夠已經理解了不一樣的依賴的解析方式,可是可能仍是不理解依賴究竟是一個什麼東西。其實依賴庫並非依賴三方庫的源代碼,而是依賴三方庫的產物,產物又是經過一系列的Task執行產生的。也就是說,projectA依賴projectB,那麼A就擁有了對於B的產物的全部權。關於產物咱們等後面再介紹。先了解一下Configuration對於產物有一些什麼支持。 咱們看Configuration的代碼, 能夠發現他繼承了FileCollection接口,而FileCollection又繼承了Buildable接口。這個接口有啥用呢,用處大得很。先看官網介紹

Buildable表示着不少Task對象生成的產物。它裏面只有一個方法。

public interface Buildable {
    TaskDependency getBuildDependencies();
}
複製代碼

咱們看看DefaultProjectDependency的實現。

// DefaultProjectDependency.java
    public TaskDependencyInternal getBuildDependencies() {
        return new DefaultProjectDependency.TaskDependencyImpl();
    }

    private class TaskDependencyImpl extends AbstractTaskDependency {
        private TaskDependencyImpl() {
        }

        public void visitDependencies(TaskDependencyResolveContext context) {
            if (DefaultProjectDependency.this.buildProjectDependencies) {
                DefaultProjectDependency.this.projectAccessListener.beforeResolvingProjectDependency(DefaultProjectDependency.this.dependencyProject);
                Configuration configuration = DefaultProjectDependency.this.findProjectConfiguration();
                context.add(configuration);
                context.add(configuration.getAllArtifacts());
            }
        }
    }

    public Configuration findProjectConfiguration() {
        ConfigurationContainer dependencyConfigurations = this.getDependencyProject().getConfigurations();
        String declaredConfiguration = this.getTargetConfiguration();
        Configuration selectedConfiguration = dependencyConfigurations.getByName((String)GUtil.elvis(declaredConfiguration, "default"));
        if (!selectedConfiguration.isCanBeConsumed()) {
            throw new ConfigurationNotConsumableException(this.dependencyProject.getDisplayName(), selectedConfiguration.getName());
        } else {
            return selectedConfiguration;
        }
    }
複製代碼

咱們能夠看到,其實就是在解析每一個依賴的時候,若是指定了ConfigurationContainer中聲明好的Configuration,好比implementation, api等,那就返回這個Configuration,不然就返回default。拿到這個Configuration以後,作了這個操做

context.add(configuration);
context.add(configuration.getAllArtifacts());
複製代碼

這裏的context是一個TaskDependencyResolveContext,它的add方法能夠添加能contribute tasks to the result的對象,好比Task,TaskDependencies,Buildable等,這些類型都能爲產生結果貢獻Task(前面咱們說到了就是靠Task產生產物的嘛)。

這裏context把configuration和configuration.getAllArtifacts()加入,都是做爲Buildable而加入。區別是configuration.getAllArtifacts()獲取的是DefaultPublishArtifactSet對象。接下來看看DefaultPublishArtifactSet是怎麼實現Buildable的方法的。

public TaskDependency getBuildDependencies() {
        return this.builtBy;   // 這裏的builtBy是下面的ArtifactsTaskDependency對象
    }

    private class ArtifactsTaskDependency extends AbstractTaskDependency {
        private ArtifactsTaskDependency() {
        }

        public void visitDependencies(TaskDependencyResolveContext context) {
            Iterator var2 = DefaultPublishArtifactSet.this.iterator();
            
            while(var2.hasNext()) {
                PublishArtifact publishArtifact = (PublishArtifact)var2.next();
                context.add(publishArtifact);
            }

        }
    }

複製代碼

咱們發現果真和DefaultPublishArtifactSet的名字同樣,是做爲set把裏面包含的PublishArtifact對象逐個的放入context中。

2.7 總結

咱們在這一小節中分析了不一樣的依賴方式的區別,告訴了你們Configuration是什麼東西,也告訴了你們依賴究竟是怎麼產生和起做用的。這樣你們在平常開發中就更能知其因此然了。總結一下就是說

  • implementation等都是經過methodmissing機制,被插件解析成不一樣的Configuration,因此要預約義。
  • 咱們不一樣的依賴聲明,將會被不一樣的轉化器Parser進行轉換。project依賴會被轉換爲ProjectDependency,其他的會被解析成可自解析的SelfResolvingDependencies。
  • project依賴最終是Task和產物artifact的依賴。

那麼Task和產物又是一種什麼關係呢?這個就涉及到更深層次了。本文篇幅有限,放在下一篇再分析吧。

總結

本文主要是從Extension和Plugin引入,主要講解了gradle依賴的原理和解析機制,而後拋下了一個疑問:產物artifacts是怎麼和依賴扯上關係的呢?這個問題咱們等下一篇《一篇文章深刻gradle(下篇):Artifacts》再解答

參考

gradle官網

我是Android笨鳥之旅,一個陪着你慢慢變強的公衆號,歡迎關注我一塊兒學習,一塊兒進步哈~

相關文章
相關標籤/搜索