Android Gradle 学习笔记整理

前言

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项目结构

image

如图所示,是一个比较小的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 语法 ,如下

image

可选的类型定义,可以省略语句结束符分号(;)

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里如下

image

解释

进入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用于输出。

image

Task API

我们看到创建的task里面可以直接调用doLast API,那是因为Task类中有doLast API,可以查看对应的代码看到对应的API

image

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的,那么为了复用性更高一些,那我们怎么把这个抽出去呢?

如下

image

其中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",ChangeAppNameConifg)
        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 会在本目录生成对应对应的插件

image

应用插件
在根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差不多就介绍完了。

官网地址:https://docs.gradle.org/current/userguide/userguide.html
可以去查看对应的API,也可以直接通过源码的方式查看

但是笔记还没完,学习了Gradle的基础,我们要让其为我们服务。下面介绍几个实际应用.

APT 技术

https://www.jianshu.com/p/94aee6b02b2b
https://blog.csdn.net/kaifa1321/article/details/79683246

APT 全称Annotation Processing Tool,编译期解析注解,生成代码的一种技术。常用一些IOC框架的实现原理都是它,著名的ButterKnife,Dagger2就是用此技术实现的,SpringBoot中一些注入也是使用他进行注入的.

在介绍APT之前,先介绍一下SPI (Service Provider Interface)它通过在ClassPath路径下的META-INF/**文件夹查找文件,自动加载文件里所定义的类。
上面自定义的ApkNamePlugin 就是使用这种机制实现的,如下.

image

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时

image

在.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 修图(换证件照背景,污点修复)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容