Gradle自動化專案構建之Gradle學習及實戰

若丶相見發表於2019-12-22

繼上一篇Gradle自動化專案構建之快速掌握Groovy,我們繼續深入Gradle自動化專案構建技術的學習。

Gradle概念

什麼是gradle wrapper?

gradle wrapper 就是由gradle 幫我們生成的gradlew指令碼,裡面包含了用到的gradle版本資訊,我們編譯程式碼的時候不直接執行gradle命令,而是執行gradlew 命令,他會自動幫我們下載對應的gradle dist,gradle wrapper被新增到程式碼管理系統, 這樣每一個開發人員都不用去折騰gradle版本。

gradle命令(Linux執行需要使用 ./)
常用任務指令:
gradlew build。生成所有的輸出,並執行所有的檢查。
gradlew run。生成應用程式並執行某些指令碼或二進位制檔案
gradlew check。執行所有檢測類任務如tests、linting等
gradlew clean。刪除build檔案目錄。
gradlew projects。檢視專案結構。
gradlew tasks。檢視任務列表。檢視某個任務詳細資訊,可用gradle help --task someTask
gradlew dependencies。檢視依賴列表。
gradlew assembleDebug(或者gradlew aD) 編譯並打Debug包
gradlew assembleRelease(或者gradlew aR) 編譯並打Release的包
除錯類:
gradlew -?, -h, --help。檢視幫助資訊。
gradlew -v,--version。檢視版本資訊。
gradlew -s,--stacktrace。執行任務時,列印棧資訊。如gradle build --s
日誌類:
-q, --quiet。只列印errors類資訊。
-i, --info。列印詳細的資訊。
效能類:
--configure-on-demand,--no-configure-on-demand。是否開啟按需配置模式。
--build-cache, --no-build-cache。是否使用快取。

其它的詳見其官方文件:https://docs.gradle.org/current/userguide/command_line_interface.html
複製程式碼

Gradle執行流程

  1. 初始化階段:執行settings.gradle指令碼,解析整個工程中所有Project,構建所有Project對應的project物件。
  2. 配置階段:解析所有project物件中的task物件,構建好所有task的拓撲圖
  3. 執行階段:執行具體的task以及依賴的task

Gradle生命週期

// setting.gradle檔案
    println '初始化階段執行完畢'

    // settings.gradle配置完後呼叫,只對settings.gradle設定生效
    gradle.settingsEvaluated {
        println "settings:執行settingsEvaluated..."
    }

    // 當settings.gradle中引入的所有project都被建立好後呼叫,只在該檔案設定才會生效
    gradle.projectsLoaded {
        println "settings:執行projectsLoaded..."
    }

    // 在每個project進行配置前呼叫,child project必須在root project中設定才會生效,root project必須在settings.gradle中設定才會生效
    gradle.beforeProject { proj ->
        println "settings:執行${proj.name} beforeProject"
    }

    // 在每個project配置後呼叫
    gradle.afterProject { proj ->
        println "settings:執行${proj.name} afterProject"
    }

    // 所有project配置完成後呼叫
    gradle.projectsEvaluated {
        println "settings: 執行projectsEvaluated..."
    }

    //構建開始前呼叫
    gradle.buildStarted {
        println "構建開始..."
    }

    //構建結束後呼叫
    gradle.buildFinished {
        println "構建結束..."
    }

// build.gradle檔案中
/**
 * 配置本Project階段開始前的監聽回撥
 */
this.beforeEvaluate {
    println '配置階段執行之前'
}

/**
 * 配置本Project階段完成以後的回撥
 */
this.afterEvaluate {
    println '配置階段執行完畢'
}

/**
 * gradle執行本Project完畢後的回撥監聽
 */
this.gradle.buildFinished {
    println '執行階段執行完畢'
}

/**
 * 所有project配置完成後呼叫,可直接在setting.gradle中監聽
 */
gradle.projectsEvaluated {
    gradle ->
        println "所有的project都配置完畢了,準備生成Task依賴關係"
}

/**
 * 表示本Project "task 依賴關係已經生成"
 */
gradle.taskGraph.whenReady {
    TaskExecutionGraph graph ->
        println "task 依賴關係已經生成"
}

/**
 * 每一個 Task 任務執行之前回撥
 */
