深入理解gradle框架之一:Plugin,Extension,buildSrc

寫在前面

這篇文章首發在位元組跳動

技術團隊公眾號上,可能有的童鞋已經看過了,不過考慮到可能有的童鞋沒關注那個公眾號,所以這裡再發一遍。

另外,最近創建了一個知識星球,名字跟專欄名字一樣,也叫AndroidGeek, 主要是分享gradle框架知識,以及我在愛奇藝和位元組跳動做插件化和熱修復的實踐經驗,大家日常會遇到的編譯問題,以及移動端開發的職業規劃,歡迎感興趣的童鞋加入。日常開發遇到的任何問題,以及職業規劃等,都可以向我提問。

緣起

從2018年下半年開始,因為工作需要,開始深入了解android gradle plugin和gradle框架,在看完android gradle plugin 3.1.x和3.2.x版本的源碼之後,發現目前開源的幾乎所有插件化框架,因為沒有理解android gradle plugin的原理,打包代碼的實現都非常混亂,導致的結果就是很難隨著android gradle plugin的升級而快速升級,所以目前幾乎所有開源的插件化項目都因不適應新的gradle版本問題而不可用。

另一方面,隨著項目的迭代,引入越來越多的gradle plugin, 其中對於Transform的濫用是導致項目編譯速度越來越慢的最重要的一個原因了,然而,實際上,對於Transform的使用是有很大的優化空間的。

加上目前不管是中文還是英文,幾乎所有這方面的文章都停留在基礎使用的階段,真正深入分析原理的幾乎沒有。

所以一直在醞釀寫一個gradle系列的文章,一方面讓大家了解android gradle plugin的原理(儘管各個大版本之間有差別,然而大版本內基本是一脈相承), 另一方面是在介紹原理的過程中,也會加入一些我覺得是Best Practice的Demo. 或許通過這個系列,能夠引導大家都去改進自己實現的Plugin, 最終能夠更快更好地實現自己的編譯時功能。

p.s:因為基礎使用的文章已經有太多了,對於這一塊我基本上就是一筆帶過,不會花太多筆墨。

系列說明

在講解整個系列之前,先看一下gradle的架構是怎樣的,如下圖所示:

  • 在最下層的是底層gradle框架,它主要提供一些基礎服務,如task的依賴,有向無環圖的構建等
  • 上面的則是google編譯工具團隊的android gradle plugin框架,它主要是在gradle框架的基礎上,創建了很多與android項目打包有關的task及artifacts
  • 最上面的則是開發者自定義的Plugin, 一般是在android gradle plugin提供的task的基礎上,插入一些自定義的task, 或者是增加Transform進行編譯時代碼注入

為了簡單起見,我將底層的gradle框架和android gradle plugin框架統稱為gradle框架,整個系列文章其實分析的就是底層gradle框架和android gradle plugin框架的原理,其中側重點在andorid gradle plugin框架,因為這與我們日常編譯息息相關,也是收益最大的部分。

這是深入理解Gradle框架系列的第一篇。整個系列共分為9篇,文章列表如下:

  • 第1篇是入門文章,主要講解Gradle Plugin以及Extension的多種用法,以及buildSrc及gradle插件調試的方法。
  • 第2篇是從dependencies出發,闡述DependencyHandler的原理
  • 第3篇則是關於gradle configuration的預備知識介紹,以及artifacts的發布流程
  • 第4篇則是artifacts的獲取流程
  • 第5篇是從TaskManager出發,分析如ApplicationTaskManager, LibraryTaskManager中各主要的Task,最後給出當前版本的編譯流程圖
  • 第6篇比較3.2.1相比3.1.2中架構的變化
  • 第7篇關於Gradle Transform
  • 第8篇從打包的角度講解app bundles的原理
  • 第9篇分析資源編譯的流程,特別是aapt2的編譯流程

Plugin

語言選擇

其實只要是JVM語言,都可以用來寫插件, 比如Android Gradle Plugin團隊,在3.2.0之前一直是用java編寫gradle插件。

國內很多開源項目都是用groovy編寫的,groovy的優勢是書寫方便,而且其閉包寫法非常靈活,然而groovy的缺點也非常明顯,最大的一點不好就是IDE對其的支持非常不好,不僅僅是語法高亮沒做好,還有導航跳轉都極為有限,比如build.gradle中的方法跳轉不到其定義處。

