*本篇文章已授權(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命令:
如我們所想,打印了我們配置的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里偷了一張圖:
其實(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)
調(diào)用成功了,當(dāng)然這是最簡(jiǎn)單的方式,不過(guò)理解這里就能繼續(xù)看AndroidDSL了,具體步驟可以自行谷歌
四、Android Plugin源碼解析
對(duì)于如何查看源碼,其實(shí)很簡(jiǎn)單,只需要把全局build.gradle里的classpath的依賴加入項(xiàng)目build.gradle文件的dependencies里就好了,如下圖:
這樣就能在項(xiàng)目的依賴樹(shù)里找到源碼了,可以選擇復(fù)制出來(lái)看,也可以直接在AS里看,個(gè)人感覺(jué)AS也挺方便的
打開(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ì)看官方文檔咯~
也不算太難,筆者這二流英語(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)建方法:
具體每個(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,大家可以有需要的話可以查看)
首先我們需要寫(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)單:
這是在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ō)明