Android开发必备——注解

2023-05-16

前言

阅读官方源码以及各类第三方框架时可以发现,很多地方都有注解,作为一名Android程序员,掌握注解属于必不可少的一项技能。

1. 什么是注解

注解是以@符号开头的用来标识如类、字段、方法等的工具。说到注解,就不得不提另外一个概念——注释,两者其实都是做解释的功能,只不过注释是面向开发者,而注解则是针对程序。注解一般需要结合注解处理器或者反射等实现对应的功能,否者将没有实际的意义。两者的区别如下:

  1. 定义不同:
    注解:英名为Annotation,它是JDK1.5及以后版本引入的一个特性。 与类、接口、枚举是在同一个层次,可以成为java 的一个类型。用一个词描述注解------元数据,它是一种描述数据的数据。所以,可以说注解就是源代码的元数据。
    注释:是对源代码作介绍、评议或说明的文字。
  2. 作用不同:
    注解是Java 编译器可以理解的部分,是给编译器看的。通过标记包、类、字段、方法、局部变量、方法参数等元素据,告诉jvm这些元素据的信息。
    注释是程序员对源代码做一些记忆或提示性描述,是给人来看的。它能告诉开发者这段代码的逻辑、说明、特点等内容,对代码起到解释、说明的作用。

2. 元注解

元注解是由Java提供的一套用来注解其他注解的基础注解,听起来可能有点绕,其实就是Java编译器在对注解做处理的时候需要知道该注解的时效性、作用范围等一些信息,于是提供了一套用来对注解做限定的工具——元注解。
JDK1.5加入的元注解有如下:

  • @Target:指定注解的作用范围
  • @Retention:指定注解的作用时机
  • @Inherited:被该注解修饰的注解,作用在某个类上,该类的此注解可以被子类继承
  • @Documented:不常用,给Javadoc配置的,这里略过

2.1 @Target

此元注解用来指定注解的作用范围,参数如下:

  • ElementType.TYPE:类、接口(包括注解类型)或枚举声明
  • ElementType.FIELD:字段声明(包括枚举常量)
  • ElementType.METHOD:方法声明
  • ElementType.PARAMETER:形参声明
  • ElementType.CONSTRUCTOR:构造函数声明
  • ElementType.LOCAL_VARIABLE:局部变量声明
  • ElementType.ANNOTATION_TYPE:注解类型声明
  • ElementType.PACKAGE:包声明
  • ElementType.TYPE_PARAMETER:类型参数声明——JDK1.8加入
  • ElementType.TYPE_USE:类型的使用——JDK1.8加入
  • ElementType.MODULE:模块声明——JDK1.9加入

2.2 @Retention

此元注解用来指定注解的作用时机,也就是说注解是在什么阶段有效,参数如下:

  • RetentionPolicy.SOURCE:指定注解只在源码阶段有用,编译器编译之后将会丢弃,这类型的注解一般用来做代码限制或者提示等。如Override用来提示方法重写了父类的方法,如果在没有重写父类方法的方法上面使用此注解,编译器会报错。
  • RetentionPolicy.CLASS:指定注解将由编译器记录在类文件中,但在运行时虚拟机不会保留,如果不指定@Retention,此为@Retention默认的参数。Android中经常用到一些方法参数的限制中,如LayoutResNonNullIntRange等。
  • RetentionPolicy.RUNTIME:指定注解会被编译器记录在类文件中,并在运行时虚拟机会保留,因此它们可以在程序运行时读取。

2.3 @Inherited

此元注解主要用来指定注解是否可以被继承,下面我们通过一个例子进行解释。

// 被@Inherited注解了的注解
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ExAnn {
}

// 没有@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface NoExAnn {
}

上面定义了两个注解,两者都是属于运行时可以获取,唯一的区别是@ExAnn使用了@Inherited

// 父类使用了上面两个注解
@ExAnn
@NoExAnn
public class Parent {
}

// 子类继承了父类,但是没有任何实现
public class Child extends Parent {
}

