最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

世界速读:注解在Android中的使用场景

来源:博客园

Android课程学习记录

注解是 在Java SE5 这个版本中引入的

1、什么是注解

在代码中最常见的一个就是 @Override,看一下它的语法定义:


(资料图片仅供参考)

@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}

可能会遇到的一个问题就是:请讲解一下什么注解?答:注解,写法上来说就是上述的形式,一般常用来标记到方法、类、参数、变量上面一句话就能解释清楚,好像说清楚了,但是又好像什么都没说清。

首先来看一下关注注解的一些基本定义

元注解- 专门负责注解其他注解的注解,常用的元注解有@Target@Retention@Documented@Inherited

  • @Target是标记这个注解在哪里使用的,参数类型是定义在 ElementType中的
/** Class, interface (including annotation type), or enum declaration */    TYPE,    /** Field declaration (includes enum constants) */域声明    FIELD,    /** Method declaration */    METHOD,    /** Formal parameter declaration */ 普通参数    PARAMETER,    /** Constructor declaration */ 构造方法    CONSTRUCTOR,    /** Local variable declaration */ 局部变量    LOCAL_VARIABLE,    /** Annotation type declaration */ 注解    ANNOTATION_TYPE,    /** Package declaration */ 包    PACKAGE,    /**     * Type parameter declaration     * Java 1.8 加入的,用于类型参数的声明     * @since 1.8      */    TYPE_PARAMETER,    /**     * Use of a type     * Java 1.8 加入的,用于一个类型的使用     * @since 1.8     */    TYPE_USE

@Target标注指定类型后,就能高速编译器做类型限制

  • @Retention表示需要在什么级别保留该注解信息。参数类型是在 RetentionPolicy中定义
public enum RetentionPolicy {    /** 表示注解仅在源码中可用,将会被编译器丢掉. */    SOURCE,    /**表示注解会被编译器记录在 class 文件中,但在运行时虚拟机(VM)不会保留*/    CLASS,    /**     * 表示注解会被编译器记录在 class 文件中,而且在运行时虚拟机(VM)会保留注解。所以这里可以通过反射读取注解的信息      * @see java.lang.reflect.AnnotatedElement     */    RUNTIME}

2、 使用场景

单独的讲一个注解是没啥意义的,它必须和其他的一些技术或者场景集合起来,才能发挥其作用

2.1 语法检查

这个用法在Android 系统的源码也用的比较多,看一下下边这个例子:

/**定义一个方法,参数用 {@link @DrawableRes} 标记*/    private static void setImg(@DrawableRes int imgRes){        Log.d("TAG", "setImg: "+imgRes);    }

可以看到,传入不是 @DrawableRes类型的 int 参数会提示传参有误,这个用法可用来规范传参,防止误传

2.2 代替枚举

枚举的每一个元素占用会比较大,是一个对象,有对象头等等,用注解定义的元素一般是基础数据类型,例如int等,能节省一部分开销,算是一个小小的优化项

2.3 注解+APT

一些使用比较广泛的开源框架就是利用这个技术点来实现的,例如 ButterKnife、Dagger2、Hilt等

2.3.1 APT是啥

APT 全称 Annotation Processing Tool,翻译过来就是注解处理程序,在Java 的.java 文件编译成.class 文件过程中做一些动作,简单画个图

2.3.2 操作流程

下边用一个demo简单说明下 APT 的实现流程

  1. 建一个新的Android项目
  2. 创建一个java-library 用来存放定义的注解,命名 annotation
// build.gradleapply plugin: "java-library"dependencies {    implementation fileTree(dir: "libs", include: ["*.jar"])}sourceCompatibility = "7"targetCompatibility = "7"

这里定义一个 找view id的注解 BindView

@Target(ElementType.FIELD)@Retention(RetentionPolicy.SOURCE)public @interface BindView {    int value();}
  1. 在创建一个 java-library,用来处理定义的注解,命名 annotation_compiler,定义如下
