Android编译时注解项目学习

前几天在学习Android模块化的时候,对注解有了新的理解,对于模块间通信的问题,可以利用动态代理,反射来获取运行时注解的参数,进而拼接成完整的Uri请求,从而实现不同模块间界面的跳转。感兴趣的朋友可以点击这里。大多数情况下,跳转界面时都会携带一些简单的参数,为了优化这个过程,打算利用编译时注解的技术,类似butterknife,免去获取参数的一系列繁琐的代码。强烈建议先看鸿洋大神的Android如何编写基于编译时注解的项目文章,写的十分详细,我这里主要记录自己整个项目搭建的过程,包括遇到的一些问题,和自己的一些心得体会。

简单来说,编译时注解框架就是为了节省我们的工作量,避免一些重复且繁琐的代码编写。且不说他的实现方式,其实就目的来说和动态代理很类似,都是替我们多“做事”,解放我们的时间。

一、搭建项目基本框架

这里搭建一个类似butterknife的注解框架,在编写编译时注解框架的时候,一般需要建立多个module:

(1)ioc-annotation:用于存放注解等,Java模块(也可以是Android模块)

(2)ioc-compiler:用于编写注解处理器,Java模块(这个必须是Java模块,而且算是最重要的一个环节)

(3)ioc-api:用于调用的API,主要是调用生成后的Java文件,Android模块(也可以是Java模块)

这里会新建一个android项目,用于测试这个框架,就直接称之为app

模块新建完后需要设置依赖关系:

ioc-compiler依赖于ioc-annotation:注解处理器需要获取注解信息

app依赖于ioc-annotation,ioc-api:app需要调用注解和接口进行使用,其实也需要导入ioc-compiler,只不过方式不一样,下面会提到。

二、注解模块的实现

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView
{
    int value();
}

注解比较简单:

(1)声明注解存活时间

(2)声明注解运用的地方

(3)声明注解的属性,资源文件的ID值,所以是一个int类型

三、注解处理器的实现

注解处理器的实现其实是比较复杂的,但因为有模板,所以整个过程还是比较直观,我看第一遍的时候由于没有抓住要点,所以整个人看的比较混乱。这里必须要先弄清楚注解处理器是怎么工作的,它的目的是什么,抓住主要的内容即可。

在我看来注解处理器主要做了两件事情:

(1)获取注解信息

(2)生成对应的Java文件,也就是我们想要省掉的那些代码

一开始看到butterknife的时候还在想,这么厉害,一个注解就能代替那么多代码,请恕我年少无知,其实代码并没有少,只不过用了另外一种方式生成了。

添加依赖:

compile 'com.google.auto.service:auto-service:1.0-rc2'

基本代码:

注解处理器一般继承于AbstractProcessor,这部分的代码基本都是固定的。

@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor
{
    private Messager messager;
    private Elements elementUtils;
    private Map<String, ProxyInfo> mProxyMap = new HashMap<String, ProxyInfo>();

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

        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes()
    {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
    {
        return true;
    }

    private boolean checkAnnotationValid(Element annotatedElement, Class clazz)
    {
        if (annotatedElement.getKind() != ElementKind.FIELD)
        {
            error(annotatedElement, "%s must be declared on field.", clazz.getSimpleName());
            return false;
        }
        if (ClassValidator.isPrivate(annotatedElement))
        {
            error(annotatedElement, "%s() must can not be private.", annotatedElement.getSimpleName());
            return false;
        }

        return true;
    }

    private void error(Element element, String message, Object... args)
    {
        if (args.length > 0)
        {
            message = String.format(message, args);
        }
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message, element);
    }
}

在实现AbstractProcessor后,process()方法是必须实现的,也是我们编写代码的核心部分,后面会介绍。

我们一般会实现getSupportedAnnotationTypes()getSupportedSourceVersion()两个方法,这两个方法一个返回支持的注解类型,一个返回支持的源码版本,参考上面的代码,写法基本是固定的。

除此以外,我们还会选择复写init()方法,该方法传入一个参数processingEnv,可以帮助我们去初始化一些父类类:

  • Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
  • Elements mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
  • Messager mMessager;跟日志相关的辅助类。

process的实现

process中的实现,相比较会比较复杂一点,一般可以认为两个大步骤:

  • 收集信息
  • 生成代理类(本文把编译时生成的类叫代理类)

就是前面说的注解处理器主要做的事情

例如本例,我们会针对每一个类生成一个代理类,例如MainActivity我们会生成一个MainActivity$$ViewInjector。那么如果多个类中声明了注解,就对应了多个类,这里就需要:

  • 一个类对象,代表具体某个类的代理类生成的全部信息,本例中为ProxyInfo
  • 一个集合,存放上述类对象(到时候遍历生成代理类),本例中为Map<String, ProxyInfo>,key为类的全路径。

这里获取信息的代码其实也都是类似的,主要是生成代理类的方法,是最重要的。

