自定義gradle插件入門

Android Gradle

Android項目使用 Gradle 作爲構建框架,Gradle 又是以Groovy爲腳本語言。所以學習Gradle之前需要先熟悉Groovy腳本語言。

Groovy是基於Java語言的腳本語言,所以它的語法和Java非常相似,但是具有比java更好的靈活性。下面就列舉一些和Java的主要區別。

Android Gradle 的 Project 和 Tasks

這個Gradle中最重要的兩個概念。每次構建(build)至少由一個project構成,一個project 由一到多個task構成。項目結構中的每個build.gradle文件代表一個project,在這編譯腳本文件中可以定義一系列的task;task 本質上又是由一組被順序執行的Action`對象構成,Action其實是一段代碼塊,類似於Java中的方法。

Android Gradle 構建生命週期

每次構建的執行本質上執行一系列的Task。某些Task可能依賴其他Task。那些沒有依賴的Task總會被最先執行,而且每個Task只會被執行一遍。每次構建的依賴關係是在構建的配置階段確定的。每次構建分爲3個階段:

  • Initialization: 初始化階段

這是創建Project階段,構建工具根據每個build.gradle文件創建出一個Project實例。初始化階段會執行項目根目錄下的settings.gradle文件,來分析哪些項目參與構建。

所以這個文件裏面的內容經常是:

include ':app'

include ':libraries'

這是告訴Gradle這些項目需要編譯,所以我們引入一些開源的項目的時候,需要在這裏填上對應的項目名稱,來告訴Gradle這些項目需要參與構建。

  • Configuration:配置階段

這個階段,通過執行構建腳本來爲每個project創建並配置Task。配置階段會去加載所有參與構建的項目的build.gradle文件,會將每個build.gradle文件實例化爲一個Gradle的project對象。然後分析project之間的依賴關係,下載依賴文件,分析project下的task之間的依賴關係。

  • Execution:執行階段

這是Task真正被執行的階段,Gradle會根據依賴關係決定哪些Task需要被執行,以及執行的先後順序。 
task是Gradle中的最小執行單元,我們所有的構建,編譯,打包,debug,test等都是執行了某一個task,一個project可以有多個task,task之間可以互相依賴。例如我有兩個task,taskA和taskB,指定taskA依賴taskB,然後執行taskA,這時會先去執行taskB,taskB執行完畢後在執行taskA。

說到這可能會有疑問,我翻遍了build.gradle也沒看見一個task長啥樣,有一種被欺騙的趕腳!

其實不是,你點擊AndroidStudio右側的一個Gradle按鈕,會打開一個面板,內容差不多是這樣的:

這裏寫圖片描述

裏面的每一個條目都是一個task,那這些task是哪來的呢?

一個是根目錄下的 build.gradle 中的

dependencies {

   classpath 'com.android.tools.build:gradle:3.2.1'

}

一個是 app 目錄下的 build.gradle 中的

apply plugin: 'com.android.application'

這兩段代碼決定的。也就是說,Gradle提供了一個框架,這個框架有一些運行的機制可以讓你完成編譯,但是至於怎麼編譯是由插件決定的。還好Google已經給我們寫好了Android對應的Gradle工具,我們使用就可以了。

根目錄下的build.gradle中 dependencies { classpath ‘com.android.tools.build:gradle:3.2.1’ } 是Android Gradle編譯插件的版本。

app目錄下的build.gradle中的apply plugin: ‘com.android.application’ 是引入了Android的應用構建項目,還有com.android.library和com.android.test用來構建library和測試。

所有Android構建需要執行的task都封裝在工具裏,如果你有一些特殊需求的話,也可以自己寫一些task。那麼對於開發一個Android應用來說,最關鍵的部分就是如何來用Android Gradle的插件了。

認知Gradle Wrapper

Android Studio中默認會使用 Gradle Wrapper 而不是直接使用Gradle。命令也是使用gradlew而不是gradle。這是因爲gradle針對特定的開發環境的構建腳本,新的gradle可能不能兼容舊版的構建環境。爲了解決這個問題,使用Gradle Wrapper 來間接使用 gradle。相當於在外邊包裹了一箇中間層。對開發者來說,直接使用Gradlew 即可,不需要關心 gradle的版本變化。Gradle Wrapper 會負責下載合適的的gradle版本來構建項目。

Android 三個文件重要的 gradle 文件

Gradle項目有3個重要的文件需要深入理解:項目根目錄的 build.gradle , settings.gradle 和模塊目錄的 build.gradle 。

  • 1.settings.gradle 文件會在構建的 initialization 階段被執行,它用於告訴構建系統哪些模塊需要包含到構建過程中。對於單模塊項目, settings.gradle 文件不是必需的。對於多模塊項目,如果沒有該文件,構建系統就不能知道該用到哪些模塊。

  • 2.項目根目錄的 build.gradle 文件用來配置針對所有模塊的一些屬性。它默認包含2個代碼塊:buildscript{…}和allprojects{…}。前者用於配置構建腳本所用到的代碼庫和依賴關係,後者用於定義所有模塊需要用到的一些公共屬性。

buildscript {
  repositories {
    jcenter()
  }

  dependencies {
     classpath 'com.android.tools.build:gradle:3.2.1'
  }
}


allprojects {
  repositories {
    jcenter()
  }
}


task clean(type: Delete) {
  delete rootProject.buildDir

}

 

buildscript:定義了 Android 編譯工具的類路徑。repositories中, jCenter是一個著名的 Maven 倉庫。

allprojects:中定義的屬性會被應用到所有 moudle 中,但是爲了保證每個項目的獨立性,我們一般不會在這裏面操作太多共有的東西。

模塊級配置文件 build.gradle 針對每個moudle 的配置,如果這裏的定義的選項和頂層 build.gradle定義的相同。它有3個重要的代碼塊:plugin,android 和 dependencies。

定製項目屬性(project properties)

在項目根目錄的build.gradle配置文件中,我們可以定製適用於所有模塊的屬性,通過ext 代碼塊來實現。如下所示:

ext {
  compileSdkVersion = 28
  buildToolsVersion = "28.0.0"
}

然後我們可以在模塊目錄的build.gradle配置文件中引用這些屬性,引用語法爲rootProject.ext.{屬性名}。如下:

android {
  compileSdkVersion rootProject.ext.compileSdkVersion
  buildToolsVersion rootProject.ext.buildToolsVersion
}

 

Android studio gradle Task

這裏寫圖片描述

//構建

gradlew app:clean //移除所有的編譯輸出文件,比如apk

gradlew app:build //構建 app module ,構建任務,相當於同時執行了check任務和assemble任務

//檢測

gradlew app:check //執行lint檢測編譯。

//打包

gradlew app:assemble //可以編譯出release包和debug包,可以使用gradlew assembleRelease或者gradlew assembleDebug來單獨編譯一種包

gradlew app:assembleRelease //app module 打 release 包

gradlew app:assembleDebug //app module 打 debug 包

//安裝,卸載

gradlew app:installDebug //安裝 app 的 debug 包到手機上

gradlew app:uninstallDebug //卸載手機上 app 的 debug 包

gradlew app:uninstallRelease //卸載手機上 app 的 release 包

gradlew app:uninstallAll //卸載手機上所有 app 的包

這些都是基本的命令,在實際項目中會根據不同的配置,會對這些task 設置不同的依賴。比如 默認的 assmeble 會依賴 assembleDebug 和assembleRelease,如果直接執行assmeble,最後會編譯debug,和release 的所有版本出來。如果我們只需要編譯debug 版本,我們可以運行assembleDebug。

除此之外還有一些常用的新增的其他命令,比如 install命令,會將編譯後的apk 安裝到連接的設備。

gradle項目實戰

最近參與基礎架構組的crashly收集項目,其中一個模塊就是收集項目中使用到的插件和sdk的四級(或三級)包路徑和混淆後的mapping文件, 然後調用python文件實現文件和數據字段的上傳。請求網絡上傳數據這塊python代碼量非常少就幾行代碼的事情,所以我在項目中使用了兩個.gradle文件分別掃描項目中使用到的插件和sdk的四級(或三級)包路徑,然後在.gradle文件中的task中調用python文件上傳數據,這樣工作就告一段落,然後當我找QA和負責打包的同事提交我的幾個文件時才知道我這做法是多麼lowbee, 這樣實現需要每個業務線的項目都接入我的幾個.gradle文件和.py文件,每次.gradle和.py文件修改在提交上去後各個業務線需要拉去最新的代碼,這樣的話我需要把各個業務線的項目源碼都下載到本地,挨個改一遍在提交,這種做法沒有做到通用性和可維護性。在同事的建議下,我又硬着頭皮開始研究自定義gradle插件,每個業務線只需要apply插件就可以,這個方案不錯,開始折騰gradle插件。

        首先說下功能就是定義2個任務,其中一個任務就是掃描業務方的項目過濾出使用到的四級(或三級)包路徑,然後生成一個文件連同參數一起使用py腳本上傳到後臺。

        但是這裏遇到了2個問題:

       1. 在插件module下的python文件在插件中是沒法訪問的,插件中是很容易拿到業務方的目錄,如果將python文件放在業務方的項目中,同樣是擴展性不好;

       2. 在插件中上傳文件和其他參數我引入了第三方庫的依賴,執行 uploadArchives 任務將代碼上傳到本地倉庫後,在業務方demo中報錯,無法訪問到插件中的依賴。

        我在技術羣裏問了幾遍結果都沒有人迴應,僅有的一個同行回覆是遇到類似的問題,然後他的建議是去網上下載一個有類似功能的demo,然後修改下邏輯。順着這條線索我去網上找到第四個纔算是試成功了,下面將操作過程重演一遍。      

1)  首先新建一個Library庫,刪掉裏面的文件最後保留和新建的文件夾如下圖所示:

2) 刪掉build.gradle中的內容,最終配置如下所示:

3)  在groovy文件夾下新建包名並在該名下新建文件CrashlyPlugin.groovy, 內容如下:

4)    在gradle-plugins文件夾下新建一個文件com.ke.crashly.plugin.properties, 內容如下:

5)    sync一下項目,然後找到開發工具右側的Gradle下的:crashlyplugin -> Tasks -> upload -> 雙擊uploadArchives,就會在當前項目下生成一個目錄crashlyrepo, 接下來就可以在項目中使用這個插件了;

6)    在工程的根目錄下的build.gradle文件下添加如下代碼:

7)    在工程的主module的build.gradle文件最後添加如下代碼:

8)    sync一下工程,同樣是在開發工具右側的Gradle標籤下,找到:app -> Tasks -> other ->  uploadRepoNameForCrashly控制檯就會打印百度首頁網頁代碼,也可以在Terminal下輸入命令: gradle task -q uploadRepoNameForCrashly -s.

生命週期函數的應用

project.gradle.buildFinished {
   // 可以上傳mapping.txt文件
}
project.afterEvaluate {
  // 可以獲取在build.gradle文件中的自定義的配置信息
  
  it.android.applicationVariants.all { variant ->
    // 當存在多個變體時,在這裏可以遍歷多個變體和applicationId, map的形式保存
    // 根據構建模式的信息最終確定applicationId
  }
}
project.gradle.startParameter.getTaskNames().each {
  // 命令行輸入 ./gradlew clean assembleRelease -s 
  // 會輸出 clean assembleRelease 
  // 可以利用這個信息確定構建模式 
}

gradle插件的三種形式

1. 在build.gradle中編寫自定義插件

2. buildSrc工程項目

將插件的源代碼放在rootProjectDir/buildSrc/src/main/groovy目錄中,Gradle會自動識別來完成編譯和測試。Android Studio會找rootPrjectDir/buildSrc/src/main/groovy目錄下的代碼。

坑:當新建libraray module並命名爲buildSrc後會提示Plugin with id'com.android.library' not found.

這是因爲buildSrc是Android的保留名稱,只能作爲plugin插件使用,我們修改buildSrc的build.gradle文件後就不報錯了,且這個工程的構建時間最早。

3. 獨立項目或獨立Module

無論是在build.gradle中編寫自定義插件,還是buildSrc項目中編寫自定義插件,都只能在自己的項目中進行使用。如果想要分享給其他人或者自己用,可以在一個獨立的項目中編寫插件,這個項目會生成一個包含插件類的JAR文件,有了JAR文件就很容易進行分享了。

這裏主要講一下buildSrc工程項目, 這個工程可以支持多個gradle插件本地調試。

1. 目錄結構如下所示

2. buildSrc#build.gradle文件內容

apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
  mavenCentral()
}

dependencies{
  //gradle sdk
  implementation gradleApi()
  //groovy sdk
  implementation localGroovy()
}

// 如果有多個gradle插件, 在下面添加文件路徑
sourceSets {
  main {
    java.srcDirs = ['src/main/java'

    ]
    groovy.srcDirs = ['src/main/groovy',
        '../lifecycle_plugin/src/main/groovy'

    ]
    resources.srcDirs = ['src/main/resources',
        '../lifecycle_plugin/src/main/resources'

    ]
  }
}
3. gradle插件的build.gradle文件
apply plugin: 'groovy'

dependencies{
  //gradle sdk
  implementation gradleApi()
  //groovy sdk
  implementation localGroovy()
}
//以上配置比較固定


//以下內容主要用來上傳插件
apply plugin: 'maven'
repositories {
  mavenCentral()
}

// classpath: 'group:name:version' // buildSrc模式下, name爲module名稱
group = 'com.ke.plugin'
version = '1.0.0-SNAPSHOT'


uploadArchives{
  repositories {
    mavenDeployer{
      repository(url: uri('../repositories')) // 和gradle插件同級目錄下
    }
  }
}

4. gradle插件實現的功能, 拷貝mapping.txt文件到指定目錄下

class LifeCyclePlugin implements Plugin<Project> {

  @Override
  void apply(Project project) {
    project.extensions.create("pluginConfig", PluginConfig)

    // 命令行輸入 ./gradlew clean assembleRelease -s
    project.gradle.startParameter.taskNames.each {
      println("taskName: " + it.toString())
      // taskName: clean
      // taskName: assembleRelease
    }

    // writeReleaseApplicationId 在這個tasks裏面
    project.afterEvaluate {
      project.tasks.each {
      }
      println("mappingPath ---> " + project.extensions.pluginConfig.mappingPath)
    }

    project.gradle.taskGraph.whenReady {
      project.gradle.taskGraph.allTasks.each {
      }
    }

    project.tasks.create(name: "copyMappingFileTask", type: Copy) {
//      from(new File('build/outputs/mapping/release/mapping.txt'))
      from(new File("build/outputs/mapping"))
      include '*/mapping.txt'
      into "build/output/mapping"
    }

    project.task('copyMappingFile') {
      doLast {
        try {
          File resultFile = new File(project.buildDir, "/output/mapping.txt")
          if (!resultFile.exists()) {
            resultFile.parentFile.mkdirs()
            resultFile.createNewFile()
          }
          List<File> plugMappings = searchFiles(new File(project.buildDir.absolutePath + "/outputs/mapping"), "mapping.txt")
          if (plugMappings && plugMappings.size() > 0 && resultFile.exists()) {
            // 使用NIO來操作流
            FileChannel channel = new FileOutputStream(resultFile.absolutePath).getChannel()
            for (File file : plugMappings) {
              FileChannel fc = new FileInputStream(file).getChannel()
              channel.transferFrom(fc, channel.size(), fc.size())
              fc.close()
            }
            channel.close()
          }
        } catch (Exception e) {
          e.printStackTrace()
        }
      }
    }

    project.tasks.whenTaskAdded { task ->
      if ("assembleRelease".equals(task.name)) {
//        task.dependsOn(project.tasks.getByName('copyMappingFile'))
      }
      if ('transformClassesWithDexBuilderForRelease'.equals(task.name)) {
//        task.dependsOn(project.tasks.getByName('copyMappingFileTask'))
      }
    }
  }

  /**
   * 獲取給定目錄下指定文件名的文件集合
   */
  static List<File> searchFiles(File folder, final String keyword) {
    List<File> result = new ArrayList<File>()

    File[] subFolders = folder.listFiles(new FileFilter() {
      @Override
      public boolean accept(File file) {
        if (file.isDirectory()) {
          return true
        }
        if (file.getName().toLowerCase().contains(keyword)) {
          return true
        }
        return false
      }
    })

    if (subFolders != null) {
      for (File file : subFolders) {
        if (file.isFile() && file.getName().toLowerCase().contains(keyword)) {
          // 如果是文件則將文件添加到結果列表中
          result.add(file)
        } else {
          // 如果是文件夾,則遞歸調用本方法,然後把所有的文件加到結果列表中
          result.addAll(searchFiles(file, keyword))
        }
      }
    }
    return result
  }
}

5. 本地使用該gradle插件

根目錄下的build.gradle文件加入如下代碼


buildscript {
    
    repositories {
       ...
        maven {
            // ../ 代表當前父目錄的父目錄
            url (uri('./repositories'))
        }
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'

        // group : PluginProjectName : version
        classpath 'com.ke.plugin:lifecycle_plugin:1.0.0-SNAPSHOT'
    }
}

module下的build.gradle添加一行代碼

apply plugin: 'com.ke.lifecycle.plugin'

就可以在工程中調試gradle插件的功能。

 

針對共享插件, 修改的地方就是將產物上傳到maven, 配置文件如下所示:

apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
group = GROUP
version = VERSION_NAME


def getPropertyFromLocalProperties(key) {
    File file = project.rootProject.file('local.properties');
    if (file.exists()) {
        Properties properties = new Properties()
        properties.load(file.newDataInputStream())
        return properties.getProperty(key)
    }
}

def getRepositoryUrl() {
    return isSnapshot() ? getPropertyFromLocalProperties("SNAPSHOT_REPOSITORY_URL") : getPropertyFromLocalProperties("RELEASE_REPOSITORY_URL")
//    return isSnapshot() ? SNAPSHOT_REPOSITORY_URL : RELEASE_REPOSITORY_URL
}

def isSnapshot() {
    return version.endsWith("SNAPSHOT");
}

def hasAndroidPlugin() {
    return getPlugins().inject(false) { a, b->
        def classStr = b.getClass().name
        def isAndroid = ("com.android.build.gradle.LibraryPlugin" == classStr) || ("com.android.build.gradle.AppPlugin" == classStr)
        a || isAndroid
    }
}
task sourcesJar(type: Jar) {
    if (hasAndroidPlugin()) {
        from android.sourceSets.main.java.srcDirs
        classifier = 'sources'
    } else {
        from sourceSets.main.allSource
        classifier = 'sources'
    }
}
artifacts {
    archives sourcesJar
}

uploadArchives {

//    repositories.mavenDeployer {
//        repository(url: uri('/Users/xxx/.m2/repository/')) // 本地倉庫的路徑
//        pom.groupId = "${project.group}"  //groupId ,自行定義,一般是包名
//        pom.artifactId = "${project.name}" //artifactId ,自行定義
//        pom.version = "${version}" //version 版本號
//    }

    repositories.mavenDeployer {
        repository(url: repositoryUrl) {
            authentication(userName: getPropertyFromLocalProperties("USER"), password: getPropertyFromLocalProperties("PASSWORD"))
        }
    }
}
bintray {
    user = getPropertyFromLocalProperties("bintray.user")
    key = getPropertyFromLocalProperties("bintray.apikey")
    configurations = ['archives']
    pkg {
        repo = 'maven'
        name = "${project.group}:${project.name}"
        userOrg = 'xxx'
        licenses = ['Apache-2.0']
        websiteUrl = 'xxx'
        vcsUrl = 'xxx'
        publish = true
    }
}
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'

一些常量信息都是在local.properties中配置。

Gradle plugin 調試

1. run - > Edit Configurations - > Remote  name = debug      

2. 命令行輸入: ./gradlew clean build -Dorg.gradle.daemon=false -Dorg.gradle.debug=true

 

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