当前位置: 首页 > 科技观察

Android基于编译时注解如何写项目

时间:2023-03-12 22:46:54 科技观察

一、概述在Android应用开发中,我们经常会选择使用一些基于注解的框架来提高开发效率,但是由于反射带来的运行效率损失,我们将更加偏爱编译时注解的框架,比如butterknife,让我们免于编写View初始化和事件注入的代码。EventBus3方便我们实现组件间的通信。fragmentargs方便的给片段添加参数信息,并提供创建方法。ParcelableGenerator可以自动将任何对象转换为Parcelable类型,方便对象传输。类似的库还有很多,这些库大多是为了帮助我们自动完成日常编码中需要重复编写的部分(例如:每个Activity中的View需要初始化,每个实现Parcelable的对象)接口需要写很多固定代码)。这并不是说上述框架一定不能使用反射。其实上面提到的一些框架内部还是有一些依赖反射的实现,但是很少而且一般都是做缓存处理的,所以相对来说对效率的影响较小。.但是,在使用此类项目时,有时很难调试错误。主要是很多用户不了解这类框架的内部原理,所以遇到问题的时候会花很多时间去排查。好吧,出于某种原因,当编译时注解框架如此流行的时候,我们有理由学习:如何编写一个有机会进行编译时注解的项目。首先是了解它的原理,这样当我们在使用类似框架的时候遇到问题的时候,我们就可以找到正确的方法来排查问题;其次,如果我们有好的想法,发现有些代码需要重复创建,我们也可以自己写一个框架,方便我们日常编码,提高编码效率;***也算是自身技术的提升。注意:下面使用的IDE是AndroidStudio。本文将以编写View注入框架为线索,详细介绍编写这样一个框架的步骤。2.编写前的准备在编写这样的框架时,一般需要创建多个模块,比如本文要实现的例子:ioc-annotation用于存储注解等,Java模块ioc-compiler是用于编写注解处理器,Java模块ioc-api用于为用户提供API。本示例是Andriod模块ioc-sample的示例。这个例子是一个Andriod模块。除了例子,一般需要创建3个模块。您可以考虑模块的名称。上面给出了一个简单的参考。当然,如果条件允许,有些开发者喜欢将存储注解和API的两个模块合二为一。对于模块之间的依赖关系,因为写注解处理器需要依赖相关的注解,所以:ioc-compiler依赖ioc-annotation我们在使用的过程中会用到注解和相关的API,所以ioc-sample依赖ioc-api;ioc-api依赖ioc-annotation3.注解模块的实现注解模块主要用来存放一些注解类。本例中模板butterknife实现了View注入,所以本例只需要一个注解类:@Retention(RetentionPolicy.CLASS)@Target(ElementType.FIELD)public@interfaceBindView{intvalue();}我们设置的保留策略是Class,注解用在Field上。这里我们需要在使用的时候传入一个id,直接以value的形式设置。写的时候分析自己需要多少注解类,正确设置@Target和@Retention。四、注解处理器的实现定义注解完成后,就可以编写注解处理器了。这有点复杂,但是有规律可循。对于这个模块,我们一般依赖注解模块,可以使用一个自动服务库build.gradle的依赖,如下:dependencies{compile'c??om.google.auto.service:auto-service:1.0-rc2'compileproject(':ioc-annotation')}自动服务库可以帮助我们生成META-INF等信息。(1)基本代码Annotationprocessors一般继承自AbstractProcessor。刚才我们说有规律可循,因为有些代码的写法基本是固定的,如下:init(processingEnv);mFileUtils=processingEnv.getFiler();mElementUtils=processingEnv.getElementUtils();mMessager=processingEnv.getMessager();}@OverrideSetupportedgetSend{annotationTypes=newLinkedHashSet();annotationTypes.add(BindView.class.getCanonicalName());returnannotationTypes;}@OverridepublicSourceVersiongetSupportedSourceVersion(){returnSourceVersion.latestSupported();}@Overridepublicbooleanprocess(SetmProxyMap=newHashMap();@Overridepublicbooleanprocess(Setannotations,RoundEnvironmentroundEnv){mProxyMap.clear();Setelements=roundEnv.getElementsAnnotatedWith(BindView.class);//一、收集信息for(Elementelement:elements){//检查元素类型if(!checkAnnotationUseValid(element)){returnfalse;}//fieldtypeVariableElementvariableElement=(VariableElement)element;//classtypeTypeElementtypeElement=(TypeElement)variableElement.getEnclosingElement();//TypeElementStringqualifiedName=typeElement.getQualifiedName().toString();ProxyInfoproxyInfo=mProxyMap.get(qualifiedName);if(proxyInfo==null){proxyInfo=newProxyInfo(mElementUtils,typeElement);mProxyMap.put(qualifiedName,proxyInfo);}BindViewannotation=variableElement.getAnnotation(BindView.class);intid=annotation.value();proxyInfo.mInjectElements.put(id,variableElement);}returntrue;}首先我们调用一下mProxyMap.clear();,因为process可能会被多次调用,避免生成重复的代理类,避免生成类的类名异常。然后通过roundEnv.getElementsAnnotatedWith获取@BindView注解的元素。这里的返回值按照我们的预期应该是VariableElement。集合,因为我们在成员变量上使用它们。接下来for循环我们的元素,首先检查类型是否为VariableElement,然后获取对应的类信息TypeElement,然后生成一个ProxyInfo对象,这里通过一个mProxyMap来检查,关键是qualifiedName是类的全路径,如果不生成生成一个新的,ProxyInfo和类是一一对应的。接下来将@BindView声明的这个类对应的VariableElement添加到ProxyInfo中。关键是我们声明的时候填写的id,也就是View的id。这样,信息的收集就完成了。收集信息后,应生成代理类。b.生成代理类@Overridepublicbooleanprocess(Setannotations,RoundEnvironmentroundEnv){//...省略收集信息的代码,试试,catch相关for(Stringkey:mProxyMap.keySet()){ProxyInfoproxyInfo=mProxyMap.get(键);JavaFileObjectsourceFile=mFileUtils.createSourceFile(proxyInfo.getProxyClassFullName(),proxyInfo.getTypeElement());Writerwriter=sourceFile.openWriter();writer.write(proxyInfo.generateJavaCode());writer.flush();writer.close();}returntrue;}可以看到生成代理类的代码很短,主要是遍历我们的mProxyMap,然后获取每一个ProxyInfo,***通过mFileUtils.createSourceFile创建文件对象,类名是代理信息。getProxyClassFullName(),写的内容是proxyInfo.generateJavaCode()。好像生成Java代码的方法都在ProxyInfo里面。C。生成Java代码这里我们主要关注它生成Java代码的方式。下面主要看生成Java代码的方法:#ProxyInfo//key为id,value为对应的成员变量publicMapmInjectElements=newHashMap();publicStringgenerateJavaCode(){StringBuilderbuilder=newStringBuilder();builder.append("package"+mPackageName).append(";\n\n");builder.append("importcom.zhy.ioc.*;\n");builder.append("publicclass").append(mProxyClassName).append("implements"+SUFFIX+"<"+mTypeElement.getQualifiedName()+">");builder.append("\n{\n");generateMethod(builder);builder.append("\n}\n");returnbuilder.toString();}privatevoidgenerateMethod(StringBuilderbuilder){builder.append("publicvoidinject("+mTypeElement.getQualifiedName()+"host,Objectobject)");builder.append("\n{\n");for(intid:mInjectElements.keySet()){VariableElementvariableElement=mInjectElements.get(id);Stringname=variableElement.getSimpleName().toString();Stringtype=variableElement.asType().toString();builder.append("if(objectinstanceofandroid.app.Activity)");builder.append("\n{\n");builder.append("host."+name).append("=");builder.append("("+type+")(((android.app.Activity)object).findViewById("+id+"));");builder.append("\n}\n").append("else").append("\n{\n");builder.append("host."+name).append("=");builder.append("("+type+")(((android.view.View)object).findViewById("+id+"));");builder.append("\n}\n");}建造者。append("\n}\n");}这个主要是根据收集到的信息,拼接完成的代理类对象,好像很头疼,不过我给个生成的代码,会比较多packagecom.zhy.ioc_sample;导入com.zhy.ioc.*;publicclassMainActivity$$ViewInjectorimplementsViewInjector{@Overridepublicvoidinject(com.zhy.sample.MainActivityhost,Objectobject){if(objectinstanceofandroid.app.Act{host.mTv=(android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));}else{host.mTv=(android.widget.TextView)(((android.view.我们具体需要实现生成java代码,这里注意生成的代码实现了一个接口ViewInjector,就是统一所有代理类对象的类型,这时候我们需要强制代理类对象为接口类型并调用它的方法;接口是泛型的,主要是传入实际的类对象,比如MainActivity,因为我们是g在代理类中生成代码,这实际上是实际的类。访问成员变量。因此,编译时注解的成员变量一般不允许使用private修饰符进行修饰。(有些是允许的,但需要提供getter和setter访问方法)。在这里,Java代码是完全拼接的方式写的。您还可以使用一些开源库通过JavaAPI生成代码,例如:javapoet.AJavaAPIforgenerating.javasourcefiles。到这里我们就完成了代理类的生成。Any写注解处理器的方式基本上都是按照收集信息和生成代理类的步骤进行的。5、API模块的实现代理类可用后,我们一般会提供API供用户访问。比如本例中的访问入口是Ioc.inject(Activity)in//Activity;//在Fragment中,获取ViewHolder中的Ioc.inject(this,view);模仿butterknife,第一个参数是宿主对象,第二个参数是实际调用findViewById的对象;当然,在Actiivty中,这两个参数是一样的。API一般怎么写?其实很简单。只要理解了它的原理,这个API就会做两件事:根据传入的host找到我们生成的代理类:比如MainActivity->MainActivity$$ViewInjector。强制统一接口,调用接口提供的方法。这两件事应该不复杂。第一件事就是拼接代理类名,然后反射生成对象,第二件事就是强制调用。publicclassIoc{publicstaticvoidinject(Activityactivity){inject(activity,activity);}publicstaticvoidinject(Objecthost,Objectroot){Classclazz=host.getClass();StringproxyClassFullName=clazz.getName()+"$$ViewInjector";//省略try,catch相关代码>{voidinject(Tt,Objectobject);}代码很简单,拼接代理类的全路径,然后通过newInstance生成实例,然后强制调用代理类的inject方法。一般情况下,生成的代理类都会被缓存起来。比如存储在一个Map中,不重新生成,我们这里就不做。这样,我们就完成了一个编译时注解框架的编写。6.小结本文通过具体的例子,介绍了如何基于编译期注解编写工程。主要步骤为:项目结构划分、注解模块实现、注解处理器编写、对外发布的API模块编写。通过文本的学习,你应该能够理解这类基于编译期注解的框架的运行原理,以及如何自己编写这类框架。