Gradle進階計劃(二)Gradle Plugin原理分析

        通過 Gradle進階計劃(一)Gradle初探 的介紹,我們已經對Gradle有了初步的瞭解。這篇文章我們更深入研究一下 Gradle Plugin 的原理。

 

一、Gradle 和 Gradle Plugin

        首先,我們需要先明確一個概念,就是 Gradle 和 Gradle Plugin 是不同的。 

(一)Gradle

        結合上一篇文章,官方已經對Gradle已經有了很詳細的定義,這裏在重點解釋一下。

        Gradle 是一個構建項目的工具,將它用來編譯Android App能夠簡化你的編譯、打包、測試過程。它其實不僅僅是用在Android Studio上。如何去理解構建工具呢?構建工具就是對你的項目進行編譯、運行、簽名、打包、依賴管理等一系列功能的合集。例如 Eclipse 最初是用來做 Java 開發的,Google 爲了能在 Eclipse 上進行Android 開發,開發了ADT 插件(Android Developer Tools),正是因爲有了 ADT ,我們纔可以在 Eclipse 上進行編譯、運行、簽名、打包等一系列流程。而這背後的工作都是 ADT 的功勞,ADT 就是我們的構建工具。一般來說,構建工具除了以上提到的編譯、運行、簽名、打包等,還具備依賴管理的功能。什麼是依賴管理呢?例如我們以前在 Eclipse 上開發 Android 引用第三方庫,一般都是先下載 jar 文件,然後把 jar 文件添加到 libs 目錄,依賴管理就是將這個過程自動化。

        在 Gradle 之前也是 Android 也有其他構建工具的,這就是大名鼎鼎的 Maven。Java 世界中主要有三大構建工具:Ant、Maven 和 Gradle,經過幾年的發展,Ant 幾乎銷聲匿跡,所以在早期 Android 開發中,是使用 Maven 作爲構建工具的。但是 Gradle 相較於 Maven 有着較大的優勢,所以 AS 時代,Google 用 Gradle 替代了 Maven 作爲 Android 開發的構建工具。

  • Gradle 對於依賴的管理更加的簡潔。不需要像 Maven 定義 groupId、artifactId、version 等繁瑣的屬性值;
  • Gradle 支持動態的版本依賴。在版本號後面使用+號的方式可以實現動態的版本管理;
  • Gradle 在解決依賴衝突方面實現機制更加明確。使用 Maven 和 Gradle 進行依賴管理時都採用的是傳遞性依賴;而如果多個依賴項指向同一個依賴項的不同版本時就會引起依賴衝突。而 Maven 處理這種依賴關係往往是噩夢一般的存在;
  • Gradle 多模塊構建方面具有優勢。相較於 Maven 聚合 POM 的形式,Gradle allprojects 和 subprojects 的定義方法更加的方便和清晰;
  • Gradle 在插件機制的支持上做的更好。Maven 配置語法太受限於XML,Gradle中則一切變得非常簡單。

(二)Gradle Plugin

        爲了支持 Gradle 能在 AS 上使用,Google 開發了一個 AS 的插件叫 Android Gradle Plugin ,所以我們能在 AS 上使用 Gradle 完全是因爲這個插件的原因。它一邊調用 Gradle 本身的代碼和批處理工具來構建項目,一邊調用 Android SDK 的編譯、打包功能,從而讓我們能夠順暢地在AS上進行開發。

        Gradle Plugin 是獨立於Android Studio 運行的,它的更新也是與Android Studio分開的。Gradle Plugin 會有版本號,每個版本號又對應有一個或一些 Gradle發行版本(一般是限定一個最低版本)。這也是爲什麼如果這兩個版本對應不上了,你的工程構建的時候會報錯。

