Android组件化实现方案(二)

APT(Annotation Processing Tool),根据注解自动给生成代码。
JavaPoet,代码生成框架。
要自动生成类文件,JavaPoet并不是必须的,比如JavaPoet的Example的一段代码,想要生成如下类文件:

package com.example.helloworld;

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

不使用JavaPoet需要这样写:

JavaFileObject sourceFile = filer.createSourceFile(someFile);
Writer writer = sourceFile.openWriter();
writer.write("package com.example.helloworld;\n");
writer.write("public final class HelloWorld {\n");
writer.write("public static void main(String[] args) {\n");
writer.write("System.out.println(\"Hello, JavaPoet!\");\n");
writer.write("}\n");
writer.write("} \n");              
writer.close();

使用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(someFile);

回到组件化工程,首选要明确希望自动生成的类长什么样子,这里需要的是一个Activity的对应关系Map,比如Order模块的路由这样的:

public class Router$$Path$$app implements RouterLoadPath {
  @Override
  public Map<String, Class> loadPath() {
    Map<String, Class> pathMap = new HashMap<>();
    pathMap.put("MainActivity", MainActivity.class);
    //该模块有几个Activity需要被其他模块访问,都要加进去
    //pathMap.put(...);
    //pathMap.put(...);
    return pathMap;
  }
}

RouterLoadPath接口是为了不需要知道具体类名就可以调用loadPath(),下面会在提到。
这样就可以根据模块名和类名拿到对应的Activity了。

明确目标之后,就要开始APT和JavaPoet的工作了:

  1. 新建Java Library:router_annotation,自定义注解Router:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS) // 要在编译时进行一些预处理操作,注解会在class文件中存在
public @interface Router {

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

    // 路由组名,如:"app"
    String group();
}
  1. 新建注解处理器抹开Java Library:router_compiler,在build.gradle中需要导入三个库
  • 谷歌提供的处理注解的服务库
  • JavaPoet,用于自动生成代码的库
  • 我们自定义的注解模块
apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // 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'

    // 帮助我们通过类调用的形式来生成Java代码
    implementation "com.squareup:javapoet:1.9.0"

    // 引入自定义annotation,处理@Router注解
    implementation project(':router_annotation')
}

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


sourceCompatibility = "7"
targetCompatibility = "7"

  1. 新建Router注解的处理类RouterProcessor,继承自AbstractProcessor:
// AutoService则是固定的写法,加个注解即可
@AutoService(Processor.class)
// 需要处理的注解
@SupportedAnnotationTypes({"com.yu.router_annotation.Router"})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions({"moduleName", "packageNameForAPT"})
public class RouterProcessor extends AbstractProcessor {

    // 该方法主要用于一些初始化的操作,通过该方法的参数ProcessingEnvironment可以获取一些列有用的工具类
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        ...
    }

    // 处理注解的函数
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        ...
        return false;
    }
}

这里的注解@ SupportedOptions要说明一下,这个是该处理器接收的参数,moduleName是模块名,packageNameForAPT是APT生成的文件存放包名,这个参数是在各模块的build.gradle中传递过来的,一会再看,先看init里需要初始化的工作,init的参数ProcessingEnvironment中可以获取一些工具类来处理注解:

    // 操作Element工具类 (类、函数、属性都是Element)
    Elements elementUtils = processingEnvironment.getElementUtils();

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

    // Messager用来报告错误,警告和其他提示信息
    Messager messager = processingEnvironment.getMessager();

    // 文件生成器 类/资源,Filter用来创建新的类文件,class文件以及辅助文件
    Filer filer = processingEnvironment.getFiler();

还需要通过ProcessingEnvironment把moduleNamepackageNameForAPT两个参数获也取出来:

Map<String, String> options = processingEnvironment.getOptions();
moduleName = options.get("moduleName");
packageNameForAPT = options.get("packageNameForAPT");

