Android Gradle 學習筆記整理

北斗星_And發表於2019-09-18

前言

Gradle 是將軟體編譯、測試、部署等步驟聯絡在一起自動化構建工具。

對於Android開發人員已經瞭解build.gradle 的 android{} 和 dependencies{} ,但是他的編譯過程是什麼樣的?這個過程中可以幹些什麼事瞭解嗎?

此文是學習Gradle時的學習筆記,讓你重新認識Gradle,讓Gradle加快並提效構建你的專案。此時分享給大家,與大家共勉

此筆記主要內容如下

  • Gradle 最基礎的一個專案配置

  • Groovy 基礎語法 並解釋 apply plugin: 'xxxx'和dependencies{}

  • Gradle Project/Task 並自定義Task和Plugin

  • 自定義一個重新命名APP名的外掛 流程

  • APT 技術- Java AbstractProcessor

  • Android 位元組碼增強技術 - Transform (Android 中使用位元組碼增強技術)

文章內容略長,如果你已經掌握Gradle基礎知識,可以直接通過目錄檢視你想看的內容,回顧或者學習都還不錯。

初識Gradle 專案構建配置

gralde專案結構

Android Gradle 學習筆記整理
如圖所示,是一個比較小的gradle配置,這裡主要說兩部分

  1. 綠色部分: gralde版本配置及gralde所需要的指令碼,其中gradlew為linux/mac下的指令碼,gradle.bat為windows下所需的指令碼
  2. 紅色部分:settings.gradle 為根專案的專案配置,外層的build.gradle為根專案的配置,內層的build.gradle為子專案的配置

gradle 配置順序

gralde的專案配置是先識別 settings.gradle,然後在配置各個build.gradle.
為了說明構建執行順序,在上述最基礎的gradle專案結構裡面設定了對應的程式碼

// settings.gradle
println "settings.gradle start"
include ':app'
println "settings.gradle end"
複製程式碼
//root build.gradle
println "project.root start"
buildscript {
    repositories {
    }
    dependencies {
    }
}

allprojects {
}
println "project.root end"
複製程式碼
//app build.gradle
println "project.app start"
project.afterEvaluate {
    println "project.app.afterEvaluate print"
}
project.beforeEvaluate {
    println "project.app.beforeEvaluate print"
}
println "project.app end"
複製程式碼

如果是mac/linux,執行./gradlew 得到如下結果:

settings.gradle start
settings.gradle end

> Configure project :
project.root start
project.root end

> Configure project :app
project.app start
project.app end
project.app.afterEvaluate print
複製程式碼

Groovy 語法

下面講一些關於groovy的語法,可以開啟Android Studio Tools-> Groovy Console練習Groovy 語法 ,如下

Android Gradle 學習筆記整理

可選的型別定義,可以省略語句結束符分號(;)

int vs = 1
def version = 'version1'

println(vs)
println(version)
複製程式碼

括號也是可選的

println vs
println version
複製程式碼

字串定義

def s1 = 'aaa'
def s2 = "version is ${version}"
def s3 = ''' str
is
many
'''
println s1
println s2
println s3
複製程式碼

集合

def list = ['ant','maven']
list << "gradle"
list.add('test')
println list.size()
println list.toString()
//map
def years = ['key1':1000,"key2":2000]
println years.key1
println years.getClass()
複製程式碼

輸出結果

[ant, maven, gradle, test]
1000
class java.util.LinkedHashMap
複製程式碼

閉包

groovy語法中支援閉包語法,閉包簡單的說就是程式碼塊,如下:

def v = {
    v -> println v
}
static def testMethod(Closure closure){
    closure('閉包 test')
}
testMethod v
複製程式碼

其中定義的v就為閉包,testMethod 為一個方法,傳入引數為閉包,然後呼叫閉包.

解釋 apply plugin: 'xxxx'和 dependencies{}

準備工作,看gradle的原始碼

我們先把子專案的build.gradle改為如下形式

apply plugin: 'java-library'
repositories {
    mavenLocal()
}
dependencies {
    compile gradleApi()
}
複製程式碼