插件版本 Gradle版本
1.0.0 - 1.1.3 2.2.1 - 2.3
1.2.0 - 1.3.1 2.2.1 - 2.9
1.5.0 2.2.1 - 2.13
2.0.0 - 2.1.2 2.10 - 2.13
2.1.3 - 2.2.3 2.14.1+
2.3.0+ 3.3+
3.0.0+ 4.1+
3.1.0+ 4.4+

        Android Studio 3.0 之後自動將插件版本升級到3.0.0,所以我們也需要對應地把Gradle升級到4.1纔行。

        其實前面我們還提到了Gradle Wrapper,這裏再簡單說一下。

        Gradle Wrapper:意爲 Gradle 的包裝,它的作用是簡化Gradle本身的安裝、部署。不同版本的項目可能需要不同版本的Gradle,手工部署的話比較麻煩,而且可能產生衝突,所以需要Gradle Wrapper幫你搞定這些事情。Gradle Wrapper是Gradle項目的一部分。

        假設我們本地有多個項目,一個是比較老的項目,還用着 Gradle 1.0 的版本,一個是比較新的項目用了 Gradle 2.0 的版本,但是你兩個項目肯定都想要同時運行的,如果你只裝了 Gradle 1.0 的話那肯定不行,所以爲了解決這個問題,Google 推出了 Gradle Wrapper 的概念,它在你每個項目都配置了一個指定版本的 Gradle ,你可以理解爲每個 Android 項目本地都有一個小型的 Gradle ,通過這個每個項目你可以支持用不同的 Gradle 版本來構建項目。   

       總結:

       Gradle:是一個構建工具,版本定義在 gradle-wrapper.properties中的distributionUrl=https/://services.gradle.org/distributions/gradle-x.xx-all.zip
       Gradle Plugin:是谷歌爲使用Gradle而自行開發的工具,版本定義在 build.gradle中依賴的classpath 'com.android.tools.build:gradle:x.x.x'

 

二、Gradle Plugin 主要流程

        對於源碼的分析這裏就不展開講了,詳情可參照 android gradle plugin 源碼地址,我只介紹一下涉及到的主要流程。

         Android gradle plugin 的入口類 com.android.build.gradle.AppPlugin,這個可以查看 properties 文件,裏面聲明瞭對應插件的入口類。

         AppPlugin 繼承自 BasePlugin。AppPlugin 裏沒有做太多的操作,主要是重寫了 createTaskManager 和 createExtension,剩下的大部分工作還是在 BasePlugin 裏做的。

        有幾點需要注意:

  • build.gradle 裏見到的 android {} dsl 是在 BasePlugin.configureExtension() 裏聲明的
  • 主要的 task 是在 BasePlugin.createAndroidTasks() 裏生成的
  • 主要 task 的實現可以在 TaskManager 中找到
  • transform 會轉化成 TransformTask

 

三、主要 Task 分析

        我們先看一下,生成一個 APK 所需的構建流程是怎樣的。官方流程圖如下:

        下面是詳細的打包流程圖:

(1)打包資源文件。使用aapt來打包res資源文件,生成R.java、resources.arsc和res文件。R.java文件是所有res資源的id列表。resources.arsc裏面會對所有的資源id進行組裝,在apk運行時獲取資源的時候會根據設備的情況獲得不同的資源。
(2)處理aidl文件,生成相應java 文件。這個階段aidl會處理.aidl文件,生成對應的Java接口文件。對於沒有使用到aidl的android工程,可以跳過此步驟。
(3)編譯工程源代碼,生成相應class 文件。通過javac編譯R.java、Java接口文件、Java源文件,生成.class文件。如果有配置混淆的話,會編譯成混淆的class文件。
(4)轉換所有class文件,生成classes.dex文件。Android系統的Dalvik虛擬機的可執行文件爲DEX格式,程序運行所需的class.dex就是在這一步生成的,使用到的工具爲dx,主要的工作是將java字節碼轉換爲Dalvik字節碼、壓縮常量池、消除冗餘信息等。
(5)打包生成apk。apkbuilder將classes.dex、resources.arsc、res文件夾(res/raw資源被原裝不動地打包進APK之外,其它的資源都會被編譯或者處理)、Other Resources(assets文件夾)、AndroidManifest.xml打包成apk文件。
(6)對apk文件進行簽名。通過jarsigner對apk進行簽名。
(7)對簽名後的apk文件進行對其處理。通過zipalign對簽名後的apk進行對齊處理,它能夠對打包後的app進行優化。





        那麼以 Task 的維度來看 apk 的打包,是什麼流程呢?通過執行下面的命令,可以打印出打包一個 apk 需要哪些 task 。

./gradlew android-gradle-plugin-source:assembleDebug --console=plain

        輸出結果如下:

