写一个ButterKnife的注解框架+原理分析

前言部分

本文主要是分为两个内容记录,一个是介绍ButterKnife的实现原理,不过我只是简单的以一个例子进行,比如我们常用的代替findviewbyid(int id)方法的注解;另一个是介绍一下本地简单的实现,如果理解了ButterKnife的原理自己写也没问题的了。

我记得ButterKnife的前期版本是用的运行时的注解,后来不知道在哪个版本改成编译期注解了,不过这也是顺理成章的,毕竟相对于运行时的注解方案,现在的方案更有优势,不需要通过大量的反射去调用方法,这对性能上是很有优势的(但是也有人说,现在的Java反射没有以前那么耗费性能了)。

内容部分

第一部分先介绍一下我写的Demo

我先介绍一下我写的一个简单的注解框架了,写的很简单只是简单的实现了ButterKnife的findviewbyid和onclick两个注解的功能

注解的主要步骤,如下:

  1. 首先定一个注解,这个就是你写在成员变量上的,里面只有一个int类型的属性。

  2. @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.CLASS)
    public @interface BindView {
        int value();
    }
    
  3. 然后定一个注解处理器,我们主要是通过实现AbstractProcessor类来完成对注解的处理。主要实现下面的三个方法即可。

    //jdk版本,用最新的了 
    @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    //支持的都是什么注解
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> types = new LinkedHashSet<>();
            types.add(BindView.class.getCanonicalName());
            types.add(ClickView.class.getCanonicalName());
            return types;
        }
        //所有的注解都会在这里汇集,我们在这里来做处理,比如,我们在这里生成类。里面内容比较多就不贴出来了。
         @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
          
        }
    
  4. 注解生成完了,我们就可以使用了啊,使用之前我们先写一些我们需要的api,下面就介绍一个@BindView()注解吧

    public class MainActivity extends AppCompatActivity {
    
    //我们这么使用,我们声明了成员变量,下面通过初始化上面我们注解生成类的构造函数,来给成员变量赋值。
    @BindView(R.id.tv)
    TextView tv;
    //然后再类里面
    
     @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    //        AnnotationUtils.getClassInfo("com.dongfang.eventbusdemo.MainActivity");
            DFButterKnife.inject(this);
        }
    }
    

    我们这里调用了inject()方法后,就可以初始化注解生成的类了,然后成员就会得到赋值并使用了。我们在到inject()里面看一下,代码不多我直接贴出来了,如下:

    public class DFButterKnife {
        private static final String SUFFIX = "$$" + ViewInject.class.getSimpleName();
        public static void inject(Activity activity) {
            inject(activity, activity);
        }
        public static void inject(Object host, Object root) {
            Class<?> clazz = host.getClass();
            //拼接类的全路径,
            String proxyClassFullName = clazz.getName() + SUFFIX;
            Class<?> proxyClazz = null;
            try {
                proxyClazz = Class.forName(proxyClassFullName);
                //通过newInstance生成实例,强转,调用代理类的inject方法
                ViewInject viewInject = (ViewInject) proxyClazz.newInstance();
                //调用生成类里面的inject方法,进行findView
                viewInject.inject(host, root);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    

    上面的核心内容主要是通过传入的class来拼接出,注解预先生成的class,然后通过反射来生成类的实例,在通过实例来调用inject()方法。我们去看一下生成类的这个方法。如下:

    public class MainActivity$$ViewInject implements ViewInject<com.dongfang.eventbusdemo.MainActivity> {
        public com.dongfang.eventbusdemo.MainActivity tage;
        public View viewTage;
        @Override
        public void inject(com.dongfang.eventbusdemo.MainActivity host, Object source) {
            tage = host;
            if (source instanceof android.app.Activity) {
                host.tv = (android.widget.TextView) (((android.app.Activity) source).findViewById(2131165313));
            } else {
                host.tv = (android.widget.TextView) (((android.view.View) source).findViewById(2131165313));
            }
            //完成成员变量注解  
            if (source instanceof android.app.Activity) {
                viewTage = (((android.app.Activity) source).findViewById(2131165217));
            } else {
                viewTage = (((android.view.View) source).findViewById(2131165217));
            }
            viewTage.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    tage.onClick();
                }
            });
        }
    }
    

    这个和ButterKnife的有一点点不同啦,初始化是在inject()方法中,而ButterKnife是在构造成完成这个操作。到了这步骤就算是完成了,但是十分简陋的一个demo。达不到使用的标准。

到这这里我的demo就算是完成了,其实现在注解能做的东西有很多,因为注解大大的减少了类之间的耦合。


第二部分介绍一下源码(简单的一带而过)

我们在项目中使用 @BindView(R.id.ll_main_tab) 注解,然后项目在编译的过程中生成添加了注解的的类的对应的一个 类名为 “类名_ViewBinding” 的类,里面存储我们需要的注解的内容,比如为view中的成员变量赋值等。

这是使用注解的前提,因为注解生成了这个类,我们其实在调用方法或者使用变量的时候是不需要反射的

上面所说的成员变量的赋值,都是在这个生成的类的构造中完成。下面我们可以看到,通过ButterKnife.bind(this);调用生成的类的构造函数。

这里说明一下,这只是最简陋的原理了,其实ButterKnife中做的更加详细和高效,比如,我们在运行项目后,每个注解后的类在打开后,都会存储到一个静态的map中,就是其实在页面打开一次后,就不会重复的去做调用_ViewBinding的构造函数,而是在记录的集合中去取,提高运行效率。

下面开始我们的源码跟读,首先我们找到一个入口,如下:

//点一下
ButterKnife.bind(this);
//到这里了
@NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    //拿到了rootview
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
  }

好吧大家都是这么开始的,下面开始进入到有内容的方法了,

 @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    //找一下Constructor,点进去看一下
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    if (constructor == null) {
      return Unbinder.EMPTY;
    }
    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      //省略。。。
    } 
  }

下面在进入到findBindingConstructorForClass()看一下吧。这个方法中主要是对注解生成的类进行了实例化,然后就并且存入到上面的map中,提高以后再次查找的效率。

@Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      //这里就是我们生成的类的名称了,就是在我们传入的类的后面➕_ViewBinding就是类名。
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked。这里调用了我们生成的类的构造函数,里面就进行了我们上面的findRequiredView()方法
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } catch (ClassNotFoundException e) {
      //省略。。。
    }
      
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

上面的步骤完成基本上你的activity中的成员变量已经进行了赋值,下面你就可以愉快的使用了。

结尾

上面就是对自己学习过程的记录吧,感觉写的很粗糙,其实注解这一块是可以详细的来展开讲的,但是我发现了一个很不错的文章,所以这里推荐出来。注解分析

写的很浅显易懂,读起来不安么枯燥。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值