gradle.taskGraph.beforeTask {
    Task task ->
        println "Project[${task.project.name}]--->Task[${task.name}] 在執行之前被回撥"
}

/**
 * 每一個 task 執行之後被回撥
 */
gradle.taskGraph.afterTask {
    task, TaskState taskState ->
        //第二個參數列示 task 的狀態,是可選的引數
        println "Project[${task.project.name}]--->Task[${task.name}] 在執行完畢,taskState[upToDate:${taskState.upToDate},skipped:${taskState.skipped},executed:${taskState.executed},didWork:${taskState.didWork}]"
}
複製程式碼
  • 注1:上述例子中setting.gradle和build.gradle中存在重複的Gradle生命週期
  • 注2:有一些生命週期只在setting.gradle中配置有效,比如settingsEvaluated
  • 注3:根據Gradle執行流程,第一步初始化setting.gradle檔案,第二步配置各個project。而配置各個project的順序是按照projectName首字母a-z的順序執行,因此若某一生命週期在所有project的中間的位置宣告,則會在宣告處以及後面的project產生效應。

附一張不知名大佬的執行流程和宣告週期圖示:

Gradle自動化專案構建之Gradle學習及實戰

Project

Peoject定義:

1. 從Gradle的角度看,Gradle的管理是樹狀結構的,最外層的是根project,裡層module是子project。
2. 每一個子project都會對應輸出,比如:apk,war,aar等等這個依賴配置完成,
3. 每個project的配置和管理都是依靠自己的build.gradle完成的,並且build.gradle檔案也是是否為project的標識。
4. 雖然Gradle的管理是樹狀結構,也可以在裡層module中再建立module,但是實際開發中絕對不會在子project中再建立子project,因此此樹狀結構只有兩層。
注:通過命令:gradlew projects,可以驗證Project的樹狀結構
複製程式碼

Project相關api

api	   作用
getAllprojects() 獲取工程中所有的project(包括根project與子project)
getSubProjects() 獲取當前project下,所有的子project(在不同的project下呼叫,結果會不一樣,可能返回null)
getParent()      獲取當前project的父project(若在rooProject的build.gradle呼叫,則返回null)
getRootProject() 獲取專案的根project(一定不會為null)
project(String path, Closure configureClosure)  根據path找到project,通過閉包進行配置(閉包的引數是path對應的Project物件)
allprojects(Closure configureClosure)	 配置當前project和其子project的所有project
subprojects(Closure configureClosure)	 配置子project的所有project(不包含當前project)
複製程式碼

屬性相關api

  1. 在gradle指令碼檔案中使用ext塊擴充套件屬性(父project中通過ext塊定義的屬性,子project可以直接訪問使用)

     // rootProject : build.gradle
     ext { // 定義擴充套件屬性
       compileSdkVersion = 28
       libAndroidDesign = 'com.android.support:design:28.0.0'
     }
    
     // app : build.gradle
     android {
       compileSdkVersion = this.compileSdkVersion // 父project中的屬性,子project可以直接訪問使用
       ...
     }
     dependencies {
       compile this.libAndroidDesign // 也可以使用:this.rootProject.libAndroidDesign
       ...
     }
    複製程式碼
  2. 在gradle.properties檔案中擴充套件屬性

     // gradle.properties
    
     isLoadTest=true // 定義擴充套件屬性
     mCompileSdkVersion=28 // 定義擴充套件屬性
    
     // setting.gradle
     // 判斷是否需要引入Test這個Module
     if(hasProperty('isLoadTest') ? isLoadTest.toBoolean() : false) {
       include ':Test'
     }
    
     // app : build.gradle
     android {
       compileSdkVersion = mCompileSdkVersion.toInteger()
       ...
     }
    複製程式碼
    1. hasProperty('xxx'):判斷是否有在gradle.properties檔案定義xxx屬性。
    2. 在gradle.properties中定義的屬性,可以直接訪問,但得到的型別為Object,一般需要通過toXXX()方法轉型。

檔案相關API

api	作用
getRootDir()	獲取rootProject目錄
getBuildDir()	獲取當前project的build目錄(每個project都有自己的build目錄)
getProjectDir()	獲取當前project目錄
File file(Object path)	定位一個檔案,相對於當前project開始查詢
ConfigurableFileCollection files(Object... paths)	定位多個檔案,與file類似
copy(Closure closure)	拷貝檔案
fileTree(Object baseDir, Closure configureClosure)	定位一個檔案樹(目錄+檔案),可對檔案樹進行遍歷