收集信息

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
    {
        messager.printMessage(Diagnostic.Kind.NOTE , "process...");
        mProxyMap.clear();

        Set<? extends Element> elesWithBind = roundEnv.getElementsAnnotatedWith(BindView.class);
        for (Element element : elesWithBind)
        {
            checkAnnotationValid(element, BindView.class);

            VariableElement variableElement = (VariableElement) element;
            //class type
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //full class name
            String fqClassName = classElement.getQualifiedName().toString();

            ProxyInfo proxyInfo = mProxyMap.get(fqClassName);
            if (proxyInfo == null)
            {
                proxyInfo = new ProxyInfo(elementUtils, classElement);
                mProxyMap.put(fqClassName, proxyInfo);
            }

            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxyInfo.injectVariables.put(id , variableElement);
        }

        for (String key : mProxyMap.keySet())
        {
            ProxyInfo proxyInfo = mProxyMap.get(key);
            try
            {
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
                        proxyInfo.getProxyClassFullName(),
                        proxyInfo.getTypeElement());
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e)
            {
                error(proxyInfo.getTypeElement(),
                        "Unable to write injector for type %s: %s",
                        proxyInfo.getTypeElement(), e.getMessage());
            }

        }
        return true;
    }

第一个for循环是为了遍历注解信息,获取对应的一些信息

第二个for循环是为了遍历整理好的信息,生成对应的代理类,即proxyInfo.generateJavaCode()方法,这个是代理类的字符串格式。

生成代理类
public String generateJavaCode()
    {
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code. Do not modify!\n");
        builder.append("package ").append(packageName).append(";\n\n");
        builder.append("import com.example.francis.commonmodule.*;\n");
        builder.append("import com.example.ioc_api.ViewInject;\n");
        builder.append('\n');

        builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfo.PROXY + "<" + typeElement.getQualifiedName() + ">");
        builder.append(" {\n");

        generateMethods(builder);
        builder.append('\n');

        builder.append("}\n");
        return builder.toString();

    }
    private void generateMethods(StringBuilder builder)
    {

        builder.append("@Override\n ");
        builder.append("public void inject(" + typeElement.getQualifiedName() + " host, Object source ) {\n");

        for (int id : injectVariables.keySet())
        {
            VariableElement element = injectVariables.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            builder.append(" if(source instanceof android.app.Activity){\n");
            builder.append("host." + name).append(" = ");
            builder.append("(" + type + ")(((android.app.Activity)source).findViewById( " + id + "));\n");
            builder.append("\n}else{\n");
            builder.append("host." + name).append(" = ");
            builder.append("(" + type + ")(((android.view.View)source).findViewById( " + id + "));\n");
            builder.append("\n};");
        }
        builder.append("  }\n");

    }

这里import的包名需要换上对应项目的包名,否则会报错,看这个也还是不太直观,可以看下下面生成后的java文件就会比较清楚了。

// Generated code. Do not modify!
package com.example.francis.commonmodule;

import com.example.francis.commonmodule.*;
import com.example.ioc_api.ViewInject;

public class MainActivity$$ViewInject implements ViewInject<com.example.francis.commonmodule.MainActivity> {
@Override
 public void inject(com.example.francis.commonmodule.MainActivity host, Object source ) {
 if(source instanceof android.app.Activity){
host.tvCommon = (android.widget.TextView)(((android.app.Activity)source).findViewById( 2131165307));

}else{
host.tvCommon = (android.widget.TextView)(((android.view.View)source).findViewById( 2131165307));

};  }

}
我认为最重要的就是生成代理类的方法,其他的都可以说是模板,改动的不多,主要是要弄清楚,你想要实现的代码也就是你不想重复写的代码到底是怎样的,我认为整个框架也是围绕这个去编写

这里采用了完全拼接的方式编写Java代码,也可以使用一些开源库,来通过Java api的方式来生成代码,例如:javapoet,我还没有使用过,

Api模块的实现

这个模块的作用在于提供使用者找到并且调用刚才生成的代理类,类似使用butterknife时,都会在onCreate方法中设置

ButterKnife.bind(this);

都是一样的作用

定义一个接口:

public interface ViewInject<T>
{
    void inject(T t, Object source);
}

模仿了butterknife,第一个参数为宿主对象,第二个参数为实际调用findViewById的对象;当然在Actiivty中,两个参数就一样了。

这个接口其实是为了调用之前的代理类,细心的朋友可以发现前面的代理类实现了这个接口,当然这都是我们自己定义的,只要保证前后的名称一致就是了。

找到代理类:

public class ViewInjector
{
    private static final String SUFFIX = "$$ViewInject";

    public static void injectView(Activity activity)
    {
        ViewInject proxyActivity = findProxyActivity(activity);
        proxyActivity.inject(activity, activity);
    }

    public static void injectView(Object object, View view)
    {
        ViewInject proxyActivity = findProxyActivity(object);
        proxyActivity.inject(object, view);
    }

    private static ViewInject findProxyActivity(Object activity)
    {
        try
        {
            Class clazz = activity.getClass();
            Class injectorClazz = Class.forName(clazz.getName() + SUFFIX);
            return (ViewInject) injectorClazz.newInstance();
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        } catch (InstantiationException e)
        {
            e.printStackTrace();
        } catch (IllegalAccessException e)
        {
            e.printStackTrace();
        }
        throw new RuntimeException(String.format("can not find %s , something when compiler.", activity.getClass().getSimpleName() + SUFFIX));
    }
}

injectView静态方法:提供给使用者调用,同时获得Activity的名字(这里不是只能传Activity,这里只是举例)

findProxyActivity方法:通过名字找到对应的代理类

最后由于代理类实现了我们自定义的接口,我们只要再调用下接口的方法就行了

集成编译模块

Android Stduio 2.2以下使用方法

在项目的根目录的build.gradle添加:

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

在项目的build.gradle添加:

apt project(':ioc-compiler')
Android Stduio 2.2以上使用方法

直接在项目的build.gradle添加:

 annotationProcessor project(':ioc-compiler')

源码地址:点这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值