流行框架源码分析(3)-编译期注解的使用例子

主目录见:Android高级进阶知识(这是总目录索引)
 我们在开发的时候为了提高效率往往会选择一个基于注解的框架,但是有时使用反射通常被认为是性能的收割机,所以我们会青睐编译期注解的使用,其实早在前面我们分析了[EventBus3.0源码解析]中我们就有看到,还有我们接下来要讲的ButterKnife也会用到,当然我今天要用来讲的例子[LRouter]这个项目也会使用这个。

一.目标

 现在编译期注解这么火热,我们有理由要去学习一下,我们今天这篇文章是为了扫盲一下,使得我们下面分析这类型的框架更加得心应手。所以今天目标是:
1.自己能编写一个编译期注解项目;
2.在实际开发中能使用到这项技术;
3.能学习此类型的框架的源码,然后收为己用。

二.例子编写

在编写这个框架之前,我们需要一些准备,因为此类框架需要的模块有点多:


目录结构

因为这是个完整的项目,我们只是挑出其中的编译期注解部分来讲:

  • lrouter-annotation:用于放注解部分,是个java的模块
  • lrouter-compiler:用于编写注解处理器,是个java模块
  • lrouter-api:用于提供用户使用的api的,是个android模块
  • app:这个是使用的实例,注解会在这里使用,也是android模块

当然了,目录不一定就是要分这么多个,大家可以根据自己的需要,有时候还可以进行合并。当然这些模块之间是有模块依赖的:

lrouter-compiler依赖lrouter-annotation模块

在使用这个注解和api的时候,我们也要添加一些依赖:

lrouter-api依赖lrouter-annotation,app依赖lrouter-api和lrouter-annotation

当然这里的lrouter-api在这里我们使用到的也不多,因为这是项目会使用到的api,具体使用可以查看github上面的说明。

1.注解模块lrouter-annotation实现

注解模块就是单纯地放一些注解,没有其他的东西:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Action {
    String name();
    String provider();
}

我们这里举其中一个注解来看看,因为我们是编译期的注解所以我们设置保留策略为CLASS即编译期注解(当然如果用反射的话Runtime或者还有一个source用的不多),并且说明我们注解是使用在类上面(还可以说明在Field或者Method等待上面),然后这里因为需要一个name和一个provider其中的值为String类型。

你如果需要几个注解就跟这个一样,只要设置上响应的Target和Retention即可。

2.注解处理器lrouter-compiler的实现

这块内容应该是编译期注解的核心了吧,不过放心,也不会太难,步骤是很固定的,在这里我们会使用一个auto-service库以及后面用来生成代码的javapoet:

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':lrouter-annotation')
}

auto-service可以帮我们去生成META-INF信息:

META-INF信息

接着我们就可以编写注解处理器的核心代码了。

2.1.ProviderActionProcessor实现

要实现注解处理器我们要实现一个类继承AbstractProcessor:

@AutoService(Processor.class)
public class ProviderActionProcessor extends AbstractProcessor{
}

注解处理器里面包含几个重要的方法:

init() 
初始化,得到Elements、Types、Filer等工具类
getSupportedAnnotationTypes() 
描述注解处理器需要处理的注解
getSupportedSourceVersion()
处理器使用的java版本
process() 
扫描分析注解,生成代码

首先我们会复写init()方法,该方法里面会有ProcessingEnvironment参数,我们可以根据这个来初始化一些辅助类:

    private Filer mFileUtils;//跟文件相关的类,用于生成java源代码的
    private Elements mElementUtils;//元素相关的类,可以理解为获取代码中的信息
    private Messager mMessager;//是用来打印日志的跟Logger类似

我们重点要说一下Elements,他有几个子类,我们来说明一下:

public class ClassA { // TypeElement
    private int var_0; // VariableElement
    public ClassA() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}

我们看到类为TypeElement,变量为VariableElement,方法为ExecuteableElement。到这里我们对Element有个简单的认知,然后我们继续重写getSupportedAnnotationTypes()方法:

 @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationTypes = new LinkedHashSet<>();
        annotationTypes.add(Action.class.getCanonicalName());
        annotationTypes.add(Provider.class.getCanonicalName());
        annotationTypes.add(Service.class.getCanonicalName());
        annotationTypes.add(Application.class.getCanonicalName());
        annotationTypes.add(IntentInterceptor.class.getCanonicalName());
        annotationTypes.add(Interceptor.class.getCanonicalName());
        return annotationTypes;
    }