這樣,我們就可以直接看gradle的原始碼了,在External Libraries裡如下

Android Gradle 學習筆記整理

解釋

進入build.gradle 點選apply 會進入到gradle的原始碼,可以看到

//PluginAware
 /**
     * Applies a plugin or script, using the given options provided as a map. Does nothing if the plugin has already been applied.
     * <p>
     * The given map is applied as a series of method calls to a newly created {@link ObjectConfigurationAction}.
     * That is, each key in the map is expected to be the name of a method {@link ObjectConfigurationAction} and the value to be compatible arguments to that method.
     *
     * <p>The following options are available:</p>
     *
     * <ul><li>{@code from}: A script to apply. Accepts any path supported by {@link org.gradle.api.Project#uri(Object)}.</li>
     *
     * <li>{@code plugin}: The id or implementation class of the plugin to apply.</li>
     *
     * <li>{@code to}: The target delegate object or objects. The default is this plugin aware object. Use this to configure objects other than this object.</li></ul>
     *
     * @param options the options to use to configure and {@link ObjectConfigurationAction} before “executing” it
     */
    void apply(Map<String, ?> options);
複製程式碼

用Groovy 語法很清楚的解釋,apply其實就是一個方法,後面傳遞的就是一個map,其中plugin為key.

那麼dependencies{}也是一樣

//Project
/**
     * <p>Configures the dependencies for this project.
     *
     * <p>This method executes the given closure against the {@link DependencyHandler} for this project. The {@link
     * DependencyHandler} is passed to the closure as the closure's delegate.
     *
     * <h3>Examples:</h3>
     * See docs for {@link DependencyHandler}
     *
     * @param configureClosure the closure to use to configure the dependencies.
     */
    void dependencies(Closure configureClosure);
複製程式碼

dependencies是一個方法 後面傳遞的是一個閉包的引數.

問題:思考那麼android {}也是一樣的實現嗎? 後面講解

Gradle Project/Task

在前面章節中提到gralde初始化配置,是先解析並執行setting.gradle,然後在解析執行build.gradle,那麼其實這些build.gradle 就是Project,外層的build.gradle是根Project,內層的為子project,根project只能有一個,子project可以有多個.

我們知道了最基礎的gradle配置,那麼怎麼來使用Gradle裡面的一些東西來為我們服務呢?

Plugin

前面提到apply plugin:'xxxx',這些plugin都是按照gradle規範來實現的,有java的有Android的,那麼我們來實現一個自己的plugin.

把build.gradle 改為如下程式碼

//app build.gradle
class LibPlugin implements Plugin<Project>{
    @Override
    void apply(Project target) {
        println 'this is lib plugin'
    }
}
apply plugin:LibPlugin
複製程式碼

執行./gradlew 結果如下

> Configure project :app
this is lib plugin
複製程式碼

Plugin 之Extension

我們在自定義的Plugin中要獲取Project的配置,可以通過Project去獲取一些基本配置資訊,那我們要自定義的一些屬性怎麼去配置獲取呢,這時就需要建立Extension了,把上述程式碼改為如下形式。

//app build.gradle
class LibExtension{
    String version
    String message
}
class LibPlugin implements Plugin<Project>{
    @Override
    void apply(Project target) {
        println 'this is lib plugin'
        //建立 Extension 
        target.extensions.create('libConfig',LibExtension)
        //建立一個task
        target.tasks.create('libTask',{
           doLast{
               LibExtension config = project.libConfig
               println config.version
               println config.message
           }
        })
    }
}
apply plugin:LibPlugin
//配置
libConfig {
    version = '1.0'
    message = 'lib message'
}
複製程式碼

配置完成後,執行./gradlew libTask 得到如下結果

> Configure project :app
this is lib plugin
> Task :lib:libTask
1.0
lib message
複製程式碼

看完上述程式碼,我們就知道android {} 其實他就是一個Extension, 他是由plugin 'com.android.application'或者'com.android.library' 建立。

Task

上述程式碼中,建立了一個名字為libTask的task,gradle中建立task的方式由很多中, 具體的建立介面在TaskContainer類中

