框架基石—注解的使用论述与实践

66 阅读7分钟

前言

本文不是基础科普文讨论注解使用的细节,主要是讨论注解在日常开发中的使用思路,关键的在于注解的生命周期,能够保留在源码,字节码,运行时三个阶段,运用不同的技术手段在不同的阶段取出注解实现一些特殊效果

常见注解辨析

注解一个好像有点大用,又没什么大用的东西

说注解有点大用是因为在各种知名框架中都能见到注解的身影,比如:butterknife,eventBus,Retrofit,Daggre2等等。

说注解没什么大用是因为抛开三方框架,单纯写业务,注解几乎是个可有可无的东西,只起到辅助作用。

开发中常见的注解:

  1. @Override 提示当前方法是重写父类方法 或 接口实现的方法
  2. @Nullable 提示当前参数,属性值,方法返回值等可以为空
  3. @Deprecated 标记在方法,提示开发者当前方法以被弃用,在AndroidStudio中调用被此注解修饰的方法会中横线示意
  4. @ColorRes @DrawableRes @xxxRes AndroidSDK提供校验资源的注解,
    1. 比如在代码中设置背景颜色,通过R.color.xxx引用图片资源,R文件生成的静态常量是int类型,
    2. protected void setTextColor(int color) 定义方法设置文本颜色,使用int类型参数接收颜色值
    3. 此时对于程序来说 并不能确定参数color 究竟是不是正确的R.color.xxx ,参数填写123对于程序也是正确的,都是int值。 对于结果来说 填些123肯定是错误的
    4. 使用@ColorRes 后 ,编译器会做校验,检测参数是不是R.color.xxx ,随便填些int数值 或是其他类型的资源编译器会提示报错。
  5. 上述举例的注解即使去除也不影响程序正常运行

可以看出 在日常开发中,注解对于程序并不起决定性作用,大多是辅助提示性作用

理解注解生命周期

详细的注解基础知识网上很多,这里不再详述。

如果开发者只是自定义一个全新的注解,把它标记在类,方法,属性上,会发现并没有任何作用。注解本身没什么作用,没有任何意义,单独的注解就是一种注释,对代码并不构成任何影响。

注解有两点特性值得关注:

  1. 能够携带少量数据
  2. 具有生命周期

这两点结合起来会产生非常奇妙的作用。自定义注解通过元注解 @Retention 可以确定注解的生命周期,也叫做注解的保留时期,保留时期有三种源码,字节码,运行时,分别对应**@Retention注解的三个取值**

  • @Retention(RetentionPolicy.SOURCE),注解仅存在于源码中(编译期)
  • @Retention(RetentionPolicy.CLASS), 默认的保留策略,注解会存在源码,字节码(类加载)
  • @Retention(RetentionPolicy.RUNTIME), 注解存在于源码,字节码,运行时(JVM中运行)

上面提到过 注解可以携带少量数据,比如:自定义注解@GET, 保存url地址。

  1. 如果@GET的保留策略 RetentionPolicy.SOURCE ,则数据会留存于源码
  2. 如果@GET的保留策略 RetentionPolicy.CLASS,则数据会留存于源码 , 字节码
  3. 如果@GET的保留策略 RetentionPolicy.RUNTIME,则数据会留存于源码 , 字节码,运行时

对于注解最重要的一个理解就是 能够根据策略保留一段时间数据

对于开发人员来说,就是应用不同的技术解析保留在源码,字节码,运行时中的注解取出数据,进行其他操作。

注解本身没什么意义,需要结合其他技术才有意义。

注解使用场景

根据注解保留级别不同,对注解使用存在不同场景,如下:

  1. RetentionPolicy.SOURCE,
    1. 常用于APT,在编译器能够获取注解与注解声明的类,包括类中所有的成员信息,一般用于生成额外的辅助类,
    2. annotation processor tools 注解处理器。javac提供的功能。在编译java文件的时候,可选择额外调用注解处理程序
  2. RetentionPolicy.CLASS,
    1. 常用于字节码插装,在编译处Class后,通过修改Class数据以实现修改代码逻辑。java有语法检查,没有引用的类用不了,但是class已经经过语法检查,可以随意修改,但是改错了,执行也会报错
  3. RetentionPolicy.RUNTIME,
    1. 常用于反射,在程序运行期间,通过反射技术动态获取注解与其标记的元素,完成不同的逻辑