然后就是process函数了,这里就是需要使用APT的工具,按照JavaPoet的规则去生成我们想要的类了,JavaPoet使用方法,看这里
处理过程:

// 处理注解的函数
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 一旦有类之上使用@Router注解
        if (!EmptyUtils.isEmpty(set)) {
            // 获取所有被 @Router 注解的 元素集合
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Router.class);

            if (!EmptyUtils.isEmpty(elements)) {
                // 解析元素
                try {
                    parseElements(elements);
                    return true;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            // 坑:必须写返回值,表示处理@Router注解完成
            return true;
        }
        return false;
    }

    private void parseElements(Set<? extends Element> elements) throws IOException {

        TypeName methodReturns = ParameterizedTypeName.get(
                ClassName.get(Map.class), // Map
                ClassName.get(String.class), // Map<String,
                ClassName.get(Class.class) // Map<String, RouterBean>
        );
        MethodSpec.Builder methodBuidler = MethodSpec.methodBuilder("loadPath") // 方法名
                .addAnnotation(Override.class) // 重写注解
                .addModifiers(Modifier.PUBLIC) // public修饰符
                .returns(methodReturns); // 方法返回值

        // 初始化Map:Map<String, RouterBean> pathMap = new HashMap<>();
        methodBuidler.addStatement("$T<$T, $T> $N = new $T<>()",
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ClassName.get(Class.class),
                "pathMap",
                HashMap.class);

        // 便利Activity,存入Map
        for (Element element : elements) {

            Router router = element.getAnnotation(Router.class);

            methodBuidler.addStatement(
                    "$N.put($S, $T.class)",
                    "pathMap",
                    router.path(), // "/app/MainActivity"
                    ClassName.get((TypeElement) element) // MainActivity.class

            );

        }
        methodBuidler.addStatement("return $N", "pathMap");

        // 最终生成的类文件名
        String finalClassName = "router$$Path$$" + moduleName;
        messager.printMessage(Diagnostic.Kind.NOTE, "APT生成路由Path类文件:" +
                packageNameForAPT + "." + finalClassName);

        // 生成类文件:Router$$Path$$app
        JavaFile.builder(packageNameForAPT, // 包名
                TypeSpec.classBuilder(finalClassName) // 类名
                        .addSuperinterface(ClassName.get(elementUtils.getTypeElement("com.yu.router_api.RouterLoadPath"))) // 实现RouterLoadPath接口
                        .addModifiers(Modifier.PUBLIC) // public修饰符
                        .addMethod(methodBuidler.build()) // 方法的构建(方法参数 + 方法体)
                        .build()) // 类构建完成
                .build() // JavaFile构建完成
                .writeTo(filer); // 文件生成器开始生成类文件

    }

然后看下上看提到的两个参数,模块名和生成文件的包名,是在各模块的build.gradle中传递过来的,packageNameForAPT在公共的config.gradle中定义:

 // 包名,用于存放APT生成的类文件
    packageNameForAPT = "com.yu.modular.apt"

各模块的build.gradle中添加:

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

生成的文件在对应模块的这个目录下,当然包名是自己定义的:


APT生成的文件路径

到这里APT自动生成的文件也搞完了,但是这里APT生成的Router\$\$Path\$\$模块名的文件是在各自的模块中,在模块不互相引用的情况下不能直接获取,用Class.forName()去取当然没问题,这里在公共库里用一个Manager去管理,做了缓存和懒加载优化:

public class RouterManager {

    private static final String TAG = "RouterManager";
    private static RouterManager instance;

    //Router$$Path$$xxx 类的缓存,防止每次调用Class.forName()、newInstance()
    //结构如:
    //  {
    //    app : Router$$Path$$app对象,
    //    order : Router$$Path$$order对象
    //    ...
    //  }

    private LruCache<String, RouterLoadPath> routerClassCache;

