Android熱修復技術(二) Groovy語法及打Patch包

一、前言

上篇我們講到了patch包的打包是通過dex命令來生成classes.dex之類的dex文件,但是實際項目開發中我們不可能每次都把對應的包、類一一拷貝出來然後自己手動去敲dx命令去打包,所我們的目的是編寫任務和插件去自動打patch包,在這個過程中我們首先需要學習的是Gradle Task和Plugin的定製

二、Groovy語法

我們平時使用的Android studio開發就是基於Gradle來構建的,而Gradle是基於Grovvy語言的構建工具,它使用DSL語法,(Domain Specified Language 是領域專用語言,通俗的說叫行話,能達到共識的一種語言)將開發過程中需要的編譯、構建、測試、打包以及部署工作,變得非常簡單方便和重複使用,所以在學習gradle之前,咱們先大概瞭解下groovy的基本語法

在Groovy中編寫Java代碼也是可以的,因爲Groovy是一門JVM語言,最終也會被編譯成JVM字節碼,交給虛擬機去執行

1.變量聲明

訪問修飾符

def:相當於局部變量
ext:相當於全局變量

聲明變量

//groovy是一種規則非常鬆的語言,語句後面不用寫分號
//而且類型也可以不用寫
int x = 1
def x = 1

2.字符串

def s1 = 'the x is $x'  =>  單引號,輸出會原樣得到 the x is $x
def s2 = "the x is $x"  =>  雙引號,輸出的時候會對$符號進行轉義 the x is 1
def s3 = '''
this is the first line
this is the second line
this is the third line
'''  
 => 三個引號'''xxx'''中的字符串支持隨意換行,對$符不能進行轉義

3.範圍

範圍是指值序列的速記,範圍由序列中的第一個和最後一個表示,下面是範圍寫法的例子

  • 1..10 =>[1,10]
  • 1..<10 =>[1,10)
  • ‘a’..’x’ =>[a,x]
  • 10..1 =>[10,1]
def test(){
   for(i in 1..5){
        println i
   }
}

4.集合和映射

集合,相當於Java中List對象

//創建空列表
def list = []

//增加一個元素
list.add("Gradle")

//左移添加,返回該列表
list << "Groovy"

//增加多個元素
list.addAll(["Groovy","Gradle"])

//刪除一個元素
list.remove("Groovy")

映射,相當於Java中的Map對象

//創建空映射
def map = [:]

//增加值
map = ['name':'shanshan','age':18]
map.put('lastName','xue')

//遍歷映射元素
map.each{
    println "$it.key : $it.value"
}
map.eachWithIndex{ it, i ->
    println "$i : $it"
}

//判斷映射是否包含某鍵
assert map.containsKey('name')

5.函數和閉包

函數,也可以寫成和Java中一樣的方法,不過groovy可以寫的更簡單些

//形參類型可以不寫
String doSomething(arg1, arg2){
    println "method doSomething(arg1, arg2)"
}
//不指定返回類型,必須加def  最後一句不用寫return 默認返回最後一句
def doSomething(){
    println "method doSomething"
}

閉包(Closure),是一種數據結構,屬於一段代碼,使用{}包括

//無參數
def closure1 = {
    println "hello world"
}
closure1()  //執行閉包,輸出 hello world

--------------------------------------------------------------

//接收一個參數
def closure2 = {
    String str -> println str  //箭頭前面是參數定義,後面是執行代碼,str爲外部傳入的參數
}
如果只有一個參數可以用it代替,也可以寫作:
def closure2 = {
    println it
}
closure2("hello world")  //執行閉包,輸出 hello world

--------------------------------------------------------------

//接收多個參數
def closure3 = {
    String str, int n -> println "$str : $n"
}
//也可以省去參數類型,寫作:
def closure3 = {
    str, n -> println "$str : $n"
}
closure3("hello world",1)  //執行閉包,輸出 hello world : 1

--------------------------------------------------------------

//使用變量
def var = "hello world"
def closure4 = {
    println var
}
closure4()  //執行閉包 輸出 hello world

--------------------------------------------------------------

//改變上下文
def closure5{
    println name  //這時name並不存在
}
MyClass m = new MyClass()
closure5.setDelegate(m)
class MyClass{
    def name = "hello world"
}
closuere5() //執行閉包,輸出 hello world

閉包的優勢

比如我們在寫一個方法的時候,中間的某項操作可能需要調用者來實現,那麼我們一般情況下是通過接口來實現,如果有了閉包,就可以直接使用閉包來實現,比如