例子:
// 列印common.gradle檔案內容
println getContent('common.gradle')
def getContent(String path){
  try{
    def file = file(path)
    return file.text
  }catch(GradleException e){
    println 'file not found..'
  }
  return null
}

// 拷貝檔案、資料夾
copy {
  from file('build/outputs/apk/')
  into getRootProject().getBuildDir().path + '/apk/'
  exclude {} // 排除檔案
  rename {} // 檔案重新命名
}

// 對檔案樹進行遍歷並拷貝
fileTree('build/outputs/apk/') { FileTree fileTree ->
    // 訪問樹結構的每個結點
    fileTree.visit { FileTreeElement element ->
        println 'the file name is: '+element.file.name
        copy {
            from element.file
            into getRootProject().getBuildDir().path + '/test/'
        }
    }
}
複製程式碼

依賴相關API

1. 配置工程倉庫及gradle外掛依賴

// rootProject : build.gradle
buildscript { ScriptHandler scriptHandler ->
    // 配置工程倉庫地址
    scriptHandler.repositories { RepositoryHandler repositoryHandler ->
        repositoryHandler.jcenter()
        repositoryHandler.mavenCentral()
        repositoryHandler.mavenLocal()
        repositoryHandler.ivy {}
        repositoryHandler.maven { MavenArtifactRepository mavenArtifactRepository ->
            mavenArtifactRepository.name 'personal'
            mavenArtifactRepository.url 'http://localhost:8081/nexus/repositories/'
            mavenArtifactRepository.credentials {
                username = 'admin'
                password = 'admin123'
            }
        }
    }
    // 配置工程的"外掛"(編寫gradle指令碼使用的第三方庫)依賴地址
    scriptHandler.dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
        classpath 'com.tencent.tinker-patch-gradle-plugin:1.7.7'
    }
}

// =========================== 上述簡化後 ============================

buildscript {
    /**
     * 配置工程倉庫地址
     *  由於repositories這個閉包中的delegate是repositoryHandler,
     *      因此可以省略repositoryHandler的引用,直接使用其屬性和方法。
     */
    repositories {
        jcenter()
        mavenCentral()
        mavenLocal()
        ivy {}
        maven {
            name 'personal'
            url 'http://localhost:8081/nexus/repositories/'
            credentials {
                username = 'admin'
                password = 'admin123'
            }
        }
    }
    // 配置工程的"外掛"(編寫gradle指令碼使用的第三方庫)依賴地址
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
        classpath 'com.tencent.tinker-patch-gradle-plugin:1.7.7'
    }
}
複製程式碼

2. 配置應用程式第三方庫依賴

// app : build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar']) // 依賴檔案樹
    // compile file() // 依賴單個檔案
    // compile files() // 依賴多個檔案
    implementation 'com.android.support:appcompat-v7:28.0.0' // 依賴倉庫中的第三方庫(即:遠端庫)
    implementation project('CommonSDK') { // 依賴工程下其他Module(即:原始碼庫工程)
      exclude module: 'support-v4' // 排除依賴:排除指定module
      exclude group: 'com.android.support' // 排除依賴:排除指定group下所有的module
      transitive false // 禁止傳遞依賴,預設值為false
    }
    implementation('xxx') {
        changing true // 每次都從服務端拉取
    }

    // 棧內編譯
    provided('com.tencent.tinker:tinker-android-anno:1.9.1')
}
複製程式碼
  1. implementation和api: 編譯依賴包並將依賴包中的類打包進apk。
  2. provided: 只提供編譯支援,但打包時依賴包中的類不會寫入apk。
    1. 依賴包只在編譯期起作用。(如:tinker的tinker-android-anno只用於在編譯期生成Application,並不需要把該庫中類打包進apk,這樣可以減小apk包體積)
    2. 被依賴的工程中已經有了相同版本的第三方庫,為了避免重複引用,可以使用provided。

外部命令API

