一步一步带你轻松打造自己的ButterKnife注解框架(上)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/binbinqq86/article/details/79610980

转载请注明出处:http://blog.csdn.net/binbinqq86/article/details/79610980

注解是从Java1.5开始引入的,当前许多java框架中大量使用注解,如Hibernate、Jersey、Spring。而在Android中,大名鼎鼎的Retrofit,ButterKnife都使用了注解的方式。我们在写Android页面的时候,经常会findViewById来绑定控件,当一个页面非常复杂的时候,这个工作就是个体力活了,于是ButterKnife就应运而生了,类似的还有之前的XUtils等,但是目前使用的最广泛的还是ButterKnife,用法简单,功能完善,持续维护。而这仅仅是在绑定控件领域的注解框架,另外还有EventBus方便我们实现组建间通讯,ParcelableGenerator可实现自动将任意对象转换为Parcelable类型,方便对象传输,等等,各种功能强大的注解都有。

虽然有了轮子,但是我们还是想造一个自己的轮子,今天就带大家一步一步打造一个属于自己的注解框架,其实明白了原理,各种功能强大的注解对我们来说都轻而易举可以实现了。我们就从findViewById入手来开始今天的讲解~

一、注解的分类

首先就是看一下Java中注解的分类,其实是元注解的分类,也就是用来注解自定义注解的原始注解。包括@Retention、@Target、@Document、@Inherited四种。

  1. @Documented —— 指明拥有这个注解的元素可以被javadoc此类的工具文档化。这种类型应该用于注解那些影响客户使用带注释的元素声明的类型。如果一种声明使用Documented进行注解,这种类型的注解被作为被标注的程序成员的公共API。
  2. @Target——指明该类型的注解可以注解的程序元素的范围。该元注解的取值可以为TYPE,METHOD,CONSTRUCTOR,FIELD等。如果Target元注解没有出现,那么定义的注解可以应用于程序的任何元素。
  3. @Inherited——指明该注解类型被自动继承。如果用户在当前类中查询这个元注解类型并且当前类的声明中不包含这个元注解类型,那么也将自动查询当前类的父类是否存在Inherited元注解,这个动作将被重复执行直到这个标注类型被找到,或者是查询到顶层的父类。
  4. @Retention——指明了该Annotation被保留的时间长短。RetentionPolicy取值为SOURCE,CLASS,RUNTIME。
    • @Retention(RetentionPolicy.SOURCE) //注解仅存在于源码中,在class字节码文件中不包含
    • @Retention(RetentionPolicy.CLASS) // 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
    • @Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在,在运行时可以通过反射获取到
二、注解的一些特性
  • 注解方法不能带有参数;
  • 注解方法返回值类型限定为:基本类型、String、Enums、Annotation或者是这些类型的数组;
  • 注解方法可以有默认值;
  • 注解本身能够包含元注解,元注解被用来注解其它注解。
三、内建注解

Java提供了三种内建注解。

  1. @Override——当我们想要复写父类中的方法时,我们需要使用该注解去告知编译器我们想要复写这个方法。这样一来当父类中的方法移除或者发生更改时编译器将提示错误信息。

  2. @Deprecated——当我们希望编译器知道某一方法不建议使用时,我们应该使用这个注解。Java在javadoc 中推荐使用该注解,我们应该提供为什么该方法不推荐使用以及替代的方法。

  3. @SuppressWarnings——这个仅仅是告诉编译器忽略特定的警告信息,例如在泛型中使用原生数据类型。它的保留策略是SOURCE(译者注:在源文件中有效)并且被编译器丢弃。

而Android内建注解就比较多了,@Keep、@NonNull、@Nullable、@StringRes等等等等,非常之多,他们都在谷歌提供的support-annotations这个库里。
这里写图片描述

我们可以看到,一共有这么多的注解。说了这么多,怎么去实现我们自己的注解呢,类似ButterKnife这种来替代重复的体力劳动的,下面就来开始打造我们自己的注解。

四、打造自己的注解(运行时注解)

这里我们先讲一下运行时的注解,编译时注解我们待下回分解~老规矩,上代码:

/**
 * @auther tb
 * @time 2018/3/20 下午2:09
 * @desc 绑定view的id或者layout的id,包含findViewById和setContentView两个功能
 */
@Retention(RUNTIME)
@Target({FIELD, TYPE})
public @interface BindId {
    int value() default View.NO_ID;
}

上面就是我们用来给view或者id进行注解的自定义注解类,以@开头,加interface修饰,跟接口的唯一区别就是多了个@。下面我们要写具体的api来实现它的绑定功能:

扫描二维码关注公众号,回复: 3759884 查看本文章
/**
 * @auther tb
 * @time 2018/3/20 下午2:24
 * @desc 执行具体绑定控件逻辑的api
 */