:android-gradle-plugin-source:preBuild UP-TO-DATE
:android-gradle-plugin-source:preDebugBuild
:android-gradle-plugin-source:compileDebugAidl
:android-gradle-plugin-source:compileDebugRenderscript
:android-gradle-plugin-source:checkDebugManifest
:android-gradle-plugin-source:generateDebugBuildConfig
:android-gradle-plugin-source:prepareLintJar UP-TO-DATE
:android-gradle-plugin-source:generateDebugResValues
:android-gradle-plugin-source:generateDebugResources
:android-gradle-plugin-source:mergeDebugResources
:android-gradle-plugin-source:createDebugCompatibleScreenManifests
:android-gradle-plugin-source:processDebugManifest
:android-gradle-plugin-source:splitsDiscoveryTaskDebug
:android-gradle-plugin-source:processDebugResources
:android-gradle-plugin-source:generateDebugSources
:android-gradle-plugin-source:javaPreCompileDebug
:android-gradle-plugin-source:compileDebugJavaWithJavac
:android-gradle-plugin-source:compileDebugNdk NO-SOURCE
:android-gradle-plugin-source:compileDebugSources
:android-gradle-plugin-source:mergeDebugShaders
:android-gradle-plugin-source:compileDebugShaders
:android-gradle-plugin-source:generateDebugAssets
:android-gradle-plugin-source:mergeDebugAssets
:android-gradle-plugin-source:transformClassesWithDexBuilderForDebug
:android-gradle-plugin-source:transformDexArchiveWithExternalLibsDexMergerForDebug
:android-gradle-plugin-source:transformDexArchiveWithDexMergerForDebug
:android-gradle-plugin-source:mergeDebugJniLibFolders
:android-gradle-plugin-source:transformNativeLibsWithMergeJniLibsForDebug
:android-gradle-plugin-source:transformNativeLibsWithStripDebugSymbolForDebug
:android-gradle-plugin-source:processDebugJavaRes NO-SOURCE
:android-gradle-plugin-source:transformResourcesWithMergeJavaResForDebug
:android-gradle-plugin-source:validateSigningDebug
:android-gradle-plugin-source:packageDebug
:android-gradle-plugin-source:assembleDebug

        下面列出了各個 task 的實現類及作用:

Task 對應實現類 作用
preBuild   空 task,只做錨點使用
preDebugBuild   空 task,只做錨點使用,與 preBuild 區別是這個 task 是 variant 的錨點
compileDebugAidl AidlCompile 處理 aidl
compileDebugRenderscript RenderscriptCompile 處理 renderscript
checkDebugManifest CheckManifest 檢測 manifest 是否存在
generateDebugBuildConfig GenerateBuildConfig 生成 BuildConfig.java
prepareLintJar PrepareLintJar 拷貝 lint jar 包到指定位置
generateDebugResValues GenerateResValues 生成 resvalues,generated.xml
generateDebugResources   空 task,錨點
mergeDebugResources MergeResources 合併資源文件
createDebugCompatibleScreenManifests CompatibleScreensManifest manifest 文件中生成 compatible-screens,指定屏幕適配
processDebugManifest MergeManifests 合併 manifest 文件
splitsDiscoveryTaskDebug SplitsDiscovery 生成 split-list.json,用於 apk 分包
processDebugResources ProcessAndroidResources aapt 打包資源
generateDebugSources   空 task,錨點
javaPreCompileDebug JavaPreCompileTask 生成 annotationProcessors.json 文件
compileDebugJavaWithJavac AndroidJavaCompile 編譯 java 文件
compileDebugNdk NdkCompile 編譯 ndk
compileDebugSources   空 task,錨點使用
mergeDebugShaders MergeSourceSetFolders 合併 shader 文件
compileDebugShaders ShaderCompile 編譯 shaders
generateDebugAssets   空 task,錨點
mergeDebugAssets MergeSourceSetFolders 合併 assets 文件
transformClassesWithDexBuilderForDebug DexArchiveBuilderTransform class 打包 dex
transformDexArchiveWithExternalLibsDexMergerForDebug ExternalLibsMergerTransform 打包三方庫的 dex,在 dex 增量的時候就不需要再 merge 了,節省時間
transformDexArchiveWithDexMergerForDebug DexMergerTransform 打包最終的 dex
mergeDebugJniLibFolders MergeSouceSetFolders 合併 jni lib 文件
transformNativeLibsWithMergeJniLibsForDebug MergeJavaResourcesTransform 合併 jnilibs
transformNativeLibsWithStripDebugSymbolForDebug StripDebugSymbolTransform 去掉 native lib 裏的 debug 符號
processDebugJavaRes ProcessJavaResConfigAction 處理 java res
transformResourcesWithMergeJavaResForDebug MergeJavaResourcesTransform 合併 java res
validateSigningDebug ValidateSigningTask 驗證簽名
packageDebug PackageApplication 打包 apk
assembleDebug   空 task,錨點

        以上就是打包一個 APK 所需要的 Task,在分析主要Task之前,我們先了解一下 Task 的分類。

        在 Gradle Plugin 中的 Task 主要有三種:

(1)普通 Task

  • 一般繼承 DefaultTask;
  •  看 @TaskAction 註解的方法,此方法就是這個 Task 做的事情

(2)增量 Task:相對於全量來說的,全量我們可以理解爲調用 clean 以後第一次編譯的過程,這個就是全量編譯,之後修改了代碼或者資源文件,再次編譯,就是增量編譯。

  •  首先這個 Task 要繼承 IncrementalTask;
  • 其次看 isIncremental 方法,如果返回 true,說明支持增量,返回 false 則不支持;
  • 然後看 doFullTaskAction 方法,是全量的時候執行的操作;
  • 最後看 doIncrementalTaskAction 方法,這裏是增量的時候執行的操作。

(3)Transform:這個後面我會專門出一篇文章來講。

  • 繼承自 Transform;
  • 重點關注 transform 方法實現。

1. generateDebugBuildConfig

(1)實現類:GenerateBuildConfig

(2)整體實現:

(3)代碼調用:

GenerateBuildConfig.generate -> BuildConfigGenerator.generate -> JavaWriter

(4)分析實現:

        在 GenerateBuildConfig 中,主要生成代碼的步驟如下:

  • 生成 BuildConfigGenerator
  • 添加默認的屬性,包括 DEBUG,APPLICATION_ID,FLAVOR,VERSION_CODE,VERSION_NAME
  • 添加自定義屬性
  • 調用 JavaWriter 生成 BuildConfig.java 文件

2. mergeDebugResources

(1)實現類:MergeResources

(2)整體實現:

(3)代碼調用:

MergeResources.doFullTaskAction -> ResourceMerger.mergeData -> MergedResourceWriter.end 
-> QueueableAapt2.compile -> Aapt2QueuedResourceProcessor.compile -> AaptProcess.compile 
-> AaptV2CommandBuilder.makeCompile

 (4)分析實現:

         MergeResources 這個類,繼承自 IncrementalTask,我們重點關注 doFullTaskAction 這個全量方法。

  • 通過 getConfiguredResourceSets() 獲取 resourceSets,包括了自己的 res/ 和 依賴庫的 res/ 以及 build/generated/res/rs
  • 創建 ResourceMerger
  • 創建 QueueableResourceCompiler,因爲 gradle3.x 以後支持了 aapt2,所以這裏有兩種選擇 aapt 和 aapt2。其中 aapt2 有三種模式,OutOfProcessAaptV2,AaptV2Jni,QueueableAapt2,這裏默認創建了 QueueableAapt2,resourceCompiler = QueueableAapt2
  • 將第一步獲取的 resourceSet 加入 ResourceMerger 中
  • 創建 MergedResourceWriter
  • 調用 ResourceMerger.mergeData 合併資源
  • 調用 MergedResourceWriter 的 start(),addItem(),end() 方法
  • 調用 QueueableAapt2 -> Aapt2QueuedResourceProcessor -> AaptProcess 處理資源,這一步調用 aapt2 命令去處理資源,處理完以後 xxx.xml.flat 格式

3. processDebugResources

(1)實現類:ProcessAndroidResources

(2)整體實現:

(3)代碼調用:

ProcessAndroidResources.doFullTaskAction -> ProcessAndroidResources.invokeAaptForSplit 
-> AndroidBuilder.processResources -> QueueAapt2.link -> Aapt2QueuedResourceProcessor.link 
-> AaptProcess.link -> AaptV2CommandBuilder.makeLink