// copyApk任務:用於將app工程生成出來apk目錄及檔案拷貝到本地下載目錄
task('copyApk') {
    // doLast中會在gradle執行階段執行
    doLast {
        // gradle的執行階段去執行
        def sourcePath = this.buildDir.path + '/outputs/apk'
        def destinationPath = '/Users/xxx/Downloads'
        def command = "mv -f ${sourcePath} ${destinationPath}"
        // exec塊程式碼基本是固定的
        exec {
            try {
                executable 'bash'
                args '-c', command
                println 'the command is executed success.'
            }catch (GradleException e){
                println 'the command is executed failed.'
            }
        }
    }
}
複製程式碼

Task

Task定義及配置

Task定義的方法很簡單,建立的方式主要為兩種: * 一種迭代宣告task任務以及doLast,doFirst方法新增可執行程式碼; * 一種是通過 “<<” 快捷建立task任務,閉合執行任務程式碼。但不僅限於這兩種。

TaskContainer:管理所有的Task,如:增加、查詢。

  1. 定義(建立)Task

    // 直接通過task函式去建立
    task helloTask {
        println 'i am helloTask.'
    }
    
    // 通過TaskContainer去建立
    this.tasks.create(name: 'helloTask2') {
        println 'i am helloTask 2.'
    }
    複製程式碼
    • 檢視所有Task命令:gradlew task
    • 執行某一Task命令:gradlew taskName
  2. 配置Task

    // 給Task指定分組與描述
    task helloTask(group: 'study', description: 'task study'){ // 語法糖
        ...
    }
    task helloTask {
        group 'study' // 或者setGroup('study')
        description 'task study' // 或者setDescription('task study')
        ...
    }
    複製程式碼

    Task除了可以配置group、description外,還可以配置name、type、dependsOn、overwrite、action。

    • 注1:給Task分組之後,該task會被放到指定組中,方便歸類查詢。(預設被分組到other中)
    • 注2:給Task新增描述,相當於給方法新增註釋。

Task的執行詳情

Gradle的執行階段執行的都是Task,即只有Task可在執行階段執行。

  1. Task中doFirst與doLast的使用:
    // 1. task程式碼塊內部使用
    task helloTask {
        println 'i am helloTask.'
        doFirst {
            println 'the task group is: ' + group
        }
        // doFirst、doLast可以定義多個
        doFirst {}
    }
    // 2. 外部指定doFirst(會比在閉包內部指定的doFirst先執行)
    helloTask.doFirst {
        println 'the task description is: ' + description
    }
    
    // 統計build執行時長
    def startBuildTime, endBuildTime
    this.afterEvaluate { Project project ->
        // 通過taskName找到指定的Task
        def preBuildTask = project.tasks.getByName('preBuild') // 執行build任務時,第一個被執行的Task
        // 在preBuildTask這個task執行前執行
        preBuildTask.doFirst {
            startBuildTime = System.currentTimeMillis()
        }
        def buildTask = project.tasks.getByName('build') // 執行build任務時,最後一個被執行的Task
        // 在buildTask這個task執行後執行
        buildTask.doLast {
            endBuildTime = System.currentTimeMillis()
            println "the build time is: ${endBuildTime - startBuildTime}"
        }
    }
    複製程式碼
  2. 總結
    1. Task閉包中直接編寫的程式碼,會在配置階段執行。可以通過doFirst、doLast塊將程式碼邏輯放到執行階段中執行。
    2. doFirst、doLast可以指定多個。
    3. 外部指定的doFirst、doLast會比內部指定的先執行。
    4. doFirst、doLast可以對gradle中提供的已有的task進行擴充套件。