这里我们把我们支持的注解都添加进来,然后我们复写另外一个方法getSupportedSourceVersion():

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

这个里面我们直接返回我们处理器支持的最新的java版本。接着我们就来实现比较复杂部分的process方法。

2.2 process()实现

process方法比较复杂,因为我们主要的代码逻辑都在这里面,一般这个方法的步骤有两个:
 1.收集信息
 2.生成源代码
什么叫收集信息呢?就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的。

在这个例子中,我们会针对一个注解生成一个类,例如我们Action在app中使用的时候会生成一个MainAction$$Inject类:


自动生成的代码

然后我们来看怎样一步一步生成这个类的。
1)收集信息

   @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mProviderMap.clear();
        mActionMap.clear();
        mServiceMap.clear();
        mApplicaitonMap.clear();
        mIntentInterceptMap.clear();
        mInterceptMap.clear();

        if (!annotations.isEmpty()) {
            Set<? extends Element> elesWithAction = roundEnv.getElementsAnnotatedWith(Action.class);
            Set<? extends Element> elesWithProvider = roundEnv.getElementsAnnotatedWith(Provider.class);
            Set<? extends Element> elesWithService = roundEnv.getElementsAnnotatedWith(Service.class);
            Set<? extends Element> elesWithApplication = roundEnv.getElementsAnnotatedWith(Application.class);
            Set<? extends Element> elesWithIntentInterceptor = roundEnv.getElementsAnnotatedWith(IntentInterceptor.class);
            Set<? extends Element> elesWithInterceptor = roundEnv.getElementsAnnotatedWith(Interceptor.class);
...........
                    return true;
        }
        return false;
    }

我们看到这里会获取到所有的使用Action等注解的元素集合,我们看到这里会先调用clear()方法清除掉map中的数据,因为process()方法有可能会调用多次,为了避免生成重复的源代码,我们这里做一下清理工作。获取到所有的注解的元素集合了我们就可以生成源代码了。
2)生成代码

   @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//省略收集信息代码
 ................
            try {
                generateProviderHelper(elesWithProvider);
                generateActionHelper(elesWithAction);
                generateServiceHelper(elesWithService);
                generateApplicationHelper(elesWithApplication);
                generateIntentInterceptorHelper(elesWithIntentInterceptor);
                generateInterceptorHelper(elesWithInterceptor);
            } catch (Exception e) {
               e.printStackTrace();
            }
            return true;
        }
        return false;
    }

