Android如何开发自定义编译时注解

 


前言

     最近给公司写android的公司开发库,自写了一个注解库,用于方便公司写某些重复代码( findviewbyId、setOnClickListener、setOnTouchListener )时使用注解。同时收集了不少材料,这里总结分享下,希望与众人多多学习、交流。
     

一、Android中的注解框架

     Android开发过程中,我们经常会遇到以下情况:在Activity中有一个View,我们要获得这个View的实例是要通过findViewById这个方法,然后这个方法返回的是一个Object类型,我们还需要进行强制的类型转换。当一个布局中有很多个控件的时候,每一个控件都要进行上面的这个操作,其实是没有意义的重复代码,类似的还有OnClick,OnTouch等方法。
    所以许多Android开发人员会引入注解框架来配合开发,以提高开发效率。以下是 国内前500APP使用注解框架的情况:
     

    Dagger框架的使用方式和Google Guice差不多,其实Dagger是Guice的一个子集,更轻量,更适合在Android平台使用在国外使用比较多,没有采取反射而使用了预编译技术,因为基于反射的DI非常占用资源和耗时。使用比较复杂,在View上的注入非常麻烦。Dagger能够注入到任何你想要的对象,只要其在module类中。或者它是构造器。但是缺少对方法和字段的注入支持。 对于Dagger我们可以把它当做应用中的一个模块, 负责为其它模块提供实例并且注入依赖关系。那是它的基本职责。模块的创建位于我们应用中的一个点上,这样我们可以拥有完全的控制权。
  ButterKnife Buffer knife只是避免样板代码,findViewById,仅此而已,所以不能算是一个真正的注入。只是一个view的代言,可以在在非activity里面实施,也可以在inflate的views里面实施。
 RoboGuice 为Android平台上基于Google Guice开发的一个库,方便findViewById在XML中查找一个View,并将其强制转换到所需类型,通过运行时读取annotations进行反射,所以可能影响应用性能。
AndroidAnnotations是最火的快速开发框架,大量的使用注解,不会对APP的造成不良影响,会影响到APP的执行性能。在编译器中加了一层额外的自动编译步骤,用来生成基于你源码的代码。使用AA的注解在编译期间就已经自动生成了对应的子类,运行期运行的其实就是这个子类.则不会造成任何负面的影响。AndroidAnnotations像是综合了其他的几个框架的优点,通过注解,减少了繁琐(R.id.btn)的使用,并在编译的时候,将相应的注解进行转换。即:加快了开发的速度,又不影响app的性能。


开发项目使用注解框架,我个人是比较倾向使用AndroidAnnotations,因为他的功能强大和便利性。示例代码:

import android.app.Activity;
import android.widget.EditText;
import android.widget.TextView;

import com.googlecode.androidannotations.annotations.Click;
import com.googlecode.androidannotations.annotations.EActivity;
import com.googlecode.androidannotations.annotations.ViewById;

@EActivity(R.layout.main)
public class MyActivity extends Activity {

    @ViewById(R.id.myInput)
    EditText myInput;

    @ViewById(R.id.myTextView)
    TextView textView;

    @Click
   void myButton() {
        String name = myInput.getText().toString();
       textView.setText("Hello " + name);
    }
}
    
     
二、JAVA的注解和分类
     什么是注解(Annotation):
  Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法。Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。
注解(Annotation),也叫元数据。一种代码级别的说明。它是在JDK5.0及以后中引入的一种特性,与类、接口、枚举是在同一层次, 在java.lang的包中已经定义了三个注解:Override,Deprecated,SuppressWarnings. 。它可以声明在包、类、字段、方法、局部变量、方法、参数等的前面,用来对这些元素进行说明、注释。
作用分类:
1.编写文档:通过代码里标识的元数据生成文档。(生成文档)
2.代码分析:通过代码里标识的元数据对代码进行分析。(使用反射)
3.编译检查:通过代码里标识的元数据让编译器能够实现基本的编译检查。(Override)

Java注解可以在编译、类加载、运行时被读取,注解不会影响程序代码的运行,如果希望注解在运行时起到作用:需要通过反射得到相应的注解,然后在对其进行处理。

包 java.lang.annotation 中包含所有定义自定义注解所需用到的原注解和接口。如接口 java.lang.annotation.Annotation 是所有注解继承的接口,并且是自动继承,不需要定义时指定,类似于所有类都自动继承Object。

该包同时定义了四个元注解,Documented,Inherited,Target(作用范围,方法,属性,构造方法等),Retention(生命范围,源代码,class,runtime)。

注解在何种时刻可获取取决于Retention决定,RetentionPolicy.RUNTIME表示在运行时可见,它将被写入class文件的VisibleAnnotation属性中。RetentionPolicy.CLASS表示写入CLASS文件但不会在运行时获取,它们被写入字节码的invisibleAnnotation属性中。这些通常是为了方便IDE或者工具开发者的。当然,通过一些字节码库,应用程序无需了解字节码结构一样可以获取它们。剩下一个表示只在编译时可见,不会被写入CLASS文件。它们用于指示编译器行为,例如检查重载,设置过时,抑制警告等。这类注解是给编译器开发者准备的。
这里特别要指出注解的Retention的作用:
1.RetentionPolicy.SOURCE —— 这种类型的Annotations只在源代码级别保留,编译时就会被忽略
2.RetentionPolicy.CLASS —— 这种类型的Annotations编译时被保留,在class文件中存在,但JVM将会忽略
3.RetentionPolicy.RUNTIME —— 这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用.