// build.gradleapply plugin: "java-library"dependencies {    implementation fileTree(dir: "libs", include: ["*.jar"])    annotationProcessor "com.google.auto.service:auto-service:1.0-rc4"    compileOnly "com.google.auto.service:auto-service:1.0-rc4"    implementation project(path: ":annotations")}sourceCompatibility = "7"targetCompatibility = "7"

TIPS:

  • 需要引入 com.google.auto.service:auto-service:1.0-rc4依赖库
  • 把前一个module annotation添加到依赖里面接下来就是 apt 的处理逻辑代码,创建一个处理类:
// 注意需要添加  @AutoService(Processor.class) 这个标记@AutoService(Processor.class)public class AnnotationsCompiler extends AbstractProcessor {    @Override    public boolean process(Set annotations, RoundEnvironment roundEnv) {        return false;    }}

apt 的模板代码 就写好了,具体的处理逻辑 在 process()里面去实现就好,这里先简单介绍几个 可能会用到的一些其他 父类方法

  • 第一个
/** 支持的版本,一般使用 SourceVersion.latestSupported() 就好 */    @Override    public SourceVersion getSupportedSourceVersion() {        return SourceVersion.latestSupported();    }
  • 第二个
/** 把需要处理的注解装到一个集合里面,比如这里要处理的 BindView 注解 */    @Override    public Set getSupportedAnnotationTypes() {        Set types = new HashSet<>();        types.add(BindView.class.getCanonicalName());        return types;    }
  • 第三个
/** 初始化入口 */   @Override    public synchronized void init(ProcessingEnvironment processingEnvironment) {        super.init(processingEnvironment);        filer = processingEnvironment.getFiler();    }
  • 第四个
/** 处理具体逻辑的地方 */    @Override    public boolean process(Set annotations, RoundEnvironment roundEnv) {        return false;    }

接下来就根据 添加注解自动找ID这个功能做一个简单的实现,整个注解处理的逻辑如下:

