配色: 字号:
Android 编译时注解-提升
2017-01-17 | 阅:  转:  |  分享 
  
Android编译时注解-提升





背景



在前面的文章中,讲解了注解和编译时注解等一些列相关的内容,为了更加全面和真是的了解Android编译时注解在实战项目中的使用,本文采取实现主流框架butterknife注入view去全面认识编译时注解。







使用



仿照butterknife实现了@BindView注解,通过WzgJector.bind方法绑定当前MainActivity,整体和butterknife使用完全一模一样,这里为了区分简单的把butterknife改名了WzgJector



publicclassMainActivityextendsAppCompatActivity{



@BindView(R.id.tv_msg)

TextViewtvMsg;

@BindView(R.id.tv_other)

TextViewtvOther;



@Override

protectedvoidonCreate(BundlesavedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

WzgJector.bind(this);

if(tvMsg!=null){

tvMsg.setText("我已经成功初始化了");

}



if(tvOther!=null){

tvOther.setText("我就来看看而已");

}

}

}

实现



实现的思路和Android编译时注解-初认识实现原理大致一样,所以这里不重复阐述重复的步骤,重点讲解提升的技术点,所以需要在了解基本编译时注解的前提下继续下面的学习



Android编译时注解-初认识

定义注解



这里使用了Java和android自带的注解,初始一个BindView注解,同时指定了@Target为FIELD,注解BindView带有一个初始的int参数及时使用时的view-id



@Retention(RetentionPolicy.CLASS)

@Target(ElementType.FIELD)

public@interfaceBindView{

intvalue();

}



对java和android自带的注解不太清楚的同学可参考下面两篇文章



Java-注解详解



Android-注解详解

Element详解



Element



有了注解,必然需要有一个对应的注解处理器去处理注解,但是在处理注解的时候需要充分的了解注解处理器中的process方法及时核心的编译代码,而process方法的核心便是Element对象,所以在讲解注解处理器前,需要对Element有全面的认识,方能事半功倍。



由于Element的知识内容的复杂性,这里重点讲解核心内容,基本使用完全是足够了



源码:



publicinterfaceElementextendsAnnotatedConstruct{

TypeMirrorasType();



ElementKindgetKind();



SetgetModifiers();



NamegetSimpleName();



ElementgetEnclosingElement();



ListgetEnclosedElements();



booleanequals(Objectvar1);



inthashCode();



ListgetAnnotationMirrors();



AgetAnnotation(Classvar1);



Raccept(ElementVisitorvar1,Pvar2);

}

可看出其实Element是定义的一个接口,定义了外部调用暴露出的接口



方法 解释

asType 返回此元素定义的类型

getKind 返回此元素的种类:包、类、接口、方法、字段…,如下枚举值

getModifiers 返回此元素的修饰符,如下枚举值

getSimpleName 返回此元素的简单名称,比如activity名

getEnclosingElement 返回封装此元素的最里层元素,如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素;如果此元素是顶层类型,则返回它的包如果此元素是一个包,则返回null;如果此元素是一个泛型参数,则返回null.

getAnnotation 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回null。注解可以是继承的,也可以是直接存在于此元素上的

getKind方法



其中getKind方法比较特殊,getKind()方法来获取具体的类型,方法返回一个枚举值TypeKind



源码:



publicenumTypeKind{

/Theprimitivetype{@codeboolean}./

BOOLEAN,

/Theprimitivetype{@codebyte}./

BYTE,

/Theprimitivetype{@codeshort}./

SHORT,

/Theprimitivetype{@codeint}./

INT,

/Theprimitivetype{@codelong}./

LONG,

/Theprimitivetype{@codechar}./

CHAR,

/Theprimitivetype{@codefloat}./

FLOAT,

/Theprimitivetype{@codedouble}./

DOUBLE,

/Thepseudo-typecorrespondingtothekeyword{@codevoid}./

VOID,

/Apseudo-typeusedwherenoactualtypeisappropriate./

NONE,

/Thenulltype./

NULL,

/Anarraytype./

ARRAY,

/Aclassorinterfacetype./

DECLARED,

/Aclassorinterfacetypethatcouldnotberesolved./

ERROR,

/Atypevariable./

TYPEVAR,

/Awildcardtypeargument./

WILDCARD,

/Apseudo-typecorrespondingtoapackageelement./

PACKAGE,

/Amethod,constructor,orinitializer./

EXECUTABLE,

/Animplementation-reservedtype.Thisisnotthetypeyouarelookingfor./

OTHER,

/Auniontype./

UNION,

/Anintersectiontype./

INTERSECTION;

}