interface Callback{
    void doCall(int a);
}
def doSomething(Callback callback){
    println "start"
    callback.doCall(10)  //需要通過接口來實現
    println "over"
}
//使用閉包
def doSomething(Closure c){
    println "start"
    c(10)  //直接通過閉包來實現,相當於傳入了一段代碼,也可以指定參數
    println "over"
}
//調用doSomething方法,省去了()
doSomething{ int a->
    println "a is ${a}"
}

6.類和對象

Groovy和其他面嚮對象語言一樣,也存在類和對象的概念,和java中類的定義是一樣的。
跟java的區別就在於Groovy會爲每個字段生成getter和setter,我們可以訪問字段本身訪問setter和getter,比如

class MyClass{
    private String name
}
def myClass = new MyClass()
myClass.name = "this is name"  //因爲groovy動態的爲name創建了setter和getter,所以可以直接訪問
println myClass.name

7.delegate機制

class Child{
    private String name
}
class Parent{
   private String name //parent也有一個name屬性
    Child child = new Child();

    void config(Closure c){
        c.delegate = clild
        c.setResolveStrategy Closure.DELEGATE_FIRST //默認情況是OWNER_FIRST,即它會先查找閉包的owner(這裏指parent)
        c()
    }
}

def parent = new Parent()
parent.config{
    name = "this is name"
}
println parent.name  //打印結果爲null
println parent.child.name //打印結果爲this is name

三、Gradle 工程和任務的認識

Gradle是一個框架,它負責定義流程和規則
每一個待編譯的工程都叫一個Project
每一個Project在構建的時候都包含一系列Task

當我們新建一個項目,在沒有編寫任何gradle代碼之前,我們可以在Terminal終端輸入gradle tasks命令(需要配置環境變量)查看當前工程中可使用的任務有哪些

在android studio中,我們有Gradle Wrapper,它爲Window、OSX、Linus平臺都提供了相應的腳本文件,這些腳本使你可在不同平臺的不同Gradle版本上正確執行編譯打包腳本,不需要配置環境變量,這時候需要看所有任務需要執行的命令是./gradlew tasks

這裏寫圖片描述

從圖中可以看到,Android studio自動爲我們添加了一些默認的任務,通過Gradle這些任務我們可以很方便的編譯打包工程,而那些任務又是通過添加插件輕鬆實現的,都是通過build.gradle中apply plugin: 'com.android.application'這句話來實現的

一、工程和任務的關係

在Gradle的世界中最重要的兩個概念就是:工程(Project)和任務(Task),
每一個Gradle項目都會包含一個和多個工程,一個工程可以是一個Module,也可以是一個第三方庫

每一個工程又由一個或多個任務組成,一個任務代表了一個工作的最小單元,它可以是一次類的編譯,打一個包等,Project爲Task提供了執行上下文

這裏寫圖片描述

二、工程屬性

Project properties是Gradle專門爲Project定義的屬性,對於一個project來說,它有很多默認屬性,比如:

  • project: Project本身
  • name: Project的名字
  • path: Project的絕對路徑
  • description: Project的描述信息
  • buildDir: Project構建結果存放目錄
  • version: Project的版本號

除了默認屬性,我們也可以給project添加自定義屬性,添加方法爲

ext{
    versionCode = 1
    versionName = 1.0.0
}

使用方法

defaultConfig{
    versionCode project.property("versionCode")
    versionName project.property("versionName")
}

另外有一個小tip:就是我們可以把一些不想讓別人看到的配置放在gradle.properties這個文件裏面,比如

USER_NAME = 'user_name'
PASS_WORD = 'password'

//在build.gradle裏面可以直接使用 "${USER_NAME}" 拿到

除了在build.gradle中定義屬性,我們也可以通過命令 -P 完成定義和賦值,這種方式具有最高優先級

./gradlew -PversionCode=2 -PversionName=2.0.0 

另外一種方式,我們可以通過JVM系統參數定義Property 使用命令-D (需要以“org.gradle.project”爲前綴)

./gradlew -Dorg.gradle.project.versionCode=3   
-Dorg.gradle.project.versionName=3.0.0

最後一種方式,通過環境變量設置Property (需要以“ORG_ GRADLE_ PROJECT_ ”爲前綴)

export ORG_ GRADLE_ PROJECT_versionCode=4  
export ORG_ GRADLE_ PROJECT_versionName=4.0.0

三、任務的定義和使用

首先我們定義一個很簡單的任務hello,有幾種方式

