2

博客主页

参考:

gradle组件化配置

Android组件化开发的配置,离不开gradle构建工具,它的出现让工程有无限的可能。gradle核心是基于groovy脚本语言,groovy脚本基于java且扩展了java,所以gradle需要依赖JDK和Groovy库。

gradle简单介绍

gradle语法:从gradle的日志输出开始讲解组件化设计之旅

// 第一种打印字符串的方式
println("hello, gradle!")

// 第二种打印字符串的方式
println "hello2, gradle!"

从这两种字符串输出方式可以看出,方法可以不写括号,一句话后可以不写分号,这是groovy的特性。

可以在Android工程中的build.gradle文件中使用println函数输出日志。然后通过 Build->Toggle view 查看build输出的日志

自定义属性

gradle可以添加额外的自定义属性,通过ext属性实现。先新建一个config.gradle文件,并自定义isRelease属性,用于动态切换:组件化模式/集成化模式

ext {
    // false: 组件化模式(子模块可以独立运行)
    // true :集成化模式(打包整个项目apk,子模块不可独立运行)
    isRelease = true
}

那么这个config文件怎么使用呢?需要在项目的根build.gradle文件通过 apply from 方式引用config.gradle文件

// build.gradle

// 可以通过apply from方式引用
apply from: 'config.gradle'

然后在app应用项目的build.gradle文件中使用自定义属性

// build.gradle

// 使用自定义属性,属性需要写在${}中
println "${rootProject.ext.isRelease}"

config.gradle文件基本配置

再新建一个购物shop库模块,项目结构如下图:

查看app应用模块和shop库模块中的build.gradle文件,发现上图红色框配置很多类似,那么是不是可以使用分模块方式配置呢,使用gradle自定义属性将共性的配置抽取出来,放在单独的文件里,供其他build引用?答案是可以的。

接下来动手操作下,红色框的配置都是跟Android版本有关的,可以定义一个Map集合存相关属性信息,配置如下:

// config.gradle

ext {
    // 定义Map存取版本相关的信息,key名称可以任意取
    versions = [
            compileSdkVersion: 29,
            buildToolsVersion: "29.0.2",
            minSdkVersion    : 21,
            targetSdkVersion : 29,
            versionCode      : 1,
            versionName      : "1.0"
    ]
}

然后在app应用模块和shop库模块中的build.gradle文件,访问Map集合中定义的属性,例如app应用模块build.gradle中访问Map中自定义的属性

// 获取Map
def versions = rootProject.ext.versions

android {
    // 直接通过map.key访问值
    compileSdkVersion versions.compileSdkVersion
    buildToolsVersion versions.buildToolsVersion
    defaultConfig {
        applicationId "com.example.modular.todo"
        minSdkVersion versions.minSdkVersion
        targetSdkVersion versions.targetSdkVersion
        versionCode versions.versionCode
        versionName versions.versionName
    }
}

1. applicationId配置
在组件化模块与集成化模块做切换,组件化模块为了能够独立运行,将库模块切换成应用模块时需要设置applicationId

所以还需要配置不同的applicationId属性

// config.gradle

ext {
    // 组件化与集成化切换时,设置不同的applicationId
    appId = [
            app : "com.example.modular.todo",
            shop: "com.example.modular.shop"
    ]
}

在app应用模块中的build.gradle文件中使用

def appId = rootProject.ext.appId
android {
    defaultConfig {
        applicationId appId.app
    }
}

2. 代码中生产和正式环境配置切换
有的时候还需要在代码中切换生产和正式环境配置,如:网络请求的URL。Android为我们提供自定义BuildConfig功能。

// config.gradle

ext {
    baseUrl = [
            debug  : "https://127.0.0.1/debug", // 测试版本URL
            release: "https://127.0.0.1/relase" // 正式版本URL
    ]
}

在app应用模块build.gradle文件中通过buildConfigField配置,在代码中就可以通过BuildConfig.baseUrl访问到了。

// build.gradle