Element子类



Element有五个直接子接口,它们分别代表一种特定类型的元素



Tables Are

TypeElement 一个类或接口程序元素

VariableElement 一个字段、enum常量、方法或构造方法参数、局部变量或异常参数

ExecutableElement 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素

PackageElement 一个包程序元素

TypeParameterElement 一般类、接口、方法或构造方法元素的泛型参数

五个子类各有各的用处并且有各种独立的方法,在使用的时候可以强制将Element对象转换成其中的任一一种,但是前提是满足条件的转换,不然会抛出异常。



其中最核心的两个子分别是TypeElement和VariableElement



TypeElement详解



TypeElement定义的一个类或接口程序元素,相当于当前注解所在的class对象,及时本案例使用代码中的MainActivity



源码如下:



publicinterfaceTypeElementextendsElement,Parameterizable,QualifiedNameable{

ListgetEnclosedElements();



NestingKindgetNestingKind();



NamegetQualifiedName();



NamegetSimpleName();



TypeMirrorgetSuperclass();



ListgetInterfaces();



ListgetTypeParameters();



ElementgetEnclosingElement();

}



这里讲解主要的方法的含义



方法 解释

getNestingKind 返回此类型元素的嵌套种类

getQualifiedName 返回此类型元素的完全限定名称。更准确地说,返回规范名称。对于没有规范名称的局部类和匿名类,返回一个空名称.

getSuperclass 返回此类型元素的直接超类。如果此类型元素表示一个接口或者类java.lang.Object,则返回一个种类为NONE的NoType

getInterfaces 返回直接由此类实现或直接由此接口扩展的接口类型

getTypeParameters 按照声明顺序返回此类型元素的形式类型参数

VariableElement详解



源码:



publicinterfaceVariableElementextendsElement{

ObjectgetConstantValue();



NamegetSimpleName();



ElementgetEnclosingElement();

}



这里VariableElement除了拥有Element的方法以外还有以下两个方法



方法 解释

getConstantValue 变量初始化的值

getEnclosingElement 获取相关类信息

注解处理器



注解处理器需要两个步骤的处理:



1.收集先关的信息



2.生成处理类



对Element有了全面的了解过后,注解处理器便可很轻松的学习了,先来看看简单版本的BindView处理



Setelements=roundEnv.getElementsAnnotatedWith(BindView.class);

//一、收集信息

