這篇文章首發在位元組跳動技術團隊公眾號上,可能有的童鞋已經看過了,不過考慮到可能有的童鞋沒關注那個公眾號,所以這裡再發一遍。
另外,最近創建了一個知識星球,名字跟專欄名字一樣,也叫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框架和android gradle plugin框架統稱為gradle框架,整個系列文章其實分析的就是底層gradle框架和android gradle plugin框架的原理,其中側重點在andorid gradle plugin框架,因為這與我們日常編譯息息相關,也是收益最大的部分。
這是深入理解Gradle框架系列的第一篇。整個系列共分為9篇,文章列表如下:
其實只要是JVM語言,都可以用來寫插件, 比如Android Gradle Plugin團隊,在3.2.0之前一直是用java編寫gradle插件。
國內很多開源項目都是用groovy編寫的,groovy的優勢是書寫方便,而且其閉包寫法非常靈活,然而groovy的缺點也非常明顯,最大的一點不好就是IDE對其的支持非常不好,不僅僅是語法高亮沒做好,還有導航跳轉都極為有限,比如build.gradle中的方法跳轉不到其定義處。
當然,我自己長期使用groovy下來,也發現了它的一些缺點,比如each這個閉包,在運行時竟然會出現找不到其成員的情況。
以及出現開發者自定義的成員與其默認成員(groovy中會為每個類增加一些默認成員)名稱重合時,不能給出有效的提示,當然,這個問題我不確定是IDE的問題還是groovy自身的編譯器實現不夠完善的問題。
其實到目前為止,使用kotlin進行插件開發是最好的選擇,有如下兩個原因:
可能正是這個原因,Google編譯工具組從3.2.0開始,新增的插件全部都是用kotlin編寫的。
比如我們常用的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其實可以理解成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這樣:
要達到這樣的效果其實並不難,只要做好以下兩點:
```groovy class Cat{ String name
String from float weight
} ```
```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 } } }
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插件,我們可能都是只要在公司內部使用,那麼只要使用公司內部的maven倉庫即可,即配置並運用maven插件,然後執行其upload task即可。這個很簡單,不再贅述。
前面說過gradle插件的發布,那如果我們在插件的代碼編寫階段,總不能修改一點點代碼,就發布一個版本,然後重新運用吧?
有人可能會說,那就不發布到maven倉庫,而是發布到本地倉庫唄,然而這樣至多發布時節省一點點時間,仍然太麻煩。
幸好有buildSrc!
在buildSrc中定義的插件,可以直接在其他module中運用,而且是類似這種運用方式:
apply plugin: wang.imallen.blog.comp.MainPlugin
即直接apply具體的類,而不是其發布名稱,這樣的話,不管做什麼修改,都能馬上體現,而不需要等到重新發布版本。
以調試:app:assembleRelease這個task為例,其實很簡單,分如下兩步即可:
推薦閱讀:
※如何看待小米啟動防回滾機制,這是否跟華為關閉解鎖碼服務有關?※Android中的Parcelable詳解※6G+128G榮耀Play維持2399元!這讓非旗艦機咋賣?※Android自定義遮罩層
TAG:Android | Android開發 | Gradle |