Task的執行順序

  1. Task執行順序指定的三種方式:
    1. dependsOn強依賴方式
    2. 通過Task輸入輸出指定(與第1種等效)
    3. 通過API指定執行順序
  2. Task的依賴
    task taskX {
        doLast {
            println 'taskX'
        }
    }
    task taskY {
        doLast {
            println 'taskY'
        }
    }
    // 方式一:靜態依賴
    // task taskZ(dependsOn: taskY) // 依賴一個task
    task taskZ(dependsOn: [taskX, taskY]) { // 依賴多個task,需要用陣列[]表示
        doLast {
            println 'taskZ'
        }
    }
    // 方式二:靜態依賴
    taskZ.dependsOn(taskX, taskY)
    // 方式三:動態依賴
    task taskZ() {
        dependsOn this.tasks.findAll {
            // 依賴所有以lib開頭的task
            task -> return task.name.startsWith('lib')
        }
        doLast {
            println 'taskZ'
        }
    }
    // lib開頭task
    task lib1 << { println 'lib1' }
    task lib2 << { println 'lib2' }
    task lib3 << { println 'lib3' }
    
    注:此處 << 為快捷建立task,閉包裡程式碼等同於在doLast閉包中執行一樣,但此寫法目前已被標記為deprecated
    複製程式碼
    • taskZ依賴了taskX與taskY,所以在執行taskZ時,會先執行taskX、taskY。
    • taskZ依賴了taskX與taskY,但taskX與taskY沒有關係,它們的執行順序是隨機的。
  3. Task的輸入輸出 流程:Task Inputs --> Task One ——> Task Outputs --> 通過輸入輸出關聯Task間的關閉 --> Task Inputs --> Task Two ——> Task Outputs --> .....
    1. 流程分析:
      1. inputs和outputs是Task的屬性。
      2. inputs可以是任意資料型別物件,而outputs只能是檔案(或資料夾)。
      3. TaskA的outputs可以作為TaskB的inputs。
    2. 程式碼實戰
      // 例子:將每個版本資訊,儲存到指定的release.xml中
      
      ext {
          versionCode = '1.0.0'
          versionName = '100'
          versionInfo = 'App的第1個版本,完成聊天功能'
          destFile = file('release.xml')
          if (destFile != null && !destFile.exists()) {
              destFile.createNewFile()
          }
      }
      
      // writeTask輸入擴充套件屬性,輸出檔案
      task writeTask {
          // 為task指定輸入
          inputs.property('versionCode', this.versionCode)
          inputs.property('versionName', this.versionName)
          inputs.property('versionInfo', this.versionInfo)
          // 為task指定輸出
          outputs.file this.destFile
          doLast {
              def data = inputs.getProperties() // 返回一個map
              File file = outputs.getFiles().getSingleFile()
              // 將map轉為實體物件
              def versionMsg = new VersionMsg(data)
              def sw = new StringWriter()
              def xmlBuilder = new groovy.xml.MarkupBuilder(sw)
              if (file.text != null && file.text.size() <= 0) { // 檔案中沒有內容
                  // 實際上,xmlBuilder將xml資料寫入到sw中
                  xmlBuilder.releases { // <releases>
                      release { // <releases>的子節點<release>
                          versionCode(versionMsg.versionCode)
                          // <release>的子節點<versionCode>1.0.0<versionCode>
                          versionName(versionMsg.versionName)
                          versionInfo(versionMsg.versionInfo)
                      }
                  }
                  // 將sw裡的內容寫到檔案中
                  file.withWriter { writer ->
                      writer.append(sw.toString())
                  }
              } else { // 已經有其它版本資訊了
                  xmlBuilder.release {
                      versionCode(versionMsg.versionCode)
                      versionName(versionMsg.versionName)
                      versionInfo(versionMsg.versionInfo)
                  }
                  def lines = file.readLines()
                  def lengths = lines.size() - 1
                  file.withWriter { writer ->
                      lines.eachWithIndex { String line, int index ->
                          if (index != lengths) {
                              writer.append(line + '\r\n')
                          } else if (index == lengths) {
                              writer.append(sw.toString() + '\r\n')
                              writer.append(line + '\r\n')
                          }
                      }
                  }
              }
          }
      }
      
      // readTask輸入writeTask的輸出檔案
      task readTask {
          inputs.file destFile
          doLast {
              def file = inputs.files.singleFile
              println file.text
          }
      }
      
      task taskTest(dependsOn: [writeTask, readTask]) {
          doLast {
              println '任務執行完畢'
          }
      }
      
      class VersionMsg {
          String versionCode
          String versionName
          String versionInfo
      }
      複製程式碼
      通過執行 gradle taskTask 之後,就可以在工程目錄下看到release.xml檔案了。
  4. Task API指定順序
    • mustRunAfter : 強行指定在某個或某些task執行之後才執行。
    • shouldRunAfter : 與mustRunAfter一樣,但不強制。
    task taskX {
        doLast {
            println 'taskX'
        }
    }
    task taskY {
        // shouldRunAfter taskX
        mustRunAfter taskX
        doLast {
            println 'taskY'
        }
    }
    task taskZ {
        mustRunAfter taskY
        doLast {
            println 'taskZ'
        }
    }
    複製程式碼
    通過執行 gradle taskY taskZ taskX 之後,可以看到終端還是按taskX、taskY、taskZ順序執行的。
  5. 掛接到構建生命週期
    1. 例子:build任務執行完成後,執行一個自定義task
      this.afterEvaluate { Project project ->
          def buildTask = project.tasks.getByName('build')
          if (buildTask == null) throw GradleException('the build task is not found')
          buildTask.doLast {
              taskZ.execute()
          }
      }
      複製程式碼
    2. 例子:Tinker將自定義的manifestTask插入到了gradle指令碼中processManifest與processResources這兩個任務之間
      TinkerManifestTask manifestTask = project.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask)
      ...
      manifestTask.mustRunAfter variantOutput.processManifest
      variantOutput.processResources.dependsOn manifestTask
      複製程式碼
  6. Task型別
    1. Gradle DSL Version 5.1
    2. Copy - Gradle DSL Version 5.1--> Task types