实践—反射版butterknife

正版的Butterknife是使用 编译期注解+注解处理器实现的,性能更好。 为了体会一下注解的妙用仿butterknife的api 使用运行时注解+反射实现类似效果

先看看原生 和 butterknfie 的用法对比

  1. view绑定时不需要使用findViewById
  2. 在属性添加注解@BindView 绑定属性与id
  3. 在onCreate() 调用ButterKnife.bind(this);

butterknife不止绑定view这一个功能,可以绑定Android中所有资源,动画,颜色,图片,字符串,点击事件,长按事件等等。

butterknife在初始化之后,通过注解的方式替代原有的资源绑定方式,简化使用减少代码量。

开发三方框架的目的肯定是弥补原生写法的一些缺陷可能是各种各样的原因,比如:使用麻烦,复杂,代码量多,容易出错等等。总之呢使用xxx功能让程序员用起来不爽,那就有些勤快的程序员为了偷懒基于麻烦的原生api封装出好用的三方框架。

想说的重点是什么呢,

butterknife是为了Android程序员绑定资源更方便的而封装的库,它肯定脱离不了原生api

虽然经过封装绑定view 变成 @BindView(R.id.xxx),findViewById(R.id.xxx) 消失不见,但仅仅是看不见了,底层肯定还是由findViewById实现的,只不过经过大师巧妙的构思隐藏起来了。

事实上很多三方库都是这样,原生写法太难用,基于原生写法封装一套更简单易用的写法。

我们现在想要使用反射仿写,那么我们要考虑也是如何一步一步的把findViewById()藏起来

//原生用法

public class TestActivity extends AppCompatActivity {
    private TextView textView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        textView = findViewById(R.id.text);
        textView.setText("啦啦啦");
    }
}

//butterknife

public class TestActivity extends AppCompatActivity {
    @BindView(R.id.text)
    TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        ButterKnife.bind(this);
        textView.setText("啦啦啦");
    }
}

分析

  1. 创建ButterKnife类,声明静态方法 bind()
  2. TextView 属性 设为 public
  3. 在bind() 方法中 传入TestActivity 作为参数,完成 findViewById

这种方式,属性必须设为public,只能针对某个特定activity,R.id.xxx仍然需要手动赋值, 不通用仍然属于硬编码的范畴

他也不是一无是处,暴露了一些问题,为下一步封装给予一些思想火花。完成一次findViewById()绑定有三要素

textView = activity.findViewById(R.id.text);

  1. 能够拿到属性的引用,对其赋值
  2. 有activity引用,调用findViewById()
  3. 知道 view Id是多少 ,R.id.xxx

我们要做的就是通过技术手段,在不是硬编码的情况下把上述三要素凑到一起,

技术手段已经很明确了,注解+反射

  1. 反射拿到属性的引用对其赋值
  2. activity引用通过方法参数传递
  3. 注解保存R.id.xxx

理清思路实现就好办了

public class TestActivity extends AppCompatActivity {
    public TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        ButterKnife.bind(this);

        textView.setText("啦啦啦");
    }
}

public class ButterKnife {

    public static void bind(TestActivity activity){
        activity.textView = activity.findViewById(R.id.text);
    }
}

实现

思路已经聊的很清楚了,没啥可讨论的。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {

    int value();
}

public class ButterKnife {

    public static void bind(Activity activity){
        Class<?> clazz = activity.getClass();
        for (Field field :clazz.getDeclaredFields()){
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView!=null){
                try {
                    field.setAccessible(true);
                    int viewId = bindView.value();
                    field.set(activity,activity.findViewById(viewId));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class TestActivity extends AppCompatActivity {
    @BindView(R.id.text)
    TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        ButterKnife.bind(this);

        textView.setText("啦啦啦");
    }
}