public class BindIdApi {
    public static void bindId(Activity obj) {
        Class<?> cls = obj.getClass();
        //使用反射调用setContentView
        if (cls.isAnnotationPresent(BindId.class)) {
            // 得到这个类的BindId注解
            BindId mId = (BindId) cls.getAnnotation(BindId.class);
            // 得到注解的值
            int id = mId.value();
            try {
                Method method = cls.getMethod("setContentView", int.class);
                method.setAccessible(true);
                method.invoke(obj, id);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        // 使用反射调用findViewById
        Field[] fields = cls.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(BindId.class)) {
                BindId mId = field.getAnnotation(BindId.class);
                int id = mId.value();
                // 使用反射调用findViewById,并为字段设置值
                try {
                    Method method = cls.getMethod("findViewById", int.class);
                    method.setAccessible(true);
                    Object view = method.invoke(obj, id);
                    field.setAccessible(true);
                    field.set(obj, view);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

具体思路就是使用反射机制,来进行方法的调用,代码里面注释写的比较清楚了,最后来看一下怎么去进行调用:

package example.tb.com.tannotation;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

@BindId(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @BindId(R.id.tv)
    private TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        BindIdApi.bindId(this);
        tv.setText("this is a test~");
    }
}

是不是用起来很方便,而且支持private变量,支持setContentView,当然你也可以分为两个注解把findViewById和setContentView区分开来,也可以返回一个id数组(@IdRes int[] value();),这样就可以绑定多个view,这里我就简单处理了,感兴趣的同学可以自行扩展。另外还有一种写法,我们可以看下:

public static void bindId2(Activity obj) {
        Class<?> cls = obj.getClass();
        if (cls.isAnnotationPresent(BindId.class)) {
            // 得到这个类的BindId注解
            BindId mId = (BindId) cls.getAnnotation(BindId.class);
            // 得到注解的值
            int id = mId.value();
            obj.setContentView(id);
        }
        Field[] fields = cls.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(BindId.class)) {
                BindId mId = field.getAnnotation(BindId.class);
                int id = mId.value();
                View view=obj.findViewById(id);
                try {
                    field.setAccessible(true);
                    field.set(obj, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

这种看起来是不是更加简洁方便呢,既然传入了activity,我们直接调用Activity的方法就可以了,何必再去反射方法呢,哈哈~

下面再来看一下点击事件setOnClickListener的注解~
首先也是去定义注解:

/**
 * @auther tb
 * @time 2018/3/21 上午11:47
 * @desc view的点击事件
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
}

注解的处理如下:

public static void bindOnClick(final Activity obj) {
        Class<?> cls = obj.getClass();
        //获取当前Activity的所有方法,包括私有
        Method methods[] = cls.getDeclaredMethods();
        for (int i = 0; i < methods.length; i++) {
            final Method method = methods[i];
            if (method.isAnnotationPresent(OnClick.class)) {
                // 得到这个类的OnClick注解
                OnClick mOnClick = (OnClick) method.getAnnotation(OnClick.class);
                // 得到注解的值
                int[] id = mOnClick.value();
                for (int j = 0; j < id.length; j++) {
                    final View view = obj.findViewById(id[j]);
                    view.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            //反射指定的点击方法
                            try {
                                //私有方法需要设置true才能访问
                                method.setAccessible(true);
                                method.invoke(obj, view);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        }
    }

基本上注释里面都写的很清楚了。也是首先找到这个view,然后调用setOnClickListener去设置点击事件,再反射调用注解的方法,最终去执行点击事件。最后在Activity的onCreate中绑定该事件:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        BindIdApi.bindId2(this);
        BindOnClick.bindOnClick(this);
        tv.setText("this is a test~");
    }

    @OnClick(R.id.tv)
    private void click(View view){
        switch (view.getId()){
            case R.id.tv:
                Toast.makeText(this,tv.getText().toString(),Toast.LENGTH_LONG).show();
                break;
        }
    }

怎么样,简单吧~网上还有一种写法就是采用动态代理方式,去生成View.OnClickListener类,然后去调用里面的onClick方法,进而完成注解的注入。

五、混淆

原来实现自己的注解这么简单,那你就too young too simple了,所谓功败垂成,正当你高兴的打包的时候,忽然发现,what?为什么所有的注解都失效了???其实很简单,就是因为你把反射需要用到的类或者方法、变量给混淆了,被重命名了,然后程序就找不到这些类、方法、变量了。。。
这里写图片描述

分析下我们打包后的apk,发现MainActivity中的click方法被移除优化掉了,所以我们的注解点击无效了,而findViewById和setContentView是没有被混淆的,所以反射后不会有任何影响,但是如果换成其他的自定义的类,反射后肯定就找不到了。
这里写图片描述

从mapping文件也可以看出,各个类及成员方法混淆后的对应关系,并没有我们的click方法,所以注解也就没效果了。解决方法很简单,就是保留这些类不被混淆。

  1. 我们可以使用@keep加在click方法上面,这个是系统注解,用来保留不被混淆的任何类、方法、变量。
  2. 我们可以使用自定义保留注解方法,仿照系统的keep
  3. 我们可以使用
    -keepclassmembers class example.tb.com.tannotation.MainActivity {
    private void click(android.view.View);
    }

我们再次打包分析apk,可以看到以下结果:
这里写图片描述

click方法已经被保留并且没有被混淆,然后我们的点击事件自然就生效了。但是你可能又想了,一个app里面那么多点击事件,要是每个地方都加上这些,那得多麻烦,好了,下面来说一下我们的终极方案:

-keep class example.tb.com.tannotation.BindOnClick
-keepclassmembers class * {
    @example.tb.com.tannotation.BindOnClick *;
}

我们可以保留所有被BindOnClick注解的方法,这样不就非常简单了吗!看看效果:
这里写图片描述

至此,完美解决混淆的问题。

六、总结

你以为就这么完了?那你就too young too simple。注意,我们在解析注解的时候使用了不少的反射,有人说这个会影响性能,的确会有一些影响。那么我们将在下一篇文章带你实现真正的butteknife——编译时注解:一步一步带你轻松打造自己的ButterKnife注解框架(下)

关于反射对性能的影响,可以参考如下文章:

最后,奉上本文的源码,欢迎有疑问的同学在下方留言,谢谢大家!

源码下载

猜你喜欢

转载自blog.csdn.net/binbinqq86/article/details/79610980