def baseUrl = rootProject.ext.baseUrl
android {
    buildTypes {
        debug {
            // void buildConfigField(
            //            @NonNull String type,
            //            @NonNull String name,
            //            @NonNull String value) { }
            buildConfigField("String", "baseUrl", "\"${baseUrl.debug}\"")
        }

        release {
            buildConfigField("String", "baseUrl", "\"${baseUrl.release}\"")
        }
    }
}

3. dependencies依赖配置

// config.gradle

ext {
    appcompatVersion = "1.0.2"
    constraintlayoutVersion = "1.1.3"
    dependencies = [
            appcompat       : "androidx.appcompat:appcompat:${appcompatVersion}",
            constraintlayout: "androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}",
    ]

    tests = [
            "junit"        : "junit:junit:4.12",
            "espresso"     : "androidx.test.espresso:espresso-core:3.1.1",
            "androidJunit": "androidx.test.ext:junit:1.1.0"
    ]
}

app模块中build.gradle文件引用

// build.gradle

def supports = rootProject.ext.dependencies
def tests = rootProject.ext.tests

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // 标准写法
    // implementation group: 'androidx.appcompat', name: 'appcompat', version: '1.0.2'
//    implementation supports.appcompat
//    implementation supports.constraintlayout

    // supports 依赖
    supports.each { key, value -> implementation value }

    testImplementation tests.junit
    androidTestImplementation tests.espresso
    androidTestImplementation tests.androidJunit
}

4. 签名配置
签名配置时需要注意:signingConfigs 必须写在buildTypes之前