    //目标类缓存,目前为  Activity
    //结构如:
    //  {
    //    app/MainActivity : com.yu.modular.app.MainActivity.class,
    //    order/Order_MainActivity : com.yu.modular.order.Order_MainActivity.class
    //    ...
    //  }
    private LruCache<String, Class> targetClassCache;

    private RouterManager() {
        routerClassCache = new LruCache<>(66);
        targetClassCache = new LruCache<>(66);
    }

    /**
     * 单例
     *
     * @return
     */
    public static RouterManager getInstance() {
        if (instance == null) {
            synchronized (RouterManager.class) {
                if (instance == null) {
                    instance = new RouterManager();
                }
            }
        }
        return instance;
    }


    /**
     * 获取目标类
     *
     * @param groupName
     * @param pathName
     * @return
     */
    public Class get(String groupName, String pathName) {

        Class targetClass = targetClassCache.get(groupName + "/" + pathName);

        if (targetClass == null) {
            RouterLoadPath routerPathClass = getRouterPathClass(groupName);
            Map<String, Class> pathMap = routerPathClass.loadPath();
            targetClass = pathMap.get(pathName);
            Log.e(TAG, "新建目标类文件:" + pathName);
            targetClassCache.put(groupName + "/" + pathName, targetClass);
        }

        return targetClass;

    }


    /**
     * 获取APT生成的类
     *
     * @param groupName
     * @return
     */
    public RouterLoadPath getRouterPathClass(String groupName) {

        try {
            RouterLoadPath routerLoadPath = routerClassCache.get(groupName);
            if (routerLoadPath == null) {
                String groupClassName = "com.yu.modular.apt.Router$$Path$$" + groupName;
                routerLoadPath = (RouterLoadPath) Class.forName(groupClassName).newInstance();
                Log.e(TAG, "新建RouterPath文件:" + groupName);
                routerClassCache.put("groupName", routerLoadPath);
            }
            return routerLoadPath;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

可以愉快的跳转了了:

    public void jumpOrder(View view) {
        Class clazz = RouterManager.getInstance().get("order", "Order_MainActivity");
        Intent intent = new Intent(this, clazz);
        startActivity(intent);
    }

还有一个问题,如果想跨模块访问数据怎么搞?
比如想要在shoppingCar模块中展示Order模块中的图片。当然这些图片资源可以放在公共库,假设这站图片就是属于shoppingCar业务的,只有Order要用一下,其他模块并不需要(也不要太较真,就是用图片做个例子,实现方式同样适用于传递Bean)。这个跟传递Activity原理是一样的,先看实现再分析为什么这么做。

首先在公共库中定义一个接口:

/**
 * 订单模块对外暴露接口,其他模块可以获取返回res资源
 * 具体的实现在Order模块中
 */
public interface OrderDrawable {
    int getDrawable();
}

然后在Order模块中实现,并使用@Router注解:

@Router(group = "order", path = "OrderDrawable")
public class OrderDrawableImpl implements OrderDrawable {
    @Override
    public int getDrawable() {
        return R.mipmap.order_wtf;
    }
}

RouterManager封装一下:

    /**
     * 获取目标类对象
     *
     * @param groupName
     * @param pathName
     * @return
     */
    public Object getResource(String groupName, String pathName) {


        try {
            // 调用获取Activity类的方法,返回newInstance()
            return this.get(groupName, pathName).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;

    }

使用:

OrderDrawable orderDrawable = (OrderDrawable) RouterManager.getInstance().getResource("order", "OrderDrawable");
imageView.setImageResource(orderDrawable.getDrawable());

和Activity不一样的只有在公共库中新建了一个接口,为什么要创建这个接口?
因为两个模块是独立的,即使通过Class.forName("...").newInstance()获取到这个类,也不知道这个类有getDrawable()方法,所以需要公共库里的这个接口。


完事,只是一个实现的简单思路,很多不严谨的地方,比如判空、判断注解格式等等等等,需要封装抽取、优化、拓展还有很多很多空间。
项目地址

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

推荐阅读更多精彩内容