//TaskContainer
Task create(Map<String, ?> options) throws InvalidUserDataException;
Task create(Map<String, ?> options, Closure configureClosure) throws InvalidUserDataException;
Task create(String name, Closure configureClosure) throws InvalidUserDataException;
Task create(String name) throws InvalidUserDataException;
<T extends Task> T create(String name, Class<T> type) throws InvalidUserDataException;
<T extends Task> T create(String name, Class<T> type, Action<? super T> configuration) throws InvalidUserDataException;
複製程式碼

Project不可以執行跑起來,那麼我們就要定義一些task來完成我們的編譯,執行,打包等。com.android.application外掛 為我們定義了打包task如assemble,我們剛才定義的外掛為我們新增了一個libTask用於輸出。

Android Gradle 學習筆記整理

Task API

我們看到建立的task裡面可以直接呼叫doLast API,那是因為Task類中有doLast API,可以檢視對應的程式碼看到對應的API

Android Gradle 學習筆記整理

Gradle的一些Task

gradle 為我們定義了一些常見的task,如clean,copy等,這些task可以直接使用name建立,如下:

task clean(type: Delete) {
    delete rootProject.buildDir
}
複製程式碼

依賴task

我們知道Android打包時,會使用assemble相關的task,但是僅僅他是不能直接打包的,他會依賴其他的一些task. 那麼怎麼建立一個依賴的Task呢?程式碼如下

task A{
    println "A task"
}
task B({
    println 'B task'
},dependsOn: A)
複製程式碼

執行./graldew B 輸出

A task
B task
複製程式碼

自定義一個重新命名APP名字的外掛

通過上述的一些入門講解,大概知道了gradle是怎麼構建的,那現在來自定義一個安卓打包過程中,重新命名APP名字的一個外掛。

上述在build.gradle直接編寫Plugin是OK的,那麼為了複用性更高一些,那我們怎麼把這個抽出去呢?

如下

Android Gradle 學習筆記整理

其中build.gradle為

apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
    mavenLocal()
    jcenter()
}

dependencies {
    compile gradleApi()
}

def versionName = "0.0.1"
group "com.ding.demo"
version versionName
uploadArchives{ //當前專案可以釋出到本地資料夾中
    repositories {
        mavenDeployer {
            repository(url: uri('../repo')) //定義本地maven倉庫的地址
        }
    }
}
複製程式碼

apkname.properties為

implementation-class=com.ding.demo.ApkChangeNamePlugin
複製程式碼

ApkChangeNamePlugin

package com.ding.demo

import org.gradle.api.Project
import org.gradle.api.Plugin



class ApkChangeNamePlugin implements Plugin<Project>{

    static  class ChangeAppNameConfig{
        String prefixName
        String notConfig
    }

    static def buildTime() {
        return new Date().format("yyyy_MM_dd_HH_mm_ss", TimeZone.getTimeZone("GMT+8"))
    }

    @Override
    void apply(Project project) {
        if(!project.android){
            throw new IllegalStateException('Must apply \'com.android.application\' or \'com.android.library\' first!');
        }
        project.getExtensions().create("nameConfig",ChangeAppNameConfig)
        ChangeAppNameConfig config
        project.afterEvaluate {
            config = project.nameConfig
        }
        project.android.applicationVariants.all{
            variant ->
                variant.outputs.all {
                    output ->
                        if (output.outputFile != null && output.outputFile.name.endsWith('.apk')
                                && !output.outputFile.name.contains(config.notConfig)) {
                            def appName = config.prefixName
                            def time = buildTime()
                            String name = output.baseName
                            name = name.replaceAll("-", "_")
                            outputFileName = "${appName}-${variant.versionCode}-${name}-${time}.apk"
                        }
                }
        }
    }
}
複製程式碼

定義完成後,執行./gradlew uploadArchives 會在本目錄生成對應對應的外掛

Android Gradle 學習筆記整理

應用外掛 在根build.gralde 配置

buildscript {
    repositories {
        maven {url uri('./repo/')}
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'
        classpath 'com.ding.demo:apkname:0.0.1'
    }
}
複製程式碼