android {
    // 签名配置(隐形坑:必须写在buildTypes之前)
    signingConfigs {
        debug {
            // 天坑:填错了,编译不通过还找不到问题
            storeFile file('/Users/xujinbing839/.android/debug.keystore')
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
        release {
            // 签名证书文件
            storeFile file('/Users/xujinbing839/work/mycode/todo/todo_modular/keystore/modular')            
            storeType "modular" // 签名证书的类型            
            storePassword "123456" // 签名证书文件的密码        
            keyAlias "modular" // 签名证书中密钥别名  
            keyPassword "123456" // 签名证书中该密钥的密码
            v2SigningEnabled true // 是否开启V2打包
        }
    }

    buildTypes {
        debug {
            // 对构建类型设置签名信息
            signingConfig signingConfigs.debug
        }

        release {
            // 对构建类型设置签名信息
            signingConfig signingConfigs.release
        }
    }
}

其它配置

android {
    defaultConfig {
        // 开启分包
        multiDexEnabled true
        // 设置分包配置
        // multiDexKeepFile file('multidex-config.txt')

        // 将svg图片生成 指定维度的png图片
        // vectorDrawables.generatedDensities('xhdpi','xxhdpi')
        // 使用support-v7兼容(5.0版本以上)
        vectorDrawables.useSupportLibrary = true
        // 只保留指定和默认资源
        resConfigs('zh-rCN')

        // 配置so库CPU架构(真机:arm,模拟器:x86)
        // x86  x86_64  mips  mips64
        ndk {
            //abiFilters('armeabi', 'armeabi-v7a')
            // 为了模拟器启动
            abiFilters('x86', 'x86_64')
        }
    }

    // AdbOptions 可以对 adb 操作选项添加配置
    adbOptions {
        // 配置操作超时时间,单位毫秒
        timeOutInMs = 5 * 1000_0

        // adb install 命令的选项配置
        installOptions '-r', '-s'
    }
    // 对 dx 操作的配置,接受一个 DexOptions 类型的闭包,配置由 DexOptions 提供
    dexOptions {
        // 配置执行 dx 命令是为其分配的最大堆内存
        javaMaxHeapSize "4g"
        // 配置是否预执行 dex Libraries 工程,开启后会提高增量构建速度,不过会影响 clean 构建的速度,默认 true
        preDexLibraries = false
        // 配置是否开启 jumbo 模式,代码方法是超过 65535 需要强制开启才能构建成功
        jumboMode true
        // 配置 Gradle 运行 dx 命令时使用的线程数量
        threadCount 8
        // 配置multidex参数
        additionalParameters = [
                '--multi-dex', // 多dex分包
                '--set-max-idx-number=50000', // 每个包内方法数上限
                // '--main-dex-list=' + '/multidex-config.txt', // 打包到主classes.dex的文件列表
                '--minimal-main-dex'
        ]
    }
    // 执行 gradle lint 命令即可运行 lint 检查,默认生成的报告在 outputs/lint-results.html 中
    lintOptions {
        // 遇到 lint 检查错误会终止构建,一般设置为 false
        abortOnError false
        // 将警告当作错误来处理(老版本:warningAsErros)
        warningsAsErrors false
        // 检查新 API
        check 'NewApi'
    }
}

组件化详细部署

组件化开发的意义

什么是组件化开发?
组件化开发就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。

组件化和插件化开发略有不同:
插件化开发时将整个app拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk分开打包。

为什么要组件化呢?
1. 开发需求
不相互依赖、可以相互交互、任意组合、高度解耦
2. 团队效率

library与application区别、切换

Phone Module 和 Android Library区别:

Phone Module是一个可以独立运行,编译成一个apk,且build.gradle文件中需要配置applicationId;而Android Library 不能独立运行,不能单独编译成一个apk,且build.gradle文件中不需要配置applicationId。

Phone Module 和 Android Library切换:

下面以子模块购物shop为例:

如果购物shop模块能够独立编译apk,就需要切换为Phone Module(也就是组件化模式),通过isRelease动态切换:

  1. 当isRelease为true:集成化模式(也就是可打包整个项目apk),子模块购物shop不可独立运行
  2. 当isRelease为false:组件化模式,子模块购物shop可以独立运行

其中isRelease变量是config.gradle中定义的属性

// build.gradle

if (isRelease) { // 如果是发布版本时,各个模块都不能独立运行
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

android {
    defaultConfig {
        // 如果是集成化模式,不能有applicationId
        if (!isRelease) applicationId appId.shop
    }
}

当子模块购物shop从library模块切换为application模块时,可能需要编写测试代码,如:启动的入口。

使用sourceSets配置,将测试的代码放入debug文件夹中,当切换到集成化模式时,打包成apk时移除所有的debug代码(也就是debug代码不会打包到apk中)。

android {
    // 配置资源路径,方便测试环境,打包不集成到正式环境
    sourceSets {
        main {
            if (!isRelease) {
                // 如果是组件化模式,需要单独运行时
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                // 集成化模式,整个项目打包apk
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    // release 时 debug 目录下文件不需要合并到主工程
                    exclude '**/debug/**'
                }
            }
        }
    }
}

组件化开发规范:

  1. 子模块购物shop:在src类和res资源命令中添加前缀,shop_,如:shop_activity_home
  2. app模块:可以不修改,默认

Module与Module之间交互

Module间怎么交互(包括:跳转、传参等)?方式有很多:

  1. EventBus 非常混乱,难以维护
  2. 反射 反射技术可以成功,但是维护成本较高,且出现高版本的@hide限制
  3. 隐式意图 维护成本还好,就是比较麻烦,需要维护Manifest的action
  4. BroadCastReceiver 需要动态注册(7.0后),需求方发送广播
  5. 类加载 需要准确的全类名路径,维护成本较高且容易出现人为失误

第一种实现方案:类加载技术交互

通过Class的forName加载目标类,需要准确知道目标类的全类名路径。

private void jump() {
    try {
        Class<?> targetClass = Class.forName("com.example.modular.shop.ShopActivity");

        Intent intent = new Intent(this, targetClass);
        intent.putExtra("moduleName","app");
        startActivity(intent);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

第二种实现方案:全局Map记录路径信息

要跳转,如果知道了目标类的Class对象,不就可以跳转了,接下来只需要解决目标类的Class对象查找就可以了。

可以定义一个PathBean用于封装目标类的相关信息,如:目标类的全路径名,目标类的Class对象

public class PathBean {

    public String path; // 跳转目标全类名
    public Class<?> targetClass; // 跳转目标类的Class对象

    public PathBean(String path, Class<?> targetClass) {
        this.path = path;
        this.targetClass = targetClass;
    }
}

一个模块中会有很多PathBean,可以List存取PathBean,而又有很多模块,可以使用Map区分不同的模块。

 // key:模块名,如shop模块  value:该模块下所有的Activity路径信息
 private static final Map<String, List<PathBean>> mGroupMap = new HashMap<>();

mGroupMap是一个Map,key是模块名,如:shop模块;value是该模块下所有的Activity路径信息.

将所有的路径信息加入到mGroupMap中,使用时通过根据组名(模块名)和路径名获取目标类对象。

/**
 * 将路径信息加入到全局的Map中
 */
public static void addGroup(String groupName, String path, Class<?> targetClass) {
    List<PathBean> pathBeans = mGroupMap.get(groupName);
    if (pathBeans == null) {
        pathBeans = new ArrayList<>();
        pathBeans.add(new PathBean(path, targetClass));
        mGroupMap.put(groupName, pathBeans);
    } else {
        pathBeans.add(new PathBean(path, targetClass));
    }
}

/**
 * 根据组名和路径名获取目标类
 */
public static Class<?> findTargetClass(String groupName,String path) {
    List<PathBean> pathBeans = mGroupMap.get(groupName);
    if (pathBeans != null) {
        for (PathBean pathBean : pathBeans) {
            if (!TextUtils.isEmpty(path) && path.equalsIgnoreCase(pathBean.path)) {
                return pathBean.targetClass;
            }
        }
    }
    return null;
}

APT介绍和使用

APT介绍

APT(Annotation Processing Tools)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotaion,使用Annotation进行额外的处理。Annotation处理器在处理Annotation时可以根据源文件中的Annotaion生成额外的源文件和其它的文件(文件具体内容由Annotaion处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

通俗的理解:根据规则,帮助我们生成代码,生成类文件。

1. APT核心实现原理

编译时Annotation解析的基本原理是,在某些代码元素上(如类型、函数、字段等)添加注解,在编译时javac编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如:根据注解生成新的java类,这也就是ARouter、Butterknife、Dragger等开源库的基本原理。

2. java源文件编程层Class文件

工具是通过javac工具,注解处理器是一个在javac中的,用来编译时扫描和处理的注解工具。可以认为是特定的注解,注册你自己的注解处理器。

3. 怎么注册注解处理器
MyProcessor到javac中。你必须提供一个.jar文件。就像其它.jar文件一样,你打包你的注解处理器到此文件中。并且,在你的jar中,你需要打包一个特定的文件javax.annotation.processing.Processor到META-INF/services路径下。

知识点详解

1. jar

  • com.google.auto.service:auto-service 谷歌提供的java生成源代码库
  • com.squareup:javapoet 提供了各种API让你用各种姿势去生成java代码文件

2. @AutoService

这是一个其它注解处理器中引入的注解。AutoService注解处理器是Google开发的,用来生成META-INF/services/javax.annotation.processing.Processor文件的。我们可以在注解处理器中使用注解。非常方便

3. 结构体语言

对于java源文件来说,也是一种结构体语言。JDK中提供了用来描述结构体语言元素的接口。

在注解处理过程中,我们扫描所有的java源文件。源代码的每一部分都是一个特定类型的Element。换句话说:Element代表程序的元素,例如包、类或者方法。每个Element代表一个静态的、语言级别的构件。

package com.example; // PackageElement

public class Foo { // TypeElement
    
   private int a; // VariableElement
   private Foo other; // VariableElement

   public Foo {} // ExecutableElement

   public void setA( // ExecutableElement
      int newA // VariableElement ) {}
}

Element程序元素

PackageElement 表示包程序元素。

TypeElement 表示一个类或接口程序元素。

ExecutableElement 表示类或接口的方法,构造函数或初始化器(静态或实例),包括注释类型元素。

VariableElement 表示一个字段, 枚举常量,方法或构造函数参数,局部变量,资源变量或异常参数。

TypeParameterElement 表示通用类,接口,方法或构造函数元素的正式类型参数。

Types
一个用来处理TypeMirror的工具类

Filer
使用Filer你可以创建java文件

AbstractProcessor核心API

1. process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

相当于每个处理器的主函数main()。可以在这里写你的扫描、评估和处理注解的代码,以及生成java文件。输入参数RoundEnvironment,可以让你查询出包含特定注解的被注解元素。

2. getSupportedAnnotationTypes()

这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。

3. getSupportedSourceVersion()

用来指定你使用的java版本。通常这里返回SourceVersion.latestSupported()。如果你有足够的理由只支持java6的话,你也可以返回SourceVersion.RELEASE_6

4. getSupportedOptions()

用来指定注解处理器处理的选项参数。需要在gradle文件中配置选项参数值

// 在gradle文件中配置选项参数值(用于APT传参接收)
// 切记:必须写在defaultConfig节点下
javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName: project.getName()]
    }
}

这些API也可以使用注解的方式指定:

// AutoService则是固定的写法,加个注解即可
// 通过auto-service中的@AutoService可以自动生成AutoService注解处理器,用来注册
// 用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)
// 允许/支持的注解类型,让注解处理器处理(新增annotation module)
@SupportedAnnotationTypes({"com.example.modular.annotations.ARouter"})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions("moduleName")
public class ARouterProcessor extends AbstractProcessor {
  // ignore
}

ProcessingEnvironment核心API

// ignore
public class ARouterProcessor extends AbstractProcessor {
   // 操作Element工具类 (类、函数、属性都是Element)
    private Elements elementUtils;

    // type(类信息)工具类,包含用于操作TypeMirror的工具方法
    private Types typeUtils;

    // Messager用来报告错误,警告和其他提示信息
    private Messager messager;

    // 文件生成器 类/资源,Filter用来创建新的源文件,class文件以及辅助文件
    private Filer filer;

    // 模块名,通过getOptions获取build.gradle传过来
    private String moduleName;

    // 该方法主要用于一些初始化的操作,通过该方法的参数ProcessingEnvironment可以获取一些列有用的工具类
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // processingEnv是父类受保护属性,可以直接拿来使用。
        // 其实就是init方法的参数ProcessingEnvironment
        // processingEnv.getMessager(); //参考源码64行
        elementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
        typeUtils = processingEnvironment.getTypeUtils();

       // 通过ProcessingEnvironment去获取build.gradle传过来的参数
        Map<String, String> options = processingEnvironment.getOptions();
        if (options != null && !options.isEmpty()) {
            moduleName = options.get("moduleName");
            // 有坑:Diagnostic.Kind.ERROR,异常会自动结束,不像安卓中Log.e那么好使
            messager.printMessage(Diagnostic.Kind.NOTE, "moduleName=" + moduleName);
        }
    }
}

RoundEnvironment 核心API

// ignore
public class ARouterProcessor extends AbstractProcessor {
      /**
     * 相当于main函数,开始处理注解
     * 注解处理器的核心方法,处理具体的注解,生成Java文件
     *
     * @param set              使用了支持处理注解的节点集合(类 上面写了注解)
     * @param roundEnvironment 当前或是之前的运行环境,可以通过该对象查找找到的注解。
     * @return true 表示后续处理器不会再处理(已经处理完成)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.isEmpty()) return false;

        // 获取所有带ARouter注解的 类节点
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
        // 遍历所有类节点
        for (Element element : elements) {
            // ignore
        }
        // ignore
        return true;
    }
}

Element核心API

APT使用

开发环境的兼容

https://github.com/google/auto

1. Android Studio 3.2.1 + Gradle 4.10.1 临界版本

dependencies {
     // 注册注解,并对其生成META-INF的配置信息,rc2在gradle5.0后有坑
     // As-3.2.1 + gradle4.10.1-all + auto-service:1.0-rc2
     implementation 'com.google.auto.service:auto-service:1.0-rc2'
}

2. Android Studio 3.4.1 + Gradle 5.1.1 向下兼容

dependencies {
    // As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
}

使用APT技术帮助我们生成代码

1. ARouter注解

新建java library工程,工程名为:annotations。然后创建ARouter注解

/**
 * <ul>
 * <li>@Target(ElementType.TYPE)   // 接口、类、枚举、注解</li>
 * <li>@Target(ElementType.FIELD) // 属性、枚举的常量</li>
 * <li>@Target(ElementType.METHOD) // 方法</li>
 * <li>@Target(ElementType.PARAMETER) // 方法参数</li>
 * <li>@Target(ElementType.CONSTRUCTOR)  // 构造函数</li>
 * <li>@Target(ElementType.LOCAL_VARIABLE)// 局部变量</li>
 * <li>@Target(ElementType.ANNOTATION_TYPE)// 该注解使用在另一个注解上</li>
 * <li>@Target(ElementType.PACKAGE) // 包</li>
 * <li>@Retention(RetentionPolicy.RUNTIME) <br>注解会在class字节码文件中存在,jvm加载时可以通过反射获取到该注解的内容</li>
 * </ul>
 *
 * 生命周期:SOURCE < CLASS < RUNTIME
 * 1、一般如果需要在运行时去动态获取注解信息,用RUNTIME注解
 * 2、要在编译时进行一些预处理操作,如ButterKnife,用CLASS注解。注解会在class文件中存在,但是在运行时会被丢弃
 * 3、做一些检查性的操作,如@Override,用SOURCE源码注解。注解仅存在源码级别,在编译的时候丢弃该注解
 */
@Target(ElementType.TYPE) // 该注解作用在类之上
@Retention(RetentionPolicy.CLASS) // 要在编译时进行一些预处理操作。注解会在class文件中存在
public @interface ARouter {

    // 详细路由路径(必填),如:"app/MainActivity"
    String path();

    // 路由组名(选填,如果不填写,可以从path中截取)
    String group() default "";
}

2. 自定义ARouterProcessor注解处理器
新建java library工程,工程名为:compiler。创建ARouterProcessor类继承AbstractProcessor。

为了使用APT技术生成代码,首先要设计我们想生成的代码模版,下面的代码是由APT生成的。

package com.example.modular.shop;

public class ARouter$$ShopActivity {
    public static Class<?> findTargetClass(String path) {
        if (path.equals("/shop/ShopActivity")) {
            return ShopActivity.class;
        }
        return null;
    }
}

在ARouterProcessor类中实现process方法,处理支持的注解,生成我们想要的代码。

/**
 *
 * 相当于main函数,开始处理注解
 *  注解处理器的核心方法,处理具体的注解,生成Java文件
 *
 * @param set              支持注释类型的集合,如:@ARouter注解
 * @param roundEnvironment 当前或是之前的运行环境,可以通过该对象查找找到的注解
 * @return true 表示后续处理器不会再处理(已经处理完成)
 */
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // set集合,就是支持的注解集合,如:ARouter注解
    if (set.isEmpty()) return false;

    // 获取所有被@ARouter注解注释的元素
    Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(ARouter.class);

    if (elementsAnnotatedWith != null && !elementsAnnotatedWith.isEmpty()) {

        for (Element element : elementsAnnotatedWith) {

            // 通过类节点获取包节点,(全路径名,如:com.example.modular.shop)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();

            // 获取被@ARouter注解的简单类名
            String simpleName = element.getSimpleName().toString();

            // 注: 包名:com.example.modular.shop 被注解的类名:ShopActivity
            messager.printMessage(Diagnostic.Kind.NOTE, "包名:" + packageName + " 被注解的类名:" + simpleName);

            // 最终生成的类文件名
            String finalClassName = "ARouter$$" + simpleName;
            try {
                // 创建一个新的源文件并返回一个JavaFileObject对象以允许写入它
                JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + finalClassName);

                // 获取此文件对象的Writer,开启写入功能
                Writer writer = sourceFile.openWriter();

                writer.write("package " + packageName + ";\n");
                writer.write("public class " + finalClassName + " {\n");
                writer.write("public static Class<?> findTargetClass(String path) {\n");

                // 获取ARouter注解
                ARouter aRouter = element.getAnnotation(ARouter.class);

                writer.write("if (path.equals(\"" + aRouter.path() + "\")) {\n");

                writer.write("return " + simpleName + ".class;\n");

                writer.write("}\n");

                writer.write("return null;\n");

                writer.write("}\n}");

                // 关闭写入流
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return true;
}

使用时要在build.gradle文件添加依赖,如购物shop模块中

dependencies {
    implementation project(path: ':ARouter:annotations')
    annotationProcessor project(path: ':ARouter:compiler')
}

然后在ShopActivity类上添加ARouter注解

@ARouter(path = "/shop/ShopActivity")
public class ShopActivity extends AppCompatActivity {}

最后build -> Make Project,就会生成我们想要的代码。

注意:如果出现中文乱码,在build.gradle中添加如下配置:

// java控制台输出中文乱码
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

APT + javapoet

javapoet是square公司推出的开源java代码生成框架,提供java api生成.java源文件。这个框架非常实用,也是我们习惯的java面向对象OOP语法。可以很方便的使用它根据注解生成对于的代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式替代繁琐冗杂的重复工作。

项目主页及源码
https://github.com/square/jav...

依赖javapoet库

dependencies {
    // 帮助我们通过类调用的方式来生成java代码
    implementation 'com.squareup:javapoet:1.11.1'
}

javapoet 8个常用的类

先来看下javapoet官网提供的一个简单的例子,生成HelloWorld类

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

使用javapoet生成上面这段代码:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

javapoet jar中提供了8个常用的类

javapoet字符串格式化规则

$L  字面量,如:"int value=$L", 10
$S  字符串,如:$S, "hello"
$T  类、接口,如:$T, MainActivity
$N  变量,如:user.$N, name

接下来还是生成ARouter$$ShopActivity

package com.example.modular.shop;

public class ARouter$$ShopActivity {
    public static Class<?> findTargetClass(String path) {
        return path.equals("/shop/ShopActivity") ? ShopActivity.class : null;
    }
}

使用javapoet生成ARouter$$ShopActivity

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // 获取所有被@ARouter注解注释的元素
    Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(ARouter.class);

    if (elementsAnnotatedWith != null && !elementsAnnotatedWith.isEmpty()) {

        for (Element element : elementsAnnotatedWith) {

            // 通过类节点获取包节点,(全路径名,如:com.example.modular.shop)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();

            // 获取被@ARouter注解的简单类名
            String simpleName = element.getSimpleName().toString();

            // 注: 包名:com.example.modular.shop 被注解的类名:ShopActivity
            messager.printMessage(Diagnostic.Kind.NOTE, "包名:" + packageName + " 被注解的类名:" + simpleName);

            // 最终生成的类文件名
            String finalClassName = "ARouter$$" + simpleName;

            ARouter aRouter = element.getAnnotation(ARouter.class);

            ClassName targetClassName = ClassName.get((TypeElement) element);
            // 构建方法体
            MethodSpec findTargetClass = MethodSpec.methodBuilder("findTargetClass")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(Class.class) // 返回值Class<?>
                    .addParameter(String.class, "path")
                    .addStatement("return path.equals($S) ? $T.class : null", aRouter.path(), targetClassName)
                    .build();

            // 构建类
            TypeSpec finalClass = TypeSpec.classBuilder(finalClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(findTargetClass)
                    .build();

            // 生成文件
            JavaFile javaFile = JavaFile.builder(packageName, finalClass)
                    .build();

            try {
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return true;
}

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。