當然,我自己長期使用groovy下來,也發現了它的一些缺點,比如each這個閉包,在運行時竟然會出現找不到其成員的情況。

以及出現開發者自定義的成員與其默認成員(groovy中會為每個類增加一些默認成員)名稱重合時,不能給出有效的提示,當然,這個問題我不確定是IDE的問題還是groovy自身的編譯器實現不夠完善的問題。

其實到目前為止,使用kotlin進行插件開發是最好的選擇,有如下兩個原因:

  • kotlin的語法糖完全不輸於groovy, 可以有效提高開發效率
  • kotlin是jetbrain自家的,IDE對其的支持更完善

可能正是這個原因,Google編譯工具組從3.2.0開始,新增的插件全部都是用kotlin編寫的。

插件名與Plugin的關係

比如我們常用的apply plugin: com.android.application, 其實是對應的AppPlugin, 其聲明在源碼的META-INF中,如下圖所示:

可以看到,不僅僅有com.android.appliation, 還有我們經常用到的com.android.library,以及com.android.feature, com.android.dynamic-feature.

以com.android.application.properties為例,其內容如下:

implementation-class=com.android.build.gradle.AppPlugin

其含義很清楚了,就表示com.android.application對應的插件實現類是com.android.build.gradle.AppPlugin這個類。

其他的類似,就不一一列舉了。

定義插件的方法

要定義一個gradle plugin,則要實現Plugin介面,該介面如下:

public interface Plugin<T>{
void apply(T var)
}

以我們經常用的AppPlugin和LibraryPlugin, 其繼承關係如下:

注意,這是3.2.0之前的繼承關係,在3.2.0之後,略微有些調整。

可以看到,LibraryPlugin和AppPlugin都繼承自BasePlugin, 而BasePlugin實現了Plugin介面,如下:

public abstract class BasePlugin<E extends BaseExtension2>
implements Plugin<Project>, ToolingRegistryProvider {

@VisibleForTesting
public static final GradleVersion GRADLE_MIN_VERSION =
GradleVersion.parse(SdkConstants.GRADLE_MINIMUM_VERSION);

private BaseExtension extension;

private VariantManager variantManager;

...
}

這裡繼承的層級多一層的原因是,有很多共同的邏輯可以抽出來放到BasePlugin中,然而大多數時候,我們可能沒有這麼複雜的關係,所以直接實現Plugin這個介面即可。

Extension

Extension其實可以理解成java中的java bean, 它的作用也是類似的,即獲取輸入數據,然後在插件中使用。

最簡單的Extension為例, 比如我定義一個名為Student的Extension,其定義如下:

class Student{
String name
int age
boolean isMale
}

然後在Plugin的apply()方法中,添加這個Extension, 不然編譯時會出現找不到的情形:

project.extensions.create("student",Student.class)

這樣,我們就可以在build.gradle中使用名為student的Extension了,如下:

student{
name Mike
age 18
isMale true
}

注意,這個名稱要與創建Extension時的名稱一致。

而獲取它的方式也很簡單:

Student studen = project.extensions.getByType(Student.class)

嵌套的Extension類似,不再贅述。

如果Extension中要包含固定數量的配置項,那很簡單, 類似下面這樣就可以:

class Fruit{
int count
Fruit(Project project){
project.extensions.create("apple",Apple,"apple")
project.extension.create("banana",Banana,"banana")
}
}

其配置如下:

fruit{
count 3
apple{
name Big Apple
weight 580f
}

banana{
name Yellow Banana
size 19f
}
}

下面要說的是包含不定數量的配置項的Extension, 就需要用到NamedDomainObjectContainer, 比如我們常用的編譯配置中的productFlavors,就是一個典型的包含不定數量的配置項的Extension. 但是,如果我們不進行特殊處理,而是直接使用NamedDomainObjectContainer的話,就會發現這個配置項都要用=賦值,類似下面這樣。

接著使用Student, 如果我需要在某個配置項中添加不定項個Student輸入,其添加方式如下:

NamedDomainObjectContainer<Student>studentContainer = project.container(Student)
project.extensions.add(team,studentContainer)

然而,此時其配置只能如下:

team{
John{
age=18
isMale=true
}
Daisy{
age=17
isMale=false
}
}

注意,這裡不需要name了,因為John和Daisy就是name了。

可是,這不科學呀,groovy的語法不是可以省略么?就比如productFlavors這樣:

要達到這樣的效果其實並不難,只要做好以下兩點:

  • item Extension的定義中必須有name這個屬性,因為在Factory中會在創建時為這個名稱的屬性賦值。定義如下:

```groovy class Cat{ String name

String from
float weight

} ```

  • 需要定義一個實現了NamedDomainObjectFactory介面的類,這個類的構造方法中必須有instantiator這個參數,如下:

```groovy class CatExtFactory implements NamedDomainObjectFactory{ private Instantiator instantiator

CatExtFactory(Instantiator instantiator){
this.instantiator=instantiator
}

@Override
Cat create(String name){
return instantiator.newInstance(Cat.class, name)
}

} ```

此時,gradle配置文件中就可以類似這樣寫了:

animal{
count 58

dog{
from America
isMale false
}

catConfig{
chinaCat{
from China
weight 2900.8f
}

birman{
from Burma
weight 5600.51f
}

shangHaiCat{
from Shanghai
weight 3900.56f
}

beijingCat{
from Beijing
weight 4500.09f
}
}
}

Plugin Transform

Transform是android gradle plugin團隊提供給開發者使用的一個抽象類,它的作用是提供介面讓開發者可以在源文件編譯成為class文件之後,dex之前進行位元組碼層面的修改。

藉助javaassist, ASM這樣的位元組碼處理工具,可在自定義的Transform中進行代碼的插入,修改,替換,甚至是新建類與方法。

像美團點評的Robust,以及我開源的Andromeda項目中,都有在Transform中插入代碼的示例。

如下是一個自定義Transform實現:

public class AllenCompTransform extends Transform {

private Project project;
private IComponentProvider provider

public AllenCompTransform(Project project,IComponentProvider componentProvider) {
this.project = project;
this.provider=componentProvider
}

@Override
public String getName() {
return "AllenCompTransform";
}

@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}

@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}

@Override
public boolean isIncremental() {
return false;
}

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

long startTime = System.currentTimeMillis();

transformInvocation.getOutputProvider().deleteAll();
File jarFile = transformInvocation.getOutputProvider().getContentLocation("main", getOutputTypes(), getScopes(), Format.JAR);
if (!jarFile.getParentFile().exists()) {
jarFile.getParentFile().mkdirs()
}
if (jarFile.exists()) {
jarFile.delete();
}

ClassPool classPool = new ClassPool()
project.android.bootClasspath.each{
classPool.appendClassPath((String)it.absolutePath)
}

def box=ConvertUtils.toCtClasses(transformInvocation.getInputs(),classPool)

CodeWeaver codeWeaver=new AsmWeaver(provider.getAllActivities(),provider.getAllServices(),provider.getAllReceivers())
codeWeaver.insertCode(box,jarFile)

System.out.println("AllenCompTransform cost "+(System.currentTimeMillis()-startTime)+" ms")
}
}

gradle插件的發布

絕大多數gradle插件,我們可能都是只要在公司內部使用,那麼只要使用公司內部的maven倉庫即可,即配置並運用maven插件,然後執行其upload task即可。這個很簡單,不再贅述。

特殊的buildSrc

前面說過gradle插件的發布,那如果我們在插件的代碼編寫階段,總不能修改一點點代碼,就發布一個版本,然後重新運用吧?

有人可能會說,那就不發布到maven倉庫,而是發布到本地倉庫唄,然而這樣至多發布時節省一點點時間,仍然太麻煩。

幸好有buildSrc!

在buildSrc中定義的插件,可以直接在其他module中運用,而且是類似這種運用方式:

apply plugin: wang.imallen.blog.comp.MainPlugin

即直接apply具體的類,而不是其發布名稱,這樣的話,不管做什麼修改,都能馬上體現,而不需要等到重新發布版本。

gradle插件的調試

以調試:app:assembleRelease這個task為例,其實很簡單,分如下兩步即可:

  • 新建remote target
  • 在命令行輸入./gradlew --no-daemon -Dorg.gradle.debug=true :app:assembleRelease
  • 之後選擇剛剛創建的remote target, 然後點擊調試按鈕即可

推薦閱讀:

如何看待小米啟動防回滾機制,這是否跟華為關閉解鎖碼服務有關?
Android中的Parcelable詳解
6G+128G榮耀Play維持2399元!這讓非旗艦機咋賣?
Android自定義遮罩層

TAG:Android | Android開發 | Gradle |