@AutoService(Processor.class)public class AnnotationsCompiler extends AbstractProcessor {    @Override    public SourceVersion getSupportedSourceVersion() {        return SourceVersion.latestSupported();    }    @Override    public Set getSupportedAnnotationTypes() {        Set types = new HashSet<>();        types.add(BindView.class.getCanonicalName());        return types;    }    /** 定义一个对象,用来生成APT目录下面的文件  */    Filer filer;    @Override    public synchronized void init(ProcessingEnvironment processingEnvironment) {        super.init(processingEnvironment);        filer = processingEnvironment.getFiler();    }    /**     * 所有的坏事都在这个方法中实现     */    @Override    public boolean process(Set set, RoundEnvironment roundEnvironment) {        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "test---------------" + set);        //获取APP中所有用到了BindView注解的对象        Set elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);//        TypeElement//类//        ExecutableElement//方法//        VariableElement//属性        //开始对elementsAnnotatedWith进行分类        Map> map = new HashMap<>();        for (Element element : elementsAnnotatedWith) {            VariableElement variableElement = (VariableElement) element;            String activityName = variableElement.getEnclosingElement().getSimpleName().toString();            Class aClass = variableElement.getEnclosingElement().getClass();            List variableElements = map.get(activityName);            if (variableElements == null) {                variableElements = new ArrayList<>();                map.put(activityName, variableElements);            }            variableElements.add(variableElement);        }        //开始生成文件//        package com.example.aptdemo;//        import com.example.aptdemo.IBinder;//        public class MainActivity_ViewBinding implements IBinder {//            @Override//            public void bind(com.example.aptdemo.MainActivity target) {//                target.textView = (android.widget.TextView) target.findViewById(2131165359);////            }//        }        if (map.size() > 0) {            Writer writer = null;            Iterator iterator = map.keySet().iterator();            while (iterator.hasNext()) {                String activityName = iterator.next();                List variableElements = map.get(activityName);                //得到包名                TypeElement enclosingElement = (TypeElement) variableElements.get(0).getEnclosingElement();                String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();                try {                    JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + activityName + "_ViewBinding");                    writer = sourceFile.openWriter();                    //        package com.example.aptdemo;                    writer.write("// source code is generated by CusButterKnife\n\n");                    writer.write("package " + packageName + ";\n");                    //        import com.example.aptdemo.IBinder;                    writer.write("import " + packageName + ".IBinder;\n");                    //        public class MainActivity_ViewBinding implements IBinder<                    //        com.example.aptdemo.MainActivity>{                    writer.write("public class " + activityName + "_ViewBinding implements IBinder<" +                            packageName + "." + activityName + ">{\n");                    //            public void bind(com.example.aptdemo.MainActivity target) {                    writer.write(" @Override\n" +                            " public void bind(" + packageName + "." + activityName + " target){\n");                    //target.tvText=(android.widget.TextView)target.findViewById(2131165325);                    for (VariableElement variableElement : variableElements) {                        //得到名字                        String variableName = variableElement.getSimpleName().toString();                        //得到ID                        int id = variableElement.getAnnotation(BindView.class).value();                        //得到类型                        TypeMirror typeMirror = variableElement.asType();                        writer.write("\ttarget." + variableName + "=(" + typeMirror + ")target.findViewById(" + id + ");\n");                    }                    writer.write("\t}\n}");                } catch (Exception e) {                    e.printStackTrace();                } finally {                    if (writer != null) {                        try {                            writer.close();                        } catch (Exception e) {                            e.printStackTrace();                        }                    }                }            }        }        return false;    }}

到此,注解处理的逻辑就完成了,接下来,在app module 中写上一些工具类

  1. 首先定义一个接口,用于绑定有添加对应注解 @BindView的界面,把当前界面 与 apt生成的代码 关联起来,apt自动生成的代码类是实现该接口的
public interface IBinder {    void bind(T target);}

2.顶一个工具类,用来传入当前界面

public class CusButterKnife {    public static void bind(@NonNull Activity activity) {        String name = activity.getClass().getName() + "_ViewBinding";        try {            Class aClass = Class.forName(name);            IBinder iBinder = (IBinder) aClass.newInstance();            iBinder.bind(activity);        } catch (Exception e) {            e.printStackTrace();        }    }}

3.看下Activity的使用

@BindView(R.id.tvText)    TextView textView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        CusButterKnife.bind(this);        textView.setText("123456");    }

4.在 annotation_compiler代码写完之后,build 一下工程,就能找到我们自己实现的需要生成的代码:

// source code is generated by CusButterKnifepackage com.example.aptdemo;import com.example.aptdemo.IBinder;public class MainActivity_ViewBinding implements IBinder {    @Override    public void bind(com.example.aptdemo.MainActivity target) {        target.textView = (android.widget.TextView) target.findViewById(2131165359);    }}

至此,这个注解+APT 实现自动找id 的功能就完成了

2.4 注解+反射+动态代理

比如于xutils 这个框架的核心处理逻辑就是通过这个来实现的

也通过一个demo实现一个功能:给方法添加注解,自动实现view 的点击事件众所周知,常规实现一个按钮的点击实现,是需要这样的代码:

Button btn1 = findViewById(R.id.btn1);btn1.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {            }        });

这里需要实现的功能是 写一个方法,添加一个注解(点击事件定义注解为@OnClick),直接实现点击功能,无需再使用上述的常规写法注册点击事件,代码如下:

@OnClick(R.id.btn1)    public void abc(@NonNull View view) {         Toast.makeText(this, "触发点击", Toast.LENGTH_SHORT).show();    }

具体实现如下

  • 第一步,把注册点击事件这段代码划分成三个部分,如下图所示:
  • 通过注解 @OnClick(R.id.btn1),就要能拿到对应的这三部分用于实现点击事件,那就创建一个注解类,用来存放这三个部分