for(Elementelement:elements){

/检查类型/

if(!(elementinstanceofVariableElement)){

returnfalse;

}

VariableElementvariableElement=(VariableElement)element;



/获取类信息/

TypeElementtypeElement=(TypeElement)variableElement.getEnclosingElement();

/类的绝对路径/

StringqualifiedName=typeElement.getQualifiedName().toString();

/类名/

StringclsName=typeElement.getSimpleName().toString();

/获取包名/

StringpackageName=processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();



BindViewannotation=variableElement.getAnnotation(BindView.class);

intid=annotation.value();



/参数名/

Stringname=variableElement.getSimpleName().toString();

/参数对象类/

Stringtype=variableElement.asType().toString();



ClassNameInterfaceName=ClassName.bestGuess("com.example.annotation.api.ViewInjector");

ClassNamehost=ClassName.bestGuess(qualifiedName);



MethodSpecmain=MethodSpec.methodBuilder("inject")

.addModifiers(Modifier.PUBLIC)

.returns(void.class)

.addAnnotation(Override.class)

.addParameter(host,"host")

.addParameter(Object.class,"object")

.addCode(""

+"if(objectinstanceofandroid.app.Activity){\n"

+"host."+name+"=("+type+")(((android.app.Activity)object).findViewById("+id+"));\n"

+"}\n"

+"else{\n"

+"host."+name+"=("+type+")(((android.view.View)object).findViewById("+id+"));\n"

+"}\n")

.build();



TypeSpechelloWorld=TypeSpec.classBuilder(clsName+"ViewInjector")

.addModifiers(Modifier.PUBLIC)

.addMethod(main)

.addSuperinterface(ParameterizedTypeName.get(InterfaceName,host))

.build();



try{

//生成com.example.HelloWorld.java

JavaFilejavaFile=JavaFile.builder(packageName,helloWorld)

.addFileComment("Thiscodesaregeneratedautomatically.Donotmodify!")

.build();

//生成文件

javaFile.writeTo(filer);

}catch(IOExceptione){

e.printStackTrace();

}



大体的思路,先判断Element类型,如果是VariableElement则继续获取相关的包名(这里必须在app包名一致,不然获取不到android类)类对象信息,以及@BindView注解修饰的参数数据;最后将所有需要的数据通过javapoet和Filer自动编译创建一个java文件



最后得到的生成类:



packagecom.wzgiceman.viewinjector;



importcom.example.ViewInjector;

importjava.lang.Object;

importjava.lang.Override;



publicclassMainActivityViewInjectorimplementsViewInjector{

@Override

publicvoidinject(MainActivityhost,Objectobject){

if(objectinstanceofandroid.app.Activity){

host.tvMsg=(android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));

}

else{

host.tvMsg=(android.widget.TextView)(((android.view.View)object).findViewById(2131492945));

}

}

}



上面的简单处理器中,只是单纯的判断一个注解情况,在信息收集的处理上简化了,导致当前处理器只能同时处理当前相同类中的莫一个注解这里只初始化了tvMsg对象,tvOther并没有初始化,当然这是不符合实际需求的,下面来优化收集和处理方案。



优化



优化方案其实就是多了一步信息的记录的工作



创建信息类对象



首先创建一个类信息对象,其中包含了一下的属性,其中varMap便是记录当前类中所有注解相关的信息



publicclassVariMsg{

/包名/

privateStringpk;

/类名/

privateStringclsName;

/注解对象/

privateHashMapvarMap;

}



BindViewProcessors



1.初始一个map记录VariMsg对象,因为process方法可能会多次调用,所以需要每次都clear一遍



MapveMap=newHashMap<>();



2.记录信息

通过veMap记录所有的相关信息,并且每次需要判断是否重复,剔除重复的数据。



for(Elementelement:elements){

/检查类型/

if(!(elementinstanceofVariableElement)){

returnfalse;

}

VariableElementvariableElement=(VariableElement)element;



/获取类信息/

TypeElementtypeElement=(TypeElement)variableElement.getEnclosingElement();

/类的绝对路径/

StringqualifiedName=typeElement.getQualifiedName().toString();

/类名/

StringclsName=typeElement.getSimpleName().toString();

/获取包名/

StringpackageName=processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString()

BindViewannotation=variableElement.getAnnwww.baiyuewang.netotation(BindView.class);

intid=annotation.value()

VariMsgvariMsg=veMap.get(qualifiedName);

if(variMsg==null){

variMsg=newVariMsg(packageName,clsName);

variMsg.getVarMap().put(id,variableElement);

veMap.put(qualifiedName,variMsg);

}else{

variMsg.getVarMap().put(id,variableElement);

}



3.通过javapoet去生成java类文件

这里主要是javapoet的运用,详细用法可去javapoet查看



javapoet-GitHub



System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

for(Stringkey:veMap.keySet()){

ClassNameInterfaceName=ClassName.bestGuess("com.example.ViewInjector");

ClassNamehost=ClassName.bestGuess(key);

VariMsgvariMsg=veMap.get(key);



StringBuilderbuilder=newStringBuilder();

builder.append("if(objectinstanceofandroid.app.Activity){\n");

builder.append(code(variMsg.getVarMap(),"android.app.Activity"));

builder.append("}\n");

builder.append("else{\n");

builder.append(code(variMsg.getVarMap(),"android.view.View"));

builder.append("}\n");



MethodSpecmain=MethodSpec.methodBuilder("inject")

.addModifiers(Modifier.PUBLIC)

.returns(void.class)

.addAnnotation(Override.class)

.addParameter(host,"host")

.addParameter(Object.class,"object")

.addCode(builder.toString())

.build();



TypeSpechelloWorld=TypeSpec.classBuilder(variMsg.getClsName()+"ViewInjector")

.addModifiers(Modifier.PUBLIC)

.addMethod(main)

.addSuperinterface(ParameterizedTypeName.get(InterfaceName,host))

.build();



try{

JavaFilejavaFile=JavaFile.builder(variMsg.getPk(),helloWorld)

.addFileComment("Thiscodesaregeneratedautomatically.Donotmodify!")

.build();

javaFile.writeTo(filer);

}catch(IOExceptione){

e.printStackTrace();

System.out.println("e--->"+e.getMessage());

}

}

System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

returntrue;

}