Gradle其它模組

Settings類

settings.gradle(對應Settings.java)決定哪些工程需要被gradle處理,佔用了整個gradle生命週期的三分之一,即Initialzation初始化階段。

SourceSet類

Gradle有一個約定的目錄結構,格式和maven的結構一樣。但不同的是,gradle的目錄結構是可以改的。對預設的檔案位置進行修改,從而讓gradle知道哪種資源要從哪些資料夾中去查詢。

// 1. sourceSets是可以呼叫多次的
android {
    sourceSets {
        main {
            // 配置jni so庫存放位置
            jniLibs.srcDirs = ['libs']
        }
    }
    sourceSets {
        main {
            // 根據模組配置不同的資源位置
            res.srcDirs = ['src/main/res',  // 普通資源目錄
                           'src/main/res-ad',   // 廣告資源目錄
                           'src/main/res-player']   // 播放器相關資源目錄
        }
    }
}

// 2. sourceSets一般情況下是一次性配置
android {
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            res.srcDirs = ['src/main/res',
                           'src/main/res-ad',
                           'src/main/res-player']
        }
    }
}

// 3. 使用程式設計的思想,配置sourceSets
this.android.sourceSets{
    main {
        jniLibs.srcDirs = ['libs']
        res.srcDirs = ['src/main/res',
                       'src/main/res-ad',
                       'src/main/res-player']
    }
}
複製程式碼

Gradle Plugin

Gradle外掛(Plugin)是什麼?

Gradle中的Plugin是對完成指定功能的Task封裝的體現,只要工程依賴了某個Plugin,就能執行該Plugin中所有的功能,如:使用java外掛,就可以打出jar包,使用Android外掛,就可以生成apk、aar。