(4)分析實現:

  • 獲取 split 數據,返回的是一個 ApkData 列表,ApkData 有三個子類,分別是 Main,Universal,FullSplit。這裏的 ApkData 會返回一個 Universal 和多個 FullSplit,Universal 代表的是主 apk,FullSplit 就是根據屏幕密度拆分的 apk。如果我們沒有配置 splits apk,那麼這裏只會返回一個 Main 的實例,標識完整的 apk。
  • 處理 main 和 不依賴 density 的 ApkData 資源
  • 調用 invokeAaptForSplit 處理資源
  • 調用 AndroidBuilder.processResources -> QueueAapt2.link -> Aapt2QueuedResourceProcessor.link -> AaptProcess.link -> AaptV2CommandBuilder.makeLink 處理資源,生成資源包以及 R.java 文件
  • 處理其他 ApkData 資源,這裏只會生成資源包而不會生成 R.java 文件

4. processDebugManifest

(1)實現類:MergeManifests

(2)整體實現:

(3)代碼調用:

MergeManifests.dofFullTaskAction -> AndroidBuilder.mergeManifestsForApplication 
-> Invoker.merge -> ManifestMerge2.merge

(4)分析實現:

        這個 task 功能主要是合併 mainfest,包括 module 和 flavor 裏的,整個過程通過 MergingReport,ManifestMerger2 和 XmlDocument 進行。
這裏直接看 ManifestMerger2.merge() 的 merge 過程 。 主要有幾個步驟:

  • 獲取依賴庫的 manifest 信息,用 LoadedManifestInfo 標識
  • 獲取主 module 的 manifest 信息
  • 替換主 module 的 Manifest 中定義的某些屬性,替換成 gradle 中定義的屬性 例如: package, version_code, version_name, min_sdk_versin 等
  • 合併 flavor,buildType 中的 manifest
  • 合併依賴庫的 manifest
  • 處理 manifest 的 placeholders
  • 之後對最終合併後的 manifest 中的一些屬性重新進行一次替換,類似步驟 4
  • 保存 manifest 到 build/intermediates/manifest/fullxxx/AndroidManifest.xml 這就生成了最終的 Manifest 文件

5. transformClassesWithDexBuilderForDebug

(1)實現類:DexArchiveBuilderTransform

(2)整體實現:

(3)代碼調用:

DexArchiveBuilderTransform.transform -> DexArchiveBuilderTransform.convertJarToDexArchive 
-> DexArchiveBuilderTransform.convertToDexArchive -> DexArchiveBuilderTransform.launchProcessing 
-> DxDexArchiveBuilder.convert

(4)分析實現:

        在 DexArchiveBuilderTransform 中,對 class 的處理分爲兩種方式,一種是對 目錄下的 class 進行處理,一種是對 .jar 裏的 class 進行處理。
爲什麼要分爲這兩種方式呢?.jar 中的 class 一般來說都是依賴庫,基本上不會改變,gradle 在這裏做了一個緩存,但是兩種方式最終都會調用到 convertToDexArchive。

  • convertJarToDexArchive 處理 jar。處理 .jar 時,會對 jar 包中的每一個 class 都單獨打成一個 .dex 文件,之後還是放在 .jar 包中
  • convertToDexArchive 處理 dir 以及 jar 的後續處理。 對 dir 處理使用 convertToDexArchive,其中會調用 launchProcessing。在 launchProcessing 中,有下面幾個步驟:a)判斷目錄下的 class 是否新增或者修改過     b)調用 DexArchiveBuilder.build 去處理修改過的 class     c)DexArchiveBuilder 有兩個子類,D8DexArchiveBuilder 和 DxDexArchiveBuilder,分別是調用 d8 和 dx 去打 dex

6. transformDexArchiveWithExternalLibsDexMergerForDebug / transformDexArchiveWithDexMergerForDebug

(1)實現類:ExternalLibsMergerTransform / DexMergerTransform

(2)整體實現:

 

(3)代碼調用:

// dx 
ExternalLibsMergerTransform / DexMergerTransform.transform -> DexMergerTransformCallable.call 
-> DxDexArchiveMerger.mergeDexArchives -> DxDexArchiveMerger.mergeMonoDex 
-> DexArchiveMergerCallable.call -> DexMerger.merge

// d8
ExternalLibsMergerTransform / DexMergerTransform.transform -> DexMergerTransformCallable.call 
-> D8DexArchiveMerger.mergeDexArchives -> 調用 D8 命令

(4)分析實現:

        主要是處理依賴庫的 dex,把上一步生成的依賴庫的 dex merge 成一個 dex。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章