//第一種
task hello{
    doLast{
        println 'hello World!'
    }
}
//上面的任務還可以使用以下寫法,不過gradle 5.0已經過時了,可以作爲了解
task hello << { //語法糖,相當於doLast
    println 'hello World!'
}

//第二種  以字符串形式定義的
task('hello'){
    doLast{
        println 'hello World!'
    }
}

//第三種 以字符串類型定義的 並定義類型
task ('copy', type: Copy){
    from (file('srcDir')
    into (buildDir)
}
//或者不使用字符串定義 定義類型
task copy(type: Copy){
    from (file('srcDir')
    into (buildDir)
}

//第四種 使用create創建
tasks.create(name: 'hello'){
    doLast{
        println 'hello World!'
    }
}
//使用create創建並指定類型
tasks.create(name: 'hello',type: Copy){
    doLast{
        from (file('srcDir')
        into (buildDir)
    }
}

上面的任務可以在終端中輸入 ./gradlew hello就能輸出 hello World! ,這樣就很簡單的定義了一個任務

task有兩個生命週期,配置階段執行階段

在配置階段,Gradle將讀取所有的build.gradle文件的所有內容來配置Project和Task等,比如設置Project和Task的Property,處理Task之間的依賴關係等等

task Task1{
    def name = "hello"  //這段代碼是在配置階段執行的,配置階段也就是說在task沒有執行的時候這段代碼就會被掃描到,如果訪問了訪問不到的字段,就會直接報錯

    doFirst{ //這段代碼在task執行階段執行,執行階段是指通過./gradle Task1執行的時候這段代碼纔會真正執行
        println "doFirst $name"
    }

    doLast{  //這段代碼在task執行階段執行
        println "doLast $name"
    }
}

四、自定義Task

在Gradle中,我們有三種方法可以自定義Task

  • 在build.gradle文件中直接定義
class HelloWorldTask extends DefaultTask{
   @Optional  //標識可選
    String message = "default message"

    @TaskAction  //標識Task要執行的動作
    def hello(){
        println $message
    }
}

//使用方式
task hello(type: HelloWorldTask)

task hello2(type: HelloWorldTasl){
    messge = "this is a new message"  //給task參數賦值
}

//對於給參數賦值有幾種方式,上面在定義task的時候賦值是一種,還有
方法二
hello2{
    message = "xxx"  //Gradle會爲每個task創建一個同名的方法,該方法接受一個閉包
}
或
hello2.message="xxx"  //Gradle會爲每個task創建一個同名的property,所以可以將Task當做property來使用

方法三  通過Task的configure()方法完成Property的設置
hello2.configure{
    message = "xxx"
}
  • 在當前工程的buildSrc目錄下定義
    在第一種方式中,在build.gradle中直接定義Task,這樣Task的定義和使用混合在一起,在需要定義Task不多時,可以採用這種方法,但是在項目中存在大量自定義Task時,這種方法就非常不合適了,一種改進方法是在另外的一個gradle文件中定義Task,然後使用apply from:到build.gradle中。

    這裏我們使用另一種方法,在buildSrc目錄下定義,Gradle執行時,會自動查找該目錄下所定義的Task類型,並首先編譯該目錄下的groovy代碼以供build.gradle文件使用。具體做法是在當前工程buildSrc/src/main/groovy/com/huli 目錄下創建HelloWorldTask.groovy文件,將Task的定義拷貝過來

  package com.huli  //需要加上包名

  class HelloWorldTask extends DefaultTask{
       @Optional  //標識可選
        String message = "default message"

        @TaskAction  //標識Task要執行的動作
        def hello(){
            println $message
        }
  }

  //使用方式區別在於 引用Task時,需要指定全名稱
  task hello(type: com.huli.HelloWorldTask)
  • 在單獨的項目中定義Task
    雖然第二種方式Task的定義和build.gradle分離開了,但是它依然只能應用在當前工程中,如果我們希望所定義的Task能夠用在另外的項目中,那麼第二種方式就不可行了,此時我們需要將Task的定義放在單獨的工程中,然後在要使用該Task的工程中通過聲明依賴的方式引入這些Task
    這種方式跟後面的自定義Plugin的第三種方式類似,這裏不詳細介紹,可以看後面。

五、任務的依賴關係

我們可以定義一個任務依賴於某個其他的任務

task build{
    doLast{
        println "i am build task"
    }
}

task release(dependsOn: build){  //通過dependsOn來指定依賴於某個任務
    doLast{
        println "i am release task"
    }
}
//如果上面兩個任務是本來就有的而不是自己新寫的,那麼可以通過下面的方式來指定依賴
release.dependsOn build

在終端運行 ./gradlew release將會得到兩行結果

i am build task
i am release task

六、對現有任務添加動作行爲

比如要對上面定義的hello添加動作行爲

//第一種方法
//在doFirst中添加
hello.doFirst{
    println "hello doFirst"
}

//在doLast中添加
hello.doLast{
    println "hello doLast1"
}

//第二種方法
hello{
    doLast{
        println "hello doLast2"
    }
}

在終端執行得到結果爲:

hello doFirst
hello World!
hello doLast1
hello doLast2

七、給任務設置屬性並訪問

task myTask{
   //ext是固定寫法
    ext.myProperty = "myValue"
    //可設置其他屬性
    ...
}
task printTaskProperties{
    doLast{
        println myTask.myProperty
    }
}

八、增量式構建

如果我們將Gradle的Task看作一個黑盒子,那麼我們便可以抽象出輸入和輸出的概念,一個Task對輸入進行操作,然後產生輸出。比如,在使用java插件編譯源代碼時,即輸入爲Java源文件,輸出則爲class文件。如果多次執行一個Task的輸入和輸出是一樣的,那麼我們便可以認爲這樣的Task是沒有必要重複執行的。此時,反覆執行相同的Task是冗餘的,並且是耗時的。

爲了解決這樣的問題,Gradle引入增量式構建的概念。在增量式構建中,我們爲每一個Task定義輸入(inputs)和輸出(outputs),如果在執行一個Task時,他的輸入和輸出與前一次執行時沒有發生變化,那麼Gradle便會認爲該Task是最新的(UP-TO-DATE),因此Gradle將不予執行。一個Task的inputs和outputs可以是一個或多個文件,可以是文件夾,還可以是Project的某個Property,甚至可以是某個閉包所定義的條件。

每個Task都擁有inuts和outputs屬性,他們的類型分別爲TaskInputs和TaskOutputs,在下面的例子中,我們展示了這麼一種場景:名爲combineFileContent的Task從sourceDir目錄中讀取所有的文件,然後將每個文件的內容合併到destination.txt中。讓我們先來看看沒有定義Task輸入和輸出的情況:

task combineFileContentNonIncremental {
   def sources = fileTree('sourceDir')

   def destination = file('destination.txt')

   doLast {
      destination.withPrintWriter { writer ->
         sources.each {source ->
            writer.println source.text
         }
      }
   }
}

多次執行./gradlew combineFileContentNonIncremental時,整個Task都會反覆執行,即便在第一次執行已經得到了所需的結果,如果該task是一個非常繁重的task,那麼多次重複執行勢必造成沒必要的時間浪費

這時,我們可以將sources聲明爲該Task的inputs,而將destination聲明爲outputs,重新創建task如下:

task combineFileContentIncremental {
   def sources = fileTree('sourceDir')
   def destination = file('destination.txt')

   //相比之下,只多了這兩行代碼
   inputs.dir sources
   outputs.file destination

   doLast {
      destination.withPrintWriter { writer ->
         sources.each {source ->
            writer.println source.text
         }
      }
   }
}

當首次執行combineFileContentIncremental時,Gradle會完整的執行該Task,但是緊接着再執行一次,命令就會顯示

:combineFileContentIncremental UP-TO-DATE

BUILD SUCCESSFUL

Total time: 2.104 secs

可以看到,combineFileContentIncremental被標記爲UP-TO-DATE,表示該Task是最新的,Gradle將不予執行,在實際應用中,你將遇到很多這樣的情況,因爲Gradle的很多插件都引入了增量式的構建機制

如果我們修改了inputs(即sourceDir文件夾)中的任何一個文件或刪掉了destination.txt,當調用該任務時,Gradle又會重新執行,因爲此時的Task已經不再是最新的了

九、Gradle目錄結構分析(番外話題,作爲了解)

在默認情況下,Gradle採用了與Maven相同的Java項目目錄結構(src/main/java src/test/java之類的),在採用Maven目錄結構的同時,還融入了自己的一些概念,即source set,Gradle爲我們創建了2個source set,一個名爲main,一個名爲test

請注意,這裏的source set的名字main與目錄結構中的main文件夾並無必然的聯繫,只是在默認情況下,Gradle爲了source set概念到文件系統目錄結構映射方便,才採用了相同的名字,對於test也是如此。我們完全可以在build.gradle文件中重新配置這些source set所對應的目錄結構,同時,我們還可以創建新的source set

從本質上講,Gradle的每個source set都包含有一個名字,幷包含有一個名爲java和另一個名爲resources的Property,他們分別用於表示該source set所包含的java源文件集合和資源文件集合,在實際應用時,我們可以將他們設置成任何目錄值,比如

sourceSets {
   main {
      java {
         srcDir 'java-sources'
      }
      resources {
         srcDir 'resources'
      }
   }
}

另外我們還可以創建新的sourceSet,比如:

sourceSet{
    api
}

默認情況下,該api對應的Java源文件目錄被Gradle設置爲src/api/java,而資源文件目錄則被設置成了src/api/resources,我們也可以像上面的main一樣重新對api的目錄結構進行配置

Gradle默認會自動爲每一個創建的source set創建相應的task,創建規律爲:對於名爲A的sourceSet,Gradle將爲其創建compile\Java、process\Resources和\Classes這3個Task,對於上面的api而言,Gradle會爲其創建compileApiJava、processApiResources和apiClasses Task

你可能會注意到,對於main而言,Gradle並沒有相應的compileMainJava,原因在於:由於main是Gradle默認創建的sourceSet,並且又是極其重要的,便省略掉了其中的Main,而是直接使用了compileJava作爲main的編譯Task,對於test來說,Gradle依然採用了compileTestJava

通常情況是,我們自己創建的名爲api的source set會被其他source set所依賴,比如main中的類需要實現api中的某個接口等,此時,我們需要做兩件事,第一,我們需要在編譯main之前對api進行編譯,即編譯main中的Java源文件的Task應該依賴於api中的Task

Classes.dependsOn apiClasses

第二,在編譯main時,我們需要將api編譯生成的class文件放在main的classpath下

sourceSets{
    main{
        compileClasspath = compileClassPath + files(api.output.classesDir)
    }

    test{
        runtimeClasspath = runtimeClasspath + files(api.output.classesDir)
    }
}

四、項目構建過程中任務分析

對於我們一個Android項目來說,在你點了Android Studio上面的運行按鈕的時候,等編譯完成項目就能安裝到手機上去了,但你知道這中間發生什麼事情了嗎?下面我們就需要分析這個過程大概都做了什麼

我們主module app的build.gradle一般都是以 apply plugin: 'com.android.application' 開始的,這是一個Android studio自帶的插件,該插件內部包含許多的Android相關的task,我們一般主要使用的是assemblecleanbuild,而其中assemble就是我們用於打包apk所用的task,而這個task的依賴關係特別複雜,下面這個圖表示了assembleDebug的依賴關係

這裏寫圖片描述

看上面的圖着實很讓人難以看懂其中的順序,所以下面給出了相應task的執行順序

:app:preBuild    
:app:preDebugBuild      
:app:checkDebugManifest  
:app:prepareDebugDependencies   
:app:compileDebugAidl
:app:compileDebugRenderscript
:app:generateDebugBuildConfig  //generated/source文件夾下生成buildConfig文件夾,根據不同的flavor和buildType生成對應文件夾
:app:generateDebugAssets
:app:mergeDebugAssets
:app:generateDebugResValues  //gennerated/res文件夾下,生成resValues文件夾,根據不同的flavor和buildType生成對應文件夾
:app:generateDebugResources
:app:mergeDebugResources
:app:processDebugManifest
:app:processDebugResources
:app:generateDebugSources  //在gennerated文件夾下生成對應的R.java文件
:app:compileDebugJavaWithJavac  //開始使用javac將.java編譯成.class文件 intermediates下生成classes文件夾,所以我們代碼注入一般放在這個步驟之後,因爲代碼注入是基於.class文件來做的
:app:compileDebugNdk
:app:compileDebugSources
:app:transformClassesWithDexForDebug
:app:mergeDebugJniLibFolders
:app:transformNative_libsWithMergeJniLibsForDebug
:app:processDebugJavaRes
:app:transformResourcesWithMergeJavaResForDebug
:app:validateDebugSigning   
:app:packageDebug
:app:zipalignDebug
:app:assembleDebug 

以上task都是屬於app的module中的,若有多個module,gradle會爲每個module執行一次該task鏈

有兩個Task我們需要留意:
:app:compileDebugJavaWithJavac ————>編譯.java文件到.class
:app: transformClassesWithDexForDebug ————>將.class文件打成dex包

根據以上依賴關係,如果我們有比如打patch包,或者代碼注入等的需求(這些需求都需要在.java文件編譯成.class文件後、打包成dex之前進行操作),就可以定義task然後進行依賴的插入,或者使用doFirstdoLast等相關操作來實現打包過程中,插入自己的業務需求

五、自定義Plugin

在Plugin中,我們可以向Project中加入新的Task,定義configurations和property等。在Gradle中創建自定義插件,有三種方式(跟Task定義的三種方式類似):

  1. 在build.gradle腳本中直接使用

  2. 在buildSrc中使用
    buildSrc是Gradle提供的在項目中配置自定義插件的默認目錄

  3. 在獨立Module中使用

每一個自定義的Plugin都需要實現Plugin接口,上面三種方式只是plugin放置的地方不一樣(放置位置跟上面task的一樣),其實Plugin的定義是一樣的,我們重點講第三種,下面是步驟分析

創建AndroidLibrary 取名爲hotfix-patch,刪除除build.gradle之外的其他文件

修改插件的build.gradle

apply plugin: 'groovy'  //使用groovy插件
apply plugin: 'maven'  //要上傳maven倉庫,所以也要用maven插件

repositories {
    mavenCentral()
    google()
    jcenter()
}

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


uploadArchives {
    repositories {
        mavenDeployer {
            //本地maven地址
            repository(url: uri('../repo-hotfix-patch'))
            pom.groupId = 'com.huli.plugin'
            pom.artifactId = "hotfix-patch"  //插件名
            pom.version = '1.0.0'
        }
    }
}

我們gradle的包管理都是存儲在maven倉庫中的,我們這裏定義一個本地的maven倉庫,我們代碼完成後需要先打包代碼到本地的maven倉庫,然後在主項目中依賴我們打好的包

按照以下目錄創建項目

這裏寫圖片描述

此處有坑,注意下面的META-INF和gradle-plugins是父子目錄關係,不是一個目錄名稱,Android Studio可能會給你優化到一個文件夾,導致你插件無法apply,請分開成父子目錄寫

首先src目錄下創建groovy和resources目錄

  • groovy包用來存儲gradle代碼
  • resources目錄存放的是gradle插件名稱,在外界apply需要根據定義的文件名進行apply,這裏我命名hotfix.patch,那麼在外界就使用apply plugin: ‘hotfix.plugin’

hotfix.plugin.properties文件內容如下,指定插件的實現是哪個類

implementation-class=com.huli.plugin.HotfixPatchPlugin

其次解釋groovy包下的代碼

下面是兩個類,一個是HotfixExtension類,可以用來進行參數的傳遞,代碼爲

package com.huli.plugin

class HotfixExtension {
    String name //打patch包的task所依賴的task
}

然後是插件的代碼

package com.huli.plugin

public class HotfixPatchPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        project.extensions.create('hotfixExt', HotfixExtension)

        println "this is a custom plugin"

        //在插件中定義task
        project.task('hello'){
                doLast{
                    println "hello " + project.hotfixExt.name
                }
        }
    }
}

在plugin中我們只需要實現apply方法,方法傳入的是引用該plugin的project,project中存儲着各種參數

把插件打包到本地倉庫

點擊下面按鈕就能將插件打包上傳到本地maven,之後會在剛纔創建的maven目錄下生成對應的jar

這裏寫圖片描述

生成的目錄如圖,每次對插件代碼的修改都需要再次上傳

這裏寫圖片描述

應用我們的插件

在根目錄的build.gradle下配置

buildscript {
    repositories {
        maven { url uri('repo-hotfix-patch') }
    }
    dependencies {
        classpath 'com.huli.plugin:hotfix-patch:1.0.0'
    }
}

在app的build.gradle中引用插件

apply plugin: 'hotfix.patch'
hotfixExt {
    name = "xiaoming"
}

在gradle中執行命令 ./gradlew hello就能成功輸出
hello xiaoming

六、patch打包任務分析

根據以上知識點,我們爲了將這個打包任務抽成第一個第三方library,方便多個項目使用,我們需要使用第三種方式自定義plugin來實現

1. 新建Android Library工程 配置build.gradle
2. 創建名爲HotfixPatchPlugin的插件,實現Plugin接口
3. 創建HotfixExtension實體類,用於傳遞參數
4. 創建PatchTask
5. 重寫apply(Project project)方法
6. 使用自定義的Plugin
7. 配置需要打入patch包的類

這裏寫圖片描述

8. 通過命令進行patch打包

./gradlew patchMe -PpatchName=shanshan

最後在根目錄下生成對應的包含dex的jar包

這裏寫圖片描述

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