在app.gralde 設定

apply plugin: 'apkname'
nameConfig{
    prefixName = 'demo'
    notConfig = 'debug'
}
複製程式碼

Gradle doc 官網

Gradle的基礎API差不多就介紹完了。

官網地址:docs.gradle.org/current/use…
可以去檢視對應的API,也可以直接通過原始碼的方式檢視

但是筆記還沒完,學習了Gradle的基礎,我們要讓其為我們服務。下面介紹幾個實際應用.

APT 技術

www.jianshu.com/p/94aee6b02…
blog.csdn.net/kaifa1321/a…

APT 全稱Annotation Processing Tool,編譯期解析註解,生成程式碼的一種技術。常用一些IOC框架的實現原理都是它,著名的ButterKnife,Dagger2就是用此技術實現的,SpringBoot中一些注入也是使用他進行注入的.

在介紹APT之前,先介紹一下SPI (Service Provider Interface)它通過在ClassPath路徑下的META-INF/**資料夾查詢檔案,自動載入檔案裡所定義的類。 上面自定義的ApkNamePlugin 就是使用這種機制實現的,如下.

Android Gradle 學習筆記整理
SPI 技術也有人用在了元件化的過程中進行解耦合。

要實現一個APT也是需要這種技術實現,但是谷歌已經把這個使用APT技術重新定義了一個,定義了一個auto-service,可以簡化實現,下面就實現一個簡單Utils的文件生成工具。

Utils文件生成外掛

我們知道,專案中的Utils可能會很多,每當新人入職或者老員工也不能完成知道都有那些Utils了,可能會重複加入一些Utils,比如獲取螢幕的密度,框高有很多Utils.我們通過一個小外掛來生成一個文件,當用Utils可以看一眼文件就很一目瞭然了.

新建一個名為DocAnnotation的Java Libary

定義一個註解

@Retention(RetentionPolicy.CLASS)
public @interface GDoc {
   String name() default "";

   String author() default "";

   String time() default "";
}
複製程式碼

新建一個名為DocComplie 的 Java Libary先

然後引入谷歌的 auto-service,引入DocAnnotation

apply plugin: 'java-library'
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.alibaba:fastjson:1.2.34'
    implementation project(':DocAnnotation')
}
複製程式碼

定義一個Entity類

public class Entity {

    public String author;
    public String time;
    public String name;
}
複製程式碼

定義註解處理器

@AutoService(Processor.class) //其中這個註解就是 auto-service 提供的SPI功能
public class DocProcessor extends AbstractProcessor{

    Writer docWriter;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //可處理的註解的集合
        HashSet<String> annotations = new HashSet<>();
        String canonicalName = GDoc.class.getCanonicalName();
        annotations.add(canonicalName);
        return annotations;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        Messager messager = processingEnv.getMessager();
        Map<String,Entity> map = new HashMap<>();
        StringBuilder stringBuilder = new StringBuilder();
        for (Element e : env.getElementsAnnotatedWith(GDoc.class)) {
            GDoc annotation = e.getAnnotation(GDoc.class);
            Entity entity = new Entity();
            entity.name = annotation.name();
            entity.author = annotation.author();
            entity.time = annotation.time();
            map.put(e.getSimpleName().toString(),entity);

            stringBuilder.append(e.getSimpleName()).append("       ").append(entity.name).append("\n");
        }

        try {
            docWriter = processingEnv.getFiler().createResource(
                    StandardLocation.SOURCE_OUTPUT,
                    "",
                    "DescClassDoc.json"
            ).openWriter();

            //docWriter.append(JSON.toJSONString(map, SerializerFeature.PrettyFormat));
            docWriter.append(stringBuilder.toString());
            docWriter.flush();
            docWriter.close();
        } catch (IOException e) {
            //e.printStackTrace();
            //寫入失敗
        }
        return true;
    }
}
複製程式碼

專案中引用

dependencies {
    implementation project(':DocAnnotation')
    annotationProcessor project(':DocComplie')
}
複製程式碼

應用一個Utils

@GDoc(name = "顏色工具類",time = "2019年09月18日19:58:07",author = "dingxx")
public final class ColorUtils {
}
複製程式碼

最後生成的文件如下:

名稱              功能            作者
ColorUtils      顏色工具類        dingxx
複製程式碼

當然最後生成的文件可以由自己決定,也可以直接是html等.

Android Transform

在說Android Transform之前,先介紹Android的打包流程,在執行task assemble時

Android Gradle 學習筆記整理

在.class /jar/resources編譯的過程中,apply plugin: 'com.android.application' 這個外掛支援定義一個回撥 (com.android.tools.build:gradle:2.xx 以上),類似攔截器,可以進行你自己的一些定義處理,這個被成為Android的Transform

那麼這個時候,可以動態的修改這些class,完成我們自己想幹的一些事,比如修復第三方庫的bug,自動埋點,給第三方庫新增函式執行耗時,完成動態的AOP等等.

我們所知道的 ARoute就使用了這種技術. 當然他是先使用了APT先生成路由檔案,然後通過Transform載入.

以下內容引用自ARoute ReadMe

使用 Gradle 外掛實現路由表的自動載入 (可選)

apply plugin: 'com.alibaba.arouter'
buildscript {
    repositories {
       jcenter()
    }

    dependencies {
        classpath "com.alibaba:arouter-register:?"
    }
}
複製程式碼

可選使用,通過 ARouter 提供的註冊外掛進行路由表的自動載入(power by AutoRegister), 預設通過掃描 dex 的方式 進行載入通過 gradle 外掛進行自動註冊可以縮短初始化時間解決應用加固導致無法直接訪問 dex 檔案,初始化失敗的問題,需要注意的是,該外掛必須搭配 api 1.3.0 以上版本使用!

看ARoute的LogisticsCenter 可以知道,init時,如果沒有使用trasnform的plugin,那麼他將在註冊時,遍歷所有dex,查詢ARoute引用的相關類,如下

//LogisticsCenter
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
  if (registerByPlugin) {
        logger.info(TAG, "Load router map by arouter-auto-register plugin.");
    } else {
        Set<String> routerMap;

        // It will rebuild router map every times when debuggable.
        if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
             logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
            // These class was generated by arouter-compiler.
            routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
            if (!routerMap.isEmpty()) {
                context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP,routerMap).apply();
            }
            PackageUtils.updateVersion(context);    // Save new version name when router map update finishes.
        } else {
            logger.info(TAG, "Load router map from cache.");
            routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
        }
    }
    ....
}
複製程式碼

Android Transform的實現簡介

通過以上,我們知道,回撥的是.class檔案或者jar檔案,那麼要處理.class 檔案或者jar檔案就需要位元組碼處理的相關工具,常用位元組碼處理的相關工具都有

  • ASM
  • Javassist
  • AspectJ

具體的詳細,可以檢視美團的推文 Java位元組碼增強探祕

怎麼定義一個Trasnfrom內,回顧上面的gradle plugin實現,看以下程式碼

public class TransfromPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        AppExtension appExtension = (AppExtension) project.getProperties().get("android");
        appExtension.registerTransform(new DemoTransform(), Collections.EMPTY_LIST);
    }
    
    class DemoTransform extends Transform{

        @Override
        public String getName() {
            return null;
        }

        @Override
        public Set<QualifiedContent.ContentType> getInputTypes() {
            return null;
        }

        @Override
        public Set<? super QualifiedContent.Scope> getScopes() {
            return null;
        }

        @Override
        public boolean isIncremental() {
            return false;
        }
    }
}
複製程式碼

結合位元組碼增加技術,就可以實現動態的一些AOP,由於篇幅原因,這裡就不在詳細把筆記拿出來了,如果想進一步學習,推薦ARoute作者的一個哥們寫的AutoRegister,可以看看原始碼

總結

到這裡Gradle的學習筆記基本整理完成了,由於作者水平有限,如果文中存在錯誤,還請指正,感謝. 也推薦閱讀我的另一篇文章,Android 修圖(換證件照背景,汙點修復)

相關文章