有一些源码使用运行时注解来注入Android的View中,这里并不建议,可能写法稍微简单点,但是在运行效率上会比编译时注解低。

运行时注解的例子:

public static void injectContentView(Activity activity) {
    Class a = activity.getClass();
    if (a.isAnnotationPresent(ContentView.class)) {
        // 得到activity这个类的ContentView注解
        ContentView contentView = (ContentView) a.getAnnotation(ContentView.class);
        // 得到注解的值
        int layoutId = contentView.value();
        // 使用反射调用setContentView
        try {
            Method method = a.getMethod("setContentView", int.class);
            method.setAccessible(true);
            method.invoke(activity, layoutId);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}


可以很明显看出是通过“反射”来将某个setContentView注入给activity,来得到activity.setContentView(layoutId)的代码实现。


下面要介绍的编译时注解,直接在编译阶段,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的,会依靠动态生成的类做一些操作,因为没有反射,效率和直接调用方法没什么区别。



三、Android中编译时注解的原理和实现

原理方面:其实就是1.定义注解类,来定义自定义的注解,比如:

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

2.通过一个继承于javax.annotation.processing.AbstractProcessor的注解处理器类(这就要求我们将所在工程定义个java library,因为Android的工程已经不支持javax),在process()方法里面,将你需要的注解生成为需要的代码,举个例子:



@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    mAnnotatedClassMap.clear();

    try {
        processBindView(roundEnv);//生成初始化“@BindView” 注入的控件的代码
        processOnClick(roundEnv);//生成初始化“@OnClick” 注入的控件的代码
        processOnTouch(roundEnv);//生成初始化“@OnTouch” 注入的控件的代码
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
        error(e.getMessage());
    }

    for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
        try {
            annotatedClass.generateFile().writeTo(mFiler);
        } catch (IOException e) {
            error("Generate file failed, reason: %s", e.getMessage());
        }
    }
    return true;
}



在以往的定义注解解析器时,需要在解析器类定义过程中,做以下操作:
在解析类名前定义:
@SupportedAnnotationTypes("com.bosssoft.zhangxinjin.ViewInjectProcesser")
@SupportedSourceVersion(SourceVersion.RELEASE_7)

同时在java的同级目录新建resources目录, 新建META-INF/services/javax.annotation.processing.Processor文件, 文件中填写你自定义的Processor全类名,这是向JVM声明解析器。


当然幸好我们现在使用的是Android Studio,可以用auto-service来替代以上操作,只要在注解类前面加上@AutoService(Processor.class)就可以替代以上操作。
还有android-apt工具。在新IDE中是这样操作的:
          
          在主module的build.gradle中添加工具
           dependencies { ... classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'}
          在使用注解的应用module的build.gradle,添加
           apply plugin: 'com.neenbedankt.android-apt'
     ...
     dependencies {
          compile project(':bosssoft-mobile-android-compiler')
          apt project(':bosssoft-mobile-android-compiler')
     }

当然新版本的gradle2.2+中,我们也可以用gradle自带的annotationProcessor来替代android-apt:
     
     dependencies {
          compile project(':bosssoft-mobile-android-compiler')
          annotationProcessorproject(':bosssoft-mobile-android-compiler')
     }




继续贴一些代码:


private void processOnClick(RoundEnvironment roundEnv) throws IllegalArgumentException {
    for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
        AnnotatedClass annotatedClass = getAnnotatedClass(element);
        OnClickMethod onClickMethod = new OnClickMethod(element);
        annotatedClass.addClickMethod(onClickMethod);
    }
}


AnnotatedClass  类
public void addClickMethod(com.bosssoft.platform.mobile.android.compiler.model.OnClickMethod method) {
    onClickMethods.add(method);
}



public JavaFile generateFile() {
    //generateMethod
    MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject")
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addParameter(TypeName.get(mTypeElement.asType()), "host", Modifier.FINAL)
            .addParameter(TypeName.OBJECT, "source")
            .addParameter(com.bosssoft.platform.mobile.android.compiler.TypeUtil.PROVIDER,"provider");

    for(BindViewField field : mFields){
        // find views
        injectMethod.addStatement("host.$N = ($T)(provider.findView(source, $L))",
                field.getFieldName(),
                ClassName.get(field.getFieldType()), field.getResId());
    }

...
//一些其他注解代码
...

    //generaClass
    TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewInject")
            .addModifiers(Modifier.PUBLIC)
            .addSuperinterface(ParameterizedTypeName.get(com.bosssoft.platform.mobile.android.compiler.TypeUtil.INJET, TypeName.get(mTypeElement.asType())))
            .addMethod(injectMethod.build())
            .build();

    String packgeName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();

    return JavaFile.builder(packgeName, injectClass).build();
}




最后会在使用注解module的build目录下生成:


以上就是我们注解自动生成的代码,也是我们注解最终能够成功运行的关键。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值