然后我们通过子类获取注解信息,测试代码如下:

private void test() {
    Annotation[] annotations = Child.class.getAnnotations();
    System.out.println("-----start-----"); 
    for (Annotation annotation : annotations) {
        System.out.println(annotation.toString());
    }
    System.out.println("------end------");
}

运行以上代码,最终会打印

System.out: -----start-----
System.out: @com.payne.annotation.demo.ExAnn()
System.out: ------end------

通过以上打印可以发现:父类里面的注解如果有被@Inherited注解,其子类才可以获取到父类的注解。

3. 自定义注解

3.1 运行时注解

以前在Android开发过程中,我们经常会使用到findViewById去获取View,大量的视图控件意味着我们会去重复多次使用findViewById,以至于后面出现了第三方框架ButterKnife,下面以findViewById做示例。

  1. 首先创建一个注解类,创建的注解类和接口有点像,只不过在interface前面多了一个@符号,如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface BindView {
    int value();
}
  1. 其次在测试类中使用,代码如下:
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_show)
    TextView tvShow;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        processView(this);
        tvShow.setText("赋值成功");
    }

    /**
     * 反射处理findViewById
     */
    public static void processView(Activity activity) {
        // 获取Activity中所有的字段
        Field[] fields = activity.getClass().getDeclaredFields();
        if (fields == null) {
            return;
        }
        // 遍历字段数组,找到带有BindView注解的字段
        for (Field field : fields) {
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView == null) {
                continue;
            }
            // 获取注解值——即ViewID
            int value = bindView.value();
            // 通过ViewID找到View
            View viewById = activity.findViewById(value);
            try {
                // 给字段View赋值
                field.set(activity, viewById);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

下面是运行效果:

运行结果

可以看到并没有直接给tvShow设置findViewById,但是运行之后TextView依然被成功设置为"赋值成功"。这是因为运行时在processView方法中通过反射获取并设值。

3.2 编译时注解

反射使用多了比较影响性能,翻看ButterKnifeRetrofit等第三方框架源码我们也能发现其并不是通过运行时反射赋值,而是通过编译工具在编译期间就对注解进行了处理,而处理注解需要使用到注解处理器,那什么是注解处理器呢?
注解处理器:英文为Annotation Processor,顾名思义,是用来处理注解的工具,其基本原理是将自定义注解处理器注册到编译器,编译器在编译阶段会去执行注册了的自定义注解处理器,完成对应的代码注入。
实现自定义注解处理器主要分为三步:

  1. 编写注解。
  2. 编写继承自javax.annotation.processing包下的AbstractProcessor类的自定义注解处理器。
  3. 将自定义注解处理器注册到编译器。

下面是自定义注解处理器的实现示例:
示例
如上图,示例主要包括两个Java module和一个Android module。

  1. test-annotation:Java module,主要用来存放自定义注解。
  2. test-compiler:Java module,主要存放继承自AbstractProcessor类的自定义注解处理器类。
  3. test-butterknife:Android module,主要存放工具类,用来反射获取编译器根据注解处理器生成的类。

3.2.1 定义注解

test-annotation模块下定义一个需要处理的注解类BindView

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

3.2.2 实现注解处理器

public class AnnotationCompiler extends AbstractProcessor {

    /**
     * 设置支持的源版本,默认为RELEASE_6
     * 两种方式设置版本:
     * 1. 此处返回指定版本
     * 2. 类上面设置SupportedSourceVersion注解,并传入版本号
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 设置支持的注解类型,默认为空集合(都不支持)
     * 两种方式设置注解集合:
     * 1. 此处返回支持的注解集合
     * 2. 类上面设置SupportedAnnotationTypes注解,并传入需要支持的注解
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }

    /**
     * 初始化操作
     *
     * @param processingEnv 环境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    /**
     * 处理注解
     *
     * @param set              待处理的注解集合
     * @param roundEnvironment RoundEnvironment
     * @return 返回true表示后续处理器不再处理
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        //TypeElement->类    ExecutableElement->方法   VariableElement->属性
        Map<String, List<VariableElement>> map = new HashMap<>(16);
        for (Element element : elements) {
            //属性元素
            VariableElement variableElement = (VariableElement) element;
            //获取类名
            String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
            //根据类名将属性元素保存在集合中
            List<VariableElement> variableElements = map.get(activityName);
            if (variableElements == null) {
                variableElements = new ArrayList<>();
                map.put(activityName, variableElements);
            }
            variableElements.add(variableElement);
        }

        if (map.size() > 0) {
            for (String activityName : map.keySet()) {
                //根据类名获取属性元素集合
                List<VariableElement> variableElements = map.get(activityName);
                //获取类元素
                TypeElement enclosingElement = (TypeElement) variableElements.get(0).getEnclosingElement();
                //获取类的包名
                String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
                //生成对应的类
                generateClass(variableElements, packageName, activityName);
            }
        }
        return false;
    }

    /**
     * 根据注解信息生成对应的类,本方法中手动生成类文件内容
     * 我们还可以使用第三方工具JavaPoet优雅的生成,具体参考地址:https://github.com/square/javapoet
     *
     * @param variableElements 设置了对应注解的属性元素的集合
     * @param packageName      包名
     * @param activityName     类名
     */
    private void generateClass(List<VariableElement> variableElements, String packageName, String activityName) {
        Writer writer = null;
        try {
            JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(packageName + "." + activityName + "_ViewBinding");
            writer = sourceFile.openWriter();
            //包名
            writer.write("package " + packageName + ";\n");
            //导入包
            writer.write("import com.payne.buf.IBinder;\n");
            //类名以及实现的接口名
            writer.write("public class " + activityName + "_ViewBinding implements IBinder<"
                    + packageName + "." + activityName + "> {\n");
            //实现接口中的方法
            writer.write("  @Override\n");
            writer.write("  public void bind(" + packageName + "." + activityName + " target) {\n");
            //遍历属性元素集合,根据信息生成findViewById操作
            for (VariableElement variableElement : variableElements) {
                String variableName = variableElement.getSimpleName().toString();
                int id = variableElement.getAnnotation(BindView.class).value();
                TypeMirror typeMirror = variableElement.asType();
                writer.write("      target." + variableName + " = (" + typeMirror + ") target.findViewById(" + id + ");\n");
            }
            writer.write("  }\n");
            writer.write("}\n");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.2.3 注册注解处理器

有两种方法将自定义注解处理器注册到编辑器。

  1. 手动注册:在test-compiler模块下的src/main/目录下创建META-INF/services/子目录,并创建文件名为javax.annotation.processing.Processor的文件,文件名是Processor类的全路径名,如下图:


    文件中加入自定义注解处理器的全路径名com.payne.annotation.test_compiler.AnnotationCompiler,如下图:
  2. 自动注册:依赖于Google的工具框架。
    首先在test-compiler模块下的build.gradle中加入依赖框架,然后在自定义注解处理器类上面添加@AutoService(Processor.class)注解即可。
implementation 'com.google.auto.service:auto-service:1.0'
annotationProcessor 'com.google.auto.service:auto-service:1.0'


编译之后可以发现test-compiler模块下的build文件夹中自动生成了和我们手动注册一样的文件。

最后在test-butterknife模块下提供IBinder接口以及findViewById的绑定类供App调用。

public interface IBinder<T> {
    void bind(T target);
}
public class PayneButterKnife {
    @SuppressWarnings("unchecked")
    public static void bind(Activity activity) {
        String name = activity.getClass().getName() + "_ViewBinding";
        try {
            Class<?> aClass = Class.forName(name);
            IBinder<Activity> iBinder = (IBinder<Activity>) aClass.newInstance();
            iBinder.bind(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4. 小结

  1. 注释面向开发者,注解面向程序。
  2. 可以通过Target元注解设置注解的作用范围,Retention元注解设置注解的作用时机。
  3. 源码时注解主要作用源码阶段,编译则被舍弃,主要面向编辑器等开发工具,用来在开发阶段提示开发者或者限制开发者使用范围。
  4. 编译时注解主要作用于编译阶段,编译之后则被舍弃,主要面向编译器,可以根据注解信息生成对应的类。
  5. 运行时注解主要作用于运行阶段,注解信息运行时依然存在,可以通过反射获取使用。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android开发必备——注解 的相关文章

  • Nuttx 驱动开发手册

    目录 Nuttx 代码获取编译 Nuttx 启动流程 Nuttx BootLoader 开发之源码分析 gpio 驱动分析 I2c驱动分析 PX4 框架分析 UORB 进程间通讯分析 PX4应用层驱动分析并实现例程 串口驱动GPS 驱动分析
  • 解析小觅中通过双目相机生成深度图的代码

    最近在使用双目摄像机生成深度图 xff0c 研读一下自带的代码 xff0c 做一个记录 第一部分 xff1a 第一部分是定义了一个命名空间 xff0c 其中包含许多个类 第一个类 xff1a 1 代码 GrabCallbacks类主要用于抓
  • altium designer PCB各层介绍+添加多层+设置正/负片+设置层的网络标号

    top layer 顶层 xff0c 用来走线 bottom layer 底层 xff0c 用来走线 mechanical 机械层 xff0c 用来定义PCB形状和尺寸 keepout layer 禁止布线层 xff0c 用来绘制禁布区 t
  • java死锁产生的条件

    以下四个条件同时满足时机会产生死锁 产生死锁的条件互斥 xff0c 共享资源 X 和 Y 只能被一个线程占用 xff1b 占有且等待 xff0c 线程 T1 已经取得共享资源 X xff0c 在等待共享资源 Y 的时候 xff0c 不释放共
  • PID--位置型PID和增量式PID比较

    一 位置型PID 位置型 PID 算法适用于不带积分元件的执行器 执行器的动作位置与其输入信号呈一一对应的关系 控制器根据第 n 次计算机采样结果与给定值之间的偏差 e 来计算出第 n 次采用后所输出的控制变量的值 以调节阀来简单说明 xf
  • 你可能不知道的室内无人机秘密都在这里(二 )

    接上篇 xff1a 你可能不知道的室内无人机秘密都在这里 xff08 一 xff09 如果说上一篇是无人机现状的一些科普知识篇 xff0c 那这篇就直接上干货了 xff0c 希望能真正帮助到喜欢无人机行业 想深入研究无人机的小伙伴们 具体我
  • 漫话程序员们的家庭装修——书房篇

    身为一名程序员 xff0c 辛辛苦苦码代码N年 xff0c 终于攒下钱买了自己的小窝 xff0c 不好好犒劳一下自己都对不起自己的近视眼和鼠标手 这就来分享一下我装修的心得 xff0c 从书房开始 xff01 书房作为程序员在公司战斗一天回
  • Windows下使用vscode 调试linux kernel

    安装WSL2 在microsoft store上安装Ubuntu xff0c 当不能安装时可能需要梯子window中访问Ubuntu的目录使用 xff1a wsl Ubuntu中需要修改软件源 xff1a 参考 https mirrors
  • 计算器算法----C语言实现(堆栈法)

    1 字符串去空格处理 实现一 xff1a span class hljs keyword void span spacess span class hljs keyword char span span class hljs keyword
  • 链接脚本

    本文转自 xff1a http www cnblogs com li hao p 4107964 html 一 概论 每一个链接过程都由 链接脚本 linker script 一般以lds作为文件的后缀名 控制 链接脚本 主要用于规定如何把
  • 记录2017/9/7趋势科技笔试题

    1 下面程序一共会在屏幕上输出多少个 xff1f include lt iostream gt include lt stdio h gt include lt sys types h gt include lt unistd h gt u
  • 字节对齐算法

    ps xff1a 遇见这种算法纯属一个巧合 xff0c 刚入职的我 xff0c 在忙着调用各种SDK中的API xff0c 无暇顾及代码的具体实现 xff0c 有些代码还被屏蔽了 xff0c 在写flash的过程中 xff0c 参考了前辈们
  • UCOSIII学习笔记

    目录 1 学习环境 2 滴答定时器 3 任务 3 1 UCOSIII系统任务 3 2 UCOSIII任务状态 3 3 UCOSIII任务调度 3 4 任务相关的API函数 3 5 钩子函数 4 UCOSIII的中断 5 UCOSIII的临界
  • QCC5125----GAIA

    1 描述 GAIA全称 xff1a Generic Application Interface Architecture xff0c 实现了端到端 xff0c 主机无关的生态系统 xff0c 支持主机应用程序访问设备功能 底层的数据包由8个
  • 【SQLserver】使用openrowset方法导入EXCEL表格数据

    一 前言 在之前的一篇博文中记录了用OPENDATASOURCE函数将EXCEL数据写入SQLserver表中的方法 这一方法需要表名sheet1为固定名称不可更改 实际业务中可能会遇到表名随着日期而改动的情况 xff0c 如果excel表
  • PDM

    PDM Pulse Density Modulation 1 Protocols Introduction1 1 PDM Introduction1 2 PCM Introduction1 3 PDM To PCM 2 PDM Struct
  • FreeRTOS --(1)链表

    Based On FreeRTOS Kernel V10 3 1 1 相关文件 链表结构是 OS 内部经常使用到的 xff0c FreeRTOS 自然也不例外 xff0c 在深入分析各个模块的工作原理之前 xff0c 首先来分析 FreeR
  • FreeRTOS --(3)任务管理之创建任务

    目录 1 描述任务的结构 2 任务创建 2 1 xTaskCreate 2 2 prvInitialiseNewTask 2 3 pxPortInitialiseStack 2 4 prvAddNewTaskToReadyList 在 Fr
  • FreeRTOS --(9)信号量之概述

    目录 1 Binary Semaphores 1 1 Usage 1 2 APIs 1 2 1 xSemaphoreCreateBinary 1 2 2 xSemaphoreTake xSemaphoreTakeFromISR 1 2 3
  • FreeRTOS --(11)资源管理之临界区

    目录 1 taskENTER CRITICAL 2 vTaskSuspendAll 3 Mutexes 3 1 Usage 临界区的概念在任何的 SoC 都存在 xff0c 比如 xff0c 针对一个寄存器 xff0c 基本操作为 xff1

随机推荐

  • MIPI 打怪升级之DCS篇

    目录 1 Overview2 Display Architectures2 1 The Type 1 Display Architecture 3 Power Level3 1 Type 1 Display Architecture Pow
  • MIPI 打怪升级之DPI篇

    目录 1 Overview2 Display Architectures2 1 Type 1 Display Architecture Block Diagram2 2 Type 2 Display Architecture Block D
  • MIPI 打怪升级之DBI篇

    目录 1 Overview2 Display Architectures2 1 Type 1 Display Architecture Block Diagram2 2 Type 2 Display Architecture Block D
  • 还不会华为交换机如何恢复出厂设置的,看这里

    哎呀 xff01 看错了 xff0c 我把三层交换机的配置写到二层交换机了 xff0c 其他的配置可都是好好的 xff0c 不会又要让我重新来一遍吧 xff01 小曼自从上次败北之后就开始研究起网络通信了 xff0c 没想到做个仿真实验还得
  • 基于stm32与NRF24L01的无线门禁系统

    首先 xff0c 需要说明梁只是一个小本科生 xff0c 水平不高 xff0c 许多错误请大家指教 xff08 qq1257681989 xff09 所写的内容是我自己做的 xff0c 写此博客仅在于让自己在完成之后有个回顾和总结 进入正文
  • 2016TI杯——寻迹小车

    首先 xff0c 我选择的是B题 自动循迹小车 xff0c 具体如下 xff1a B题 xff1a 自动循迹小车 1 xff0e 任务 设计制作一个自动循迹小车 小车采用一片 TI公司LDC1314或LDC1000电感数字转换器作为循迹传感
  • C语言小函数——删除字符串str1中含有的字符串str2

    本函数实现的是删除str1中的含有的所有str2 char span class hljs variable delstr span char span class hljs variable src span const char spa
  • 设备树简介

    设备树简介 一 xff1a 设备树由来 linux内核源码中 xff0c 之前充斥着大量的平台相关 xff08 platform Device xff09 配置 xff0c 而这些代码大多是杂乱且重复的 xff0c 这使得ARM体系结构的代
  • NVIDIA TX2上手体验

    NVIDIA TX2上手体验 硬件基础刷机流程远程登陆CAN总线调试回环模式硬件测试 硬件基础 新入手Nvidia TX2开发套件 xff0c 准备安装ubuntu16 04 xff0c 对应jetpack3 3版本 刷机流程 首先你要有一
  • 解决Pixhawk启动解锁过程中出现一些问题

    硬件 xff1a pixhawk2 4 6 固件 xff1a PX4 1 6 4 平台 xff1a linux qgroundcontrol 问题 xff1a 解锁过程中提示 Flying with usb is not safe xff0
  • 使用无线数传 radio telemetry 连接pixhawk进入offboard模式进行mavlink协议通讯的尝试

    环境 xff1a Windows电脑一台 Linux电脑一台 Radio telemetry 收发两端 Pixhawk 2 4 6飞控 USB micro 连接线 软件 xff1a PX4固件 QGroundcontrol 地面站 mavl
  • ROS学习笔记(四)ros 无法rosdep init 或者update解决方法

    如果提示的是 ERROR unable to process source https raw githubusercontent com ros rosdistro master rosdep xxxxx 之类的错误 xff0c 同时保证
  • ROS学习笔记(十二)ROS noetic ubuntu20.04 版本 rosdep init,rosdep update 问题解决方法

    ROS1 noetic 版本在ubuntu20 04安装出现问题 xff0c rosdep update无法下载 xff0c 网络地址访问超时 ROS1 noetic 版本在ubuntu20 04系统上的安装方法见博客 xff1a Ubun
  • FreeRTOS之xTaskCreate()

    xTaskCreate 函数解析 task span class token punctuation span h BaseType t span class token function xTaskCreate span span cla
  • 解决笔记本双USB接口散热器无法给其他外接设备供电的问题

    问题描述 xff1a 有个双usb接口的笔记本散热器 xff0c 之前是散热器接笔记本的一个usb接口 xff0c 连接到散热器的一个USB接口上 xff0c 散热器上的另一个usb接口可以连其他设备 xff0c 比如外界键盘 出现了问题
  • FreeRTOS之vTaskDelete()

    vTaskDelete 函数解析 task span class token punctuation span h span class token keyword void span span class token function v
  • MobaXterm连接不上虚拟机linux的问题

    目录 问题描述 xff1a step1 进入centOS下的 etc sysconfig network scripts step2 输入命令vi ifcfg ens33 查看并编辑该文件 step3 将文件中的 step4 重启网络服务
  • Linux LVM root分区 磁盘扩容

    LVM 的基本概念 物理卷 Physical Volume PV xff1a 可以在上面建立卷组的媒介 xff0c 可以是硬盘分区 xff0c 也可以是硬盘本身或者 回环文件 xff08 loopback file xff09 物理卷包括一
  • 浅尝树莓派3之串口配置

    树莓派3硬件串口的使用及编程 发表于 2017 01 29 分类于 树莓派 暂无评论 阅读次数 54 引言 本文转载自 xff1a http etrd org 2017 01 29 E6 A0 91 E8 8E 93 E6 B4 BE3 E
  • Android开发必备——注解

    前言 阅读官方源码以及各类第三方框架时可以发现 xff0c 很多地方都有注解 xff0c 作为一名Android程序员 xff0c 掌握注解属于必不可少的一项技能 1 什么是注解 注解是以 64 符号开头的用来标识如类 字段 方法等的工具