自定義Plugin

  1. 建立外掛工程

    • 在工程目錄下建立buildSrc資料夾。
    • 在buildSrc目錄下,建立src資料夾、build.gradle檔案。
    • 在buildSrc/src目錄下,再建立main資料夾。
    • 在buildSrc/src/main目錄下,再分別建立groovy、resources資料夾。
    • 在buildSrc/src/main/resources再建立一個META-INF資料夾,再在META-INF下建立一個gradle-plugins資料夾。
    • 在build.gradel檔案中輸入如下指令碼:
      apply plugin: 'groovy'
      
      sourceSets {
          main {
              groovy {
                  srcDir 'src/main/groovy'
              }
              resources {
                  srcDir 'src/main/resources'
              }
          }
      }
      複製程式碼
      最後,Async一下工程,buildSrc就會被識別出來了,整體目錄如圖:E:\CodeProject\android\Github\JcyDemoList\SourceCodeAnalysis\src\原始碼分析\圖示講解\Gradle自定義Plugin.png
  2. 建立外掛類: 與Java一樣,在groovy目錄下,建立一個包,再建立一個外掛類(如:com.android.gradle.GradleStudyPlugin),該外掛類必須實現Plugin介面。

    注意:gradle外掛類是.groovy檔案,不是.java檔案

    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    /**
     * 自定義Gradle外掛
     */
    class GradleStudyPlugin implements Plugin<Project> {
    
        /**
         * 外掛引入時要執行的方法
         * @param project 引入當前外掛的project
         */
        @Override
        void apply(Project project) {
            println 'hello gradle study plugin. current project name is ' + project.name
        }
    }
    複製程式碼
  3. 指定外掛入口: 在編寫完外掛類的邏輯之後,需要在META-INF.gradle-plugins目錄下建立一個properties檔案(建議以外掛類包名來命名,如:com.android.gradle.properties),在該properties中宣告外掛類,以此來指定外掛入口。

    該properties檔案的名字將作為當前gradle外掛被app工程引用的依據。

    implementation-class=com.android.gradle.GradleStudyPlugin
    // 如果報錯 Could not find implementation class 'xxx' 的話,
    // 一般是類全路徑有問題,預設包不需要寫包路徑,修改如下即可:implementation-class=GradleStudyPlugin
    複製程式碼
  4. 使用自定義外掛: 開啟app工程的build.gradle,應用上面的自定義gradle外掛,並Async。

    apply plugin: 'com.android.application'
    apply plugin: 'com.android.gradle'
    
    android {
      ...
    }
    複製程式碼

    在Terminal中可以看到,在gradle的配置階段,就輸出了前面自定義外掛的apply方法中的日誌。

  5. 建立擴充套件屬性: 外掛往往會在gradle指令碼中進行引數配置,如在android{}中,可以配置compileSdkVersion等引數,其實本質上,就是在gradle指令碼中使用閉包方式建立了一個javaBean,並將其傳遞到外掛中被外掛識別讀取而已。

    步驟:

    1. 建立一個實體類,宣告成員變數,用於接收gradle中配置的引數。(可以理解為就是javaBean,不過要注意,該檔案字尾是.groovy,不是.java)
      class ReleaseInfoExtension {
          String versionCode
          String versionName
          String versionInfo
          String fileName
      
          ReleaseInfoExtension() {}
      
          @Override
          String toString() {
              return "versionCode = ${versionCode} , versionName = ${versionName} ," +
                      " versionInfo = ${versionInfo} , fileName = ${fileName}"
          }
      }
      複製程式碼
    2. 在自定義外掛中,對當前project進行擴充套件。
      class GradleStudyPlugin implements Plugin<Project> {
      
          /**
           * 外掛引入時要執行的方法
           * @param project 引入當前外掛的project
           */
          @Override
          void apply(Project project) {
              // 這樣就可以在gradle指令碼中,通過releaseInfo閉包來完成ReleaseInfoExtension的初始化。
              project.extensions.create("releaseInfo", ReleaseInfoExtension)
          }
      }
      複製程式碼
    3. 開啟在app工程的build.gradle,通過擴充套件key值命名閉包的方式,就可以配置指定引數了。
      apply plugin: 'com.android.gradle'
      
      releaseInfo {
          versionCode = '1.0.0'
          versionName = '100'
          versionInfo = '第一個app資訊'
          fileName = 'release.xml'
      }
      複製程式碼
    4. 接收引數
      def versionCodeMsg = project.extensions.releaseInfo.versionCode
      複製程式碼
  6. 建立擴充套件Task: 自定義外掛無非就是封裝一些常用Task,所以,擴充套件Task才是自定義外掛的最重要的一部分。擴充套件Task也很簡單,繼承DefaultTask,編寫TaskAction註解方法。

    // 例子:把app版本資訊寫入到xml檔案中
    import groovy.xml.MarkupBuilder
    import org.gradle.api.DefaultTask
    import org.gradle.api.tasks.TaskAction
    
    class ReleaseInfoTask extends DefaultTask {
    
        ReleaseInfoTask() {
            group 'android' // 指定分組
            description 'update the release info' // 新增說明資訊
        }
    
        /**
         * 使用TaskAction註解,可以讓方法在gradle的執行階段去執行。
         * doFirst其實就是在外部為@TaskAction的最前面新增執行邏輯。
         * 而doLast則是在外部為@TaskAction的最後面新增執行邏輯。
         */
        @TaskAction
        void doAction() {
            updateInfo()
        }
    
        private void updateInfo() {
            // 獲取gradle指令碼中配置的引數
            def versionCodeMsg = project.extensions.releaseInfo.versionCode
            def versionNameMsg = project.extensions.releaseInfo.versionName
            def versionInfoMsg = project.extensions.releaseInfo.versionInfo
            def fileName = project.extensions.releaseInfo.fileName
            // 建立xml檔案
            def file = project.file(fileName)
            if (file != null && !file.exists()) {
                file.createNewFile()
            }
            // 建立寫入xml資料所需要的類。
            def sw = new StringWriter();
            def xmlBuilder = new groovy.xml.MarkupBuilder(sw)
            // 若xml檔案中沒有內容,就多建立一個realease節點,並寫入xml資料
            if (file.text != null && file.text.size() <= 0) {
                xmlBuilder.releases {
                    release {
                        versionCode(versionCodeMsg)
                        versionName(versionNameMsg)
                        versionInfo(versionInfoMsg)
                    }
                }
                file.withWriter { writer ->
                    writer.append(sw.toString())
                }
            } else { // 若xml檔案中已經有內容,則在原來的內容上追加。
                xmlBuilder.release {
                    versionCode(versionCodeMsg)
                    versionName(versionNameMsg)
                    versionInfo(versionInfoMsg)
                }
                def lines = file.readLines()
                def lengths = lines.size() - 1
                file.withWriter { writer ->
                    lines.eachWithIndex { String line, int index ->
                        if (index != lengths) {
                            writer.append(line + '\r\n')
                        } else if (index == lengths) {
                            writer.append(sw.toString() + '\r\n')
                            writer.append(line + '\r\n')
                        }
                    }
                }
            }
        }
    }
    複製程式碼

    與建立擴充套件屬性一樣,擴充套件Task也需要在project中建立注入。

    /**
     * 自定義Gradle外掛
     */
    class GradleStudyPlugin implements Plugin<Project> {
    
        /**
         * 外掛引入時要執行的方法
         * @param project 引入當前外掛的project
         */
        @Override
        void apply(Project project) {
            // 建立擴充套件屬性
            // 這樣就可以在gradle指令碼中,通過releaseInfo閉包來完成ReleaseInfoExtension的初始化。
            project.extensions.create("releaseInfo", ReleaseInfoExtension)
            // 建立Task
            project.tasks.create("updateReleaseInfo", ReleaseInfoTask)
        }
    }
    複製程式碼

    再次Async工程之後,就可以在Idea的gradle標籤裡android分組中看到自定義好的Task了。

    注:這種在工程下直接建立buildSrc目錄編寫的外掛,只能對當前工程可見,所以,如果需要將我們自定義好的grdle外掛被其他工程所使用,則需要單獨建立一個庫工程,並建立如buildSrc目錄下所有的檔案,最後上傳maven倉庫即可

  7. Demo請參考:github.com/Endless5F/J…