@Target(ElementType.ANNOTATION_TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface EventBase {    //1.订阅关系    setOnClickListener    String listenerSetter();    //2.事件本身    new View.OnClickListener()    Class listenerType();    //3.事件处理程序  onClick方法    String callbackMethod();}
  • 把这个注解 标记到我们的功能注解 @OnClick
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@EventBase(listenerSetter = "setOnClickListener"        ,listenerType = View.OnClickListener.class        ,callbackMethod = "onClick")public @interface OnClick {    int[] value() default -1;}
  • 接下来,写一个工具类,通过反射的语法,把@OnCLick注解的具体逻辑给它实现
public class InjectUtils {    public static void inject(Activity context) {        inJeckEvent(context);    }    private static void inJeckEvent(Activity context) {        Class clazz = context.getClass();        // 反射获取传入类所有的方法        Method[] methods = clazz.getDeclaredMethods();//遍历,筛选出有所需注解的方法        for (Method method : methods) {//拿到有注解的方法的注解            Annotation[] annotations = method.getAnnotations();            for (Annotation annotation : annotations) {                Class annotationClass = annotation.annotationType();//遍历方法注解,判断是否有  EventBase 注解                EventBase eventBase = annotationClass.getAnnotation(EventBase.class);                //判断是不是事件处理程序  onClick  onLongClink                if (eventBase == null) {                    continue;                }                //1.setOnClickListener 订阅关系//                String listenerSetter();                String listenerSetter = eventBase.listenerSetter();                //2.new View.OnClickListener()  事件本身//                Class listenerType();                Class listenerType = eventBase.listenerType();                //3.事件处理程序//                String callbackMethod();                String callBackMethod = eventBase.callbackMethod();                //得到3要素之后,就可以执行代码了                Method valueMethod = null;                try {                    //拿到注解 value (传入的id)                    valueMethod = annotationClass.getDeclaredMethod("value");                    int[] viewId = (int[]) valueMethod.invoke(annotation);                    for (int id : viewId) {                        //为了得到Button对象,使用findViewById                        Method findViewById = clazz.getMethod("findViewById", int.class);                        View view = (View) findViewById.invoke(context, id);                        if (view == null) {                            continue;                        }                        //context===activity   click=method                        ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(context, method);                        //new View.OnClickListener()                        Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, listenerInvocationHandler);                        //   view.setOnClickListener(new View.OnClickListener())                        Method  onClickMethod = view.getClass().getMethod(listenerSetter, listenerType);                        onClickMethod.invoke(view, proxy);                    }                } catch (Exception e) {                    e.printStackTrace();                }            }        }    }}

这一段代码,是使用了动态代理,把组件原本onclick代理到 有对应注解的方法上,看一下相关实现代码

/** * 这个类用来代理 事件本身 (new View.OnClickListener()) * 并执行这个对象身上的onClick方法 */public class ListenerInvocationHandler implements InvocationHandler {    //需要在onClick中执行activity.click();    private Activity activity;    private Method activityMethod;    public ListenerInvocationHandler(Activity activity, Method activityMethod) {        this.activity = activity;        this.activityMethod = activityMethod;    }    /**     * 就表示onClick的执行     * 程序执行onClick方法,就会转到这里来     * 因为框架中不直接执行onClick     * 所以在框架中必然有个地方让invoke和onClick关联上     */    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        //在这里去调用被注解了的click();        return activityMethod.invoke(activity,args);    }}
  • 最后在 activity 中调用 InjectUtils.inject,就完成了,看下运行

到此 ,APT+注解+动态代理 的常见使用方式就讲完了

补充

这里只是实现了 点击事件,那比如要实现注解实现长按事件呢?先前已经定了一个注解用来存放 一个事件注册的及要素嘛,这个时候,要在进行扩展就比较容易了,长按事件的注解实现,就只需要定义一个这样的注解类就行了:

@EventBase(listenerSetter = "setOnLongClickListener"        ,listenerType = View.OnLongClickListener.class        ,callbackMethod = "onLongClick")@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface OnLongClick {    int[] value() default -1;}

是不是很眼熟,跟上面 OnCLick 注解的定义止呕三要素的不同,传不同的参数到 @EventBase注解里面就行了,这个就是一个 注解的多态这个概念的实际应用了。在想要扩展Android 中的其他各种出入事件监听,就一次类推,定义对应的注解就行了;

好了,结束

关键词: 事件处理 操作流程