我们看到代码会分别生成对应的注解的源代码,我们这里挑一个Action的生成代码方法来看:

    private void generateActionHelper(Set<? extends Element> elesWithAction) throws Exception{
//遍历添加了Action注解的元素集合
        for (Element element : elesWithAction){
//因为我们action是放在类上面的注解,所以我们这里会进行检查一下,而且这里类不能为private的
            checkAnnotationValid(element, Action.class);

            TypeElement  classElement = (TypeElement) element;
            //full class name
            String actionClassName = classElement.getQualifiedName().toString();

            ProxyInfo proxyInfo = mActionMap.get(actionClassName);
            if (null == proxyInfo){
//构造proxyInfo对象,里面主要用于存放注解标注的类信息与生成类信息
                proxyInfo = new ProxyInfo(mElementUtils, classElement);
                mActionMap.put(actionClassName, proxyInfo);
            }
//同时将action注解里面的name和provider取出设置给proxyinfo
            Action actionAnnotation = classElement.getAnnotation(Action.class);
            proxyInfo.setName(actionAnnotation.name());
            proxyInfo.setProvider(actionAnnotation.provider());
        }
//有了完整的信息之后就遍历出来生成源代码
        for (String key : mActionMap.keySet()) {
            ProxyInfo proxyInfo = mActionMap.get(key);
//这个是用javapoet生成的,这个是方法的生成
            MethodSpec.Builder initBuilder = MethodSpec.methodBuilder(METHOD_NAME)
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(TypeName.VOID);
//这个是方法里面的代码生成
            initBuilder.addStatement("$T $N = $T.getInstance($T.getInstance()).findProvider($S)",
                    LRProviderClass,LRProviderClass.simpleName().toLowerCase(),LRouterClass,LRouterApplicationClass,proxyInfo.getProvider());
            initBuilder.addCode("if(null != $N){\n",LRProviderClass.simpleName().toLowerCase());
            initBuilder.addCode("$N.registerAction($S,new $T());\n}\n",
                    LRProviderClass.simpleName().toLowerCase(),proxyInfo.getName(),ClassName.get(proxyInfo.typeElement));
//这个是类的生成
            TypeSpec actionInject = TypeSpec.classBuilder(proxyInfo.proxyClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(InjectorClass)
                    .addMethod(initBuilder.build())
                    .build();
            JavaFile javaFile = JavaFile.builder(PACKAGE_NAME,actionInject).build();
            javaFile.writeTo(mFileUtils);
        }
    }

我们看到代码比较长,其实逻辑不怎么难,我们都在关键地方有注释,代码其实就是获取到使用了注解的类的信息,然后再用javapoet来生成代码,关于[javapoet]我们这里不做详细的说明,有兴趣可以去这个github上面看看怎么使用。

3.注解在例子中的使用

我们前面编写了注解和注解生成器,这样我们就可以来使用这个注解来生成我们的源代码了,使用注解很简单,跟我们平常是一样的,我们来看下:

@Action(name = "main",provider = "main")
public class MainAction extends LRAction {//动作的执行
}

我们看到注解的使用非常简单,因为这个注解是用在类上面的,所以我们用法如上面所示,当然在编写这个之前我们还要配置一下我们的gradle文件。首先在工程目录下的gradle文件下添加:

    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

然后在当前模块下的gradle文件添加:

annotationProcessor project(':lrouter-compiler')

这样的话,我们只要编译一下工程,就会在当前模块的build——generated——source——apt——debug下生成我们的源代码了。

4.生成的源码的使用

生成完源码了,我们这里在lrouter-api中进行使用,我们这里会用到一点反射,跟大家理解的可能不大一样,不是说我们用到编译期注解就一点反射不会使用,这是不正确的,有可能会我们还是需要用到一点反射,有时我们会用到缓存来减少性能的消耗。我们现在看lrouter-api下的PackageScanner类:

public class PackageScanner {
    /**
     * 扫描
     * */
    public static List<InjectorPriorityWrapper> scan(Context ctx){
        List<InjectorPriorityWrapper> clazzs = new ArrayList<>();
        try{
            PathClassLoader classLoader = (PathClassLoader) Thread
                    .currentThread().getContextClassLoader();

            DexFile dex = new DexFile(ctx.getPackageResourcePath());
            Enumeration<String> entries = dex.entries();
            while (entries.hasMoreElements()) {
                String entryName = entries.nextElement();
                if (entryName.contains("com.lenovohit")){//过滤掉系统的类
                    Class<?> entryClass = Class.forName(entryName, false,classLoader);
                    if (entryName.contains("Provider$$Inject")){
                        clazzs.add(new InjectorPriorityWrapper(InjectorPriorityWrapper.PROVIDER_PRIORITY,entryClass));
                    }else if (entryName.contains("Action$$Inject")){
                        clazzs.add(new InjectorPriorityWrapper(InjectorPriorityWrapper.ACTION_PRIORITY,entryClass));
                    }else if(entryName.contains("$$Inject")){
                        ((Injector)entryClass.newInstance()).inject();
                    }
                }
            }

            //进行实例化
            if (null != clazzs && clazzs.size() > 0){
                Collections.sort(clazzs);
                for (int i = 0; i < clazzs.size(); i ++){
                    InjectorPriorityWrapper clazz = clazzs.get(i);
                    ((Injector)clazz.mClass.newInstance()).inject();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return clazzs;
    }
}

这个类是得到dex文件下的信息,接着根据类名称反射生成这个类(这个类就是我们生成的源码),然后进行实例化调用。到这里我们编译期注解的例子已经讲解完毕了,其实步骤非常固定,只要操作一遍就熟悉了。如果文章写得有不懂的你可以下载源码下来看看,希望大家能在以后的项目中用到这个技术,还是非常方便。

总结:本文通过一个实际的例子来说明这项技术怎么使用,主要步骤包括:项目结构划分,注解模块实现,注解处理器模块实现,注解生成源码,生成的源码的使用等,希望大家能在学习本文完有个大的提高,因为你有能力去阅读这类型框架的源码了,而且能自己实现一套复杂的框架,希望本篇是你的第一步引导。

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

推荐阅读更多精彩内容