android外掛對gradle擴充套件

  1. 譯者序 | Gradle Android外掛使用者指南翻譯
  2. Manipulation tasks(操作task) | Gradle Android外掛使用者指南翻譯
  3. 自定義Apk輸出位置:
    this.afterEvaluate {
        this.android.applicationVariants.all { variant ->
            def output = variant.outpus.first() // 獲取變體輸出檔案(outputs返回是一個集合,但只有一個元素,即輸出apk的file)
            def apkName = "app-${variant.baseName}-${variant.versionName}.apk"
            output.outputFile = new File(output.outputFile.parent, apkName)
        }
    }
    複製程式碼

Jenkins

Jenkins是一個開源的、提供友好操作介面的持續整合(CI)工具,起源於Hudson(Hudson是商用的),主要用於持續、自動的構建/測試軟體專案、監控外部任務的執行(這個比較抽象,暫且寫上,不做解釋)。Jenkins用Java語言編寫,可在Tomcat等流行的servlet容器中執行,也可獨立執行。通常與版本管理工具(SCM)、構建工具結合使用。常用的版本控制工具有SVN、GIT,構建工具有Maven、Ant、Gradle。

具體學習請參考:Jenkins詳細教程

參考連結

www.bilibili.com/video/av415…

www.jianshu.com/p/498ae3fab…

www.jianshu.com/u/f9de25923…

...

注:若有什麼地方闡述有誤,敬請指正。期待您的點贊哦!!!

相關文章