美文网首页源码分析Android开发Android知识
自己动手打造一个注解框架,彻底摆脱FindViewById

自己动手打造一个注解框架,彻底摆脱FindViewById

作者: dreamruner | 来源:发表于2017-04-29 17:10 被阅读370次

    每次写代码的时候,看到一大堆FindViewById()都挺烦的,没有没什么方法能帮我摆脱这种烦恼呢?当然是有的,目前比较流行的时注解框架是Xutils3ButterKnife,接下来我们来分析一下它们是如何实现的.

    Xutils3注解实现原理

    1. 首先看一下Xutils3的简单用法
     @ViewInject(R.id.tv_test)
        private TextView mTvTest;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            x.view().inject(this);
    
            mTvTest.setText("hello ioc");
        }
    

    用法是相当的简单,通过@ViewInject绑定id,然后通过inject(this)注入当前对象,就完成了findViewById的过程.

    1. 查看Xutils原码,看看inject(this)做了什么
    
            // inject view
            Field[] fields = handlerType.getDeclaredFields();
            if (fields != null && fields.length > 0) {
                for (Field field : fields) {
    
                    Class<?> fieldType = field.getType();
                    if (
                    /* 不注入静态字段 */     Modifier.isStatic(field.getModifiers()) ||
                    /* 不注入final字段 */    Modifier.isFinal(field.getModifiers()) ||
                    /* 不注入基本类型字段 */  fieldType.isPrimitive() ||
                    /* 不注入数组类型字段 */  fieldType.isArray()) {
                        continue;
                    }
    
                    ViewInject viewInject = field.getAnnotation(ViewInject.class);
                    if (viewInject != null) {
                        try {
                            View view = finder.findViewById(viewInject.value(), viewInject.parentId());
                            if (view != null) {
                                field.setAccessible(true);
                                field.set(handler, view);
                            } else {
                                throw new RuntimeException("Invalid @ViewInject for "
                                        + handlerType.getSimpleName() + "." + field.getName());
                            }
                        } catch (Throwable ex) {
                            LogUtil.e(ex.getMessage(), ex);
                        }
                    }
                }
            } // end inject view
    

    我们发现这里主要是通过反射,先获取该类的所有的属性,然后遍历,找到ViewInject注解,然后获取value值,即对应的viewId,然后再调用FindViewById找到对应的View,

     field.setAccessible(true);
     field.set(handler, view);
    

    然后通过上述代码注入属性,就完成了FindViewById的全过程.仔细一分析,是不是也不是那么难.主要就是利用了java的反射机制,通过动态获取Annotation,然后实现View的注入.

    属性注入 : 利用反射去 获取Annotation --> value --> findViewById --> 反射注入属性
    事件注入 :利用反射去 获取Annotation --> value --> findViewById --> setOnclickListener --> 动态代理反射执行方法

    ButterKnife注解实现原理

    1. 同样的,我们来看一下ButterKnife的简单用法
     @Bind(R.id.tv_test)
        private TextView mTvTest;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ButterKnife.bind(this);
            mTvTest.setText("hello ioc");
        }
    

    也非常简洁,两行代码完成属性注入.(这里使用的7.0.1所以是@Bind而不是@BindView);

    1. 查看源码
      ButterKnife的源码看起来就比较费劲了,主要是通过ButterKnifeProcessor这个类动态解析Annotation注解,在编译时生成xxxxx$$ViewBinder这个类,然后实现FindViewById过程.
    public class MainActivity$$ViewBinder<T extends com.example.wenjian.eassyjoke.MainActivity> implements ViewBinder<T> {
      @Override public void bind(final Finder finder, final T target, Object source) {
        View view;
        view = finder.findRequiredView(source, 2131427415, "field 'mTvTest'");
        target.mTvTest = finder.castView(view, 2131427415, "field 'mTvTest'");
      }
    
      @Override public void unbind(T target) {
        target.mTvTest = null;
      }
    }
    

    在这里我们也可以看到为什么声明属性的时候不能private

    两者实现的方式有差异,ButterKnife基于编译时注解,相对较为高效

    打造自己的注解框架

    看了别人的代码,自己也可以动手撸一下了

    1. 首先我们得了解javaAnnatation,在这里我们先参考一下java的@Override
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
    

    这里我们可以知道如何自定义注解
    @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,包

    @Retention指定何时生效,有以下三种场景

     * Annotations are to be discarded by the compiler.
     */
    SOURCE,源码
    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,编译时
    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME,运行时
    

    2.开始码自己的注解框架
    自定义@ViewById

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)  //什么时候生效:运行时
    public @interface ViewById {
        int value();
    }
    

    实现ViewUtils兼容各种场景

    public class ViewUtils {
    
        public static void inject(Activity activity) {
            inject(new ViewFinder(activity),activity);
    
        }
    
        public static void inject(View view) {
            inject(new ViewFinder(view),view);
        }
    
        public static void inject(View view, Object object) {
            inject(new ViewFinder(view),object);
        }
    
        private static void inject(ViewFinder finder, Object object) {
            injectField(finder, object);
            injectEvent(finder, object);
        }
    

    通过反射注入属性

     /**
         * 注入属性
         * @param finder
         * @param object
         */
        private static void injectField(ViewFinder finder, Object object) {
            //1.获取class
            Class<?> clazz = object.getClass();
    
            //2.获取所有的属性
            Field[] fields = clazz.getDeclaredFields();
    
            for (Field field : fields) {
                ViewById viewById = field.getAnnotation(ViewById.class);
                if (viewById != null) {
                    int viewId = viewById.value();
                    View view = finder.findViewById(viewId);
                    if (view != null) {
                        //设置可以访问所有修饰符  包括private
                        field.setAccessible(true);
                        try {
                            //注入属性
                            field.set(object, view);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    

    到此已经完成了,我们来验证一下吧

        @ViewById(R.id.tv_test)
        private TextView mTvTest;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ViewUtils.inject(this);
            mTvTest.setText("hello ioc");
        }
    

    Xutils的实现方式差不多,同样是基于反射,重要的是不必依赖第三方库了.你也自己动手实现setOnclickListener,原理是一样的.

    相关文章

      网友评论

        本文标题:自己动手打造一个注解框架,彻底摆脱FindViewById

        本文链接:https://www.haomeiwen.com/subject/hwkosttx.html