/

根据注解对象生成code方法体



@parammap

@parampk

@return

/

privateStringcode(Mapmap,Stringpk){

StringBuilderbuilder=newStringBuilder();

for(Integerid:map.keySet()){

VariableElementvariableElement=map.get(id);

Stringname=variableElement.getSimpleName().toString();

Stringtype=variableElement.asType().toString();



builder.append("host."+name+"=("+type+")((("+pk+")object).findViewById("+id+"));\n");

}

returnbuilder.toString();

}



到这里注解处理器最终版本就生成成功了,看下最后生成的代码类



packagecom.wzgiceman.viewinjector;



importcom.example.ViewInjector;

importjava.lang.Object;

importjava.lang.Override;



publicclassMainActivityViewInjectorimplementsViewInjector{

@Override

publicvoidinject(MainActivityhost,Objectobject){

if(objectinstanceofandroid.app.Activity){

host.tvMsg=(android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));

host.tvOther=(android.widwww.tt951.comget.TextView)(((android.app.Activity)object).findViewById(2131492946));

}

else{

host.tvMsg=(android.widget.TextView)(((android.view.View)object).findViewById(2131492945));

host.tvOther=(android.widget.TextView)(((android.view.View)object).findViewById(2131492946));

}

}

}



api



api模块主要定义的是给外部提供的使用方法,这里使用方法便是WzgJector.bind(this)方法,相同于ButterKnife中的ButterKnife.bind(this);



publicclassMainActivityextendsAppCompatActivity{



xxxxxx

@Override

protectedvoidonCreate(BundlesavedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

WzgJector.bind(this);

xxxx

}

}



实现



创建一个appmodule



定义接口类ViewInjector



暴露的外部方法,主要在编译自动生成的注解处理类中使用



/

接口

CreatedbyWZGon2017/1/11.

/

publicinterfaceViewInjector{

voidinject(Mm,Objectobject);

}



实际处理类WzgJector



提供了两个方法,一种是activity绑定,一种是view或者fragment绑定,绑定完以后,通过反射得到相关注解编译处理类及时ViewInjector子类对象,调用inject(Mm,Objectobject)方法完成初始过程。



publicclassWzgJector{

publicstaticvoidbind(Objectactivity){

bind(activity,activity);

}



publicstaticvoidbind(Objecthost,Objectroot){

Classclazz=host.getClass();

StringproxyClassFullName=clazz.getName()+"ViewInjector";

try{

ClassproxyClazz=Class.forName(proxyClassFullName);

ViewInjectorviewInjector=(ViewInjector)proxyClazz.newInstance();

viewInjector.inject(host,root);

}catch(ClassNotFoundExceptione){

e.printStackTrace();

}catch(InstantiationExceptione){

e.printStackTrace();

}catch(IllegalAccessExceptione){

e.printStackTrace();

}

}

}

到这里其实会发现,编译时注解并不是完全不使用反射,但是它避免了关键性重复性代码的多次反射使用,继而提升了编译的性能。



结果



到这里butterknife的@BindView实现原理基本就是如此,由于时间原因@OnClick就不再讲解了。其实原理一样,小伙伴们可以自己安装本文思路添加@OnClick的处理。

献花(0)
+1
(本文系thedust79首藏)