手写butterknife来剖析其原理

基本使用:

对于butterknife库我想基本上都非常熟了,如今在项目中用它也用得非常之频繁了,不过为了学习的完整性,先来简单的回顾一下基本用法,先新建一个工程:

然后给textview增加一个点击事件,做个超简单的事:

运行效果:

这是我们通常的做法,而有了butterknife之后,则使用会更加的简单,解决的其实就是findViewById这个比较机械式又不得不写的代码,下面简单来用一下butterknife,一切原理的剖析都需要建立在会用的基础之上,先上一下它的官网:

不过多解释,先来将其集成到工程中来使用一下:

编译报错了。。

Manifest merger failed : Attribute application@appComponentFactory value=(android.support.v4.app.CoreComponentFactory) from [com.android.support:support-compat:28.0.0] AndroidManifest.xml:22:18-91
    is also present at [androidx.core:core:1.0.0] AndroidManifest.xml:22:18-86 value=(androidx.core.app.CoreComponentFactory).
    Suggestion: add 'tools:replace="android:appComponentFactory"' to <application> element at AndroidManifest.xml:5:5-19:19 to override.

其实这个错误就是我们的项目的某些属性和第三方库中的属性有冲突时或者我们想修改第三方库中某些资源的属性时,我们就需要使用tools:replace来处理。其实最新版的butterknife在androidX上会一些冲突,按建立来弄也会报其它错,所以简单起见降一下版本,这里采用这个版本:

编译:

butterknife里面使用了lambda,所以需要指定一下JDK版本才行,如下:

好接下来则使用一下butterknife,如下:

使用比较简单,不多说,接下来重点就是我们自己从0开始来自己实现一个这样的功能来了解butterknife的原理机制。

利用反射自己来实现:

接下来咱们新建一个Library:

然后我们将butterknife的依赖给去掉,因为我们要自己来实现这样的效果:

此时肯定会报错了:

咱们先将点击事件的报错给去掉,先实现@BindView的功能:

然后咱们在我们的library中新建一个注解BindView:

然后咱们再来新建一个ButterKnife类,如下:

然后我们的主工程依赖一下我们新建的这个library,如下:

然后导一下我们自己的包,就不会报错了:

目前BindView这个注解还没加限定所以报错了,关于注解第一个需要配置的是它的存活周期,如下:

其中可以指定如下选项:

咱们这里用第三个,因为在运行时需要进行反馈的,所以指定一下:

接下来需要指定一个target,如下:

看一下它可以指定哪些选项:

指定它只能加在成员变量上之后,我们如果将此注解加在非成员变量上则会报错的,比如放在类上,如下:

其它注解是一个特殊的接口,所以里面可以定义方法,下面来学习下它的基本用法,咱们先定义一个方法:

此时就可以这样用了:

再增加一个方法:

其中还可以有默认值,如下:

这样我们在调用时不传name也不会报错,因为有默认值了,如下:

另外还有一个小细节,对于Butterknife而言,貌似写法并没有xxx=,如下:

其实是因为对于注解来说,如果定义的唯一的方法名是value,其赋值就可以省略了,如下:

此时的调用写法就跟butterknife一模一样啦,如下:

至此,注解就定义好了,接下来则需要对其注解进行处理,也就是它:

为了更加专注,将调用程序改得更加简单一些,完全不要点击事件,专心实现findViewById功能,如下:

好,焦点就回到了ButterKnife的bind()方法了,肯定得要用反射技术了,实现比较简单,如下:

这就是利用反射对butterknife的简单实现,当然这是最初级的版本,但是能看到butterknife的一些原理。

利用AnnotationProcessor自己来实现【高仿butterknife】:

1、将查找View代码拆到新类MainActivityBinding,并ButterKnife.bind()方法利用反射来调用该类。

接下来咱们再新建一个module,来还原一下真正butterknife是如何实现的,如下:

而回顾一下利用反射的方式,其实是自己做了findViewById了,如下:

接下来为了往ButterKnife的原理靠,需要转换一下思路,先去掉这个反射库的依赖:

此时肯定会报错:

咱们这时依赖一下我们新建的lib:

然后在里面新建一个ButterKnife类,里面的bind()方法先空实现:

此时重新导一下包之后就只有注解这块飘红了:

咱们第一步先不用这个注解,一步步来变成ButterKnife真正的样子,所以代码会变成:

接下来就是得实现bind()方法了,这里的思路转变是此bind()里面的findView的过程放到另外一个类,而这里面通过反射去调用它来,具体做法如下,新建一个类,注意是在主功程里面:

然后里面只定义一个构造方法,并且构造方法中有初始化view的代码,具体如下:

就是这么简单直接,这里不采用反射方式了,毕境反射还是有性能问题,那此类新建的目的是为了啥呢?其实就是将来要将这个类动态生成,而不用人为去手动编写到,然后在ButterKnife.bind()方法去通过反射来调用此类,具体形态先手动模拟一下:

其中,这里涉及到一个貌似不怎么常用的API:

它跟getName()很类似:

当有内部类的时候这俩显示就有区别了,下面实验一下:

所以将测试代码还原,还是回到bind()方法继续来编写:

比较容易理解,也就是用反射来调用MainActivityBinding这个未来要通过自动生成的查找View的类,运行发现一切都正常,目前这是ButterKnife的雏形。

2、想办法利用annotationprocessor来自动生成MainActivityBinding:

接下来首先得要了解annotationprocessor的用法,所以需要再新建一个library,不过这里只需要Java Library既可,不需要Android Library了,因为它只是用来生成代码用的,如下:

注意这里用的是annotionProcessor来进行依赖了,接着还得在这个lib里面按一定的格式来新建文件,固定规则,如下:

然后再建一个子目录,如下:

再建一个目录:

然后再建一个文件:

然后将我们的BindProcessor文件的全路径在这个文件中进行声明,如下:

这样注册的目的就是告诉gradle,由于在工程中有这个依赖:

所以它就会找lib-processor这个项目中的指定目录下注册的BindProcessor,所以接下来就是专心来编写BindProcessor,首先需要继承一个类:

这样就可以在编译阶段来根据我们编写的processor的代码来生成指定的代码。首先来写这个方法:

但是!!如今gradle依赖上有个这样的问题,先来挼一挼:

那最好的办法就再新建一个Java Library专门定义注解然后共享给它们,所以再新建一个:

然后再来定义该注解,首先声明其使用范围,还记得用反射来实现时它的定义么?

但是!!咱们此时需要改一改了,因为只需要在编译阶段中看到,在运行阶段是不需要看见的,所以更改如下:

接下来定义一下target:

然后再定义一个方法:

好,接下来定义好依赖关系:

所以在lib-processor中添加对lib-annotations的依赖:

接下来继续:

所以我们的主功能也需要添加对lib-annotations的依赖,但是由于主功能已经依赖于lib-annatationprocessor,如下:

因为我们写的library是要被用户使用的,能尽量让用户少加一个依赖就尽量不用,所以此时可以将此依赖放到lib-annotationprocessor来了,但是此时只要用一个传递依赖既可:

好,此时依赖关系就定义好了,比较容易混迷糊,下面用图来对目前的依赖关系简单画一下:

挼清依赖关系之后,接下来来处理我们的processor逻辑:

好,接下来再来实现它里面关键的process()方法,在正式编码之前先来打印一下,看是否在编译时执行了:

然后在编译之前得在应用层先使用一下注解才行,这样才能扫描到让我们的processor发挥作用,如下:

gradle编译一下:

发现木有打印出来呀,啥情况呢?其实是因为配置目录这块有错:

更改一下:

再次运行:

ok,木啥问题,那证明目前咱们的配置一切正常,接下来要做的就是利用这个processor来自动生成这个类,再生成正式的之前我们先来做个实验生成一个简单的测试类,在保证生成一切正常之后再来生成最终我们想要的:

那怎么实现呢?其实是有工具的,需要再重写一个方法:

但是!!实际可以更加方便的工具来写,叫:javapoet,下面先来增加它的依赖:

下面则用它来生成我们想要的这个类,怎么写,直接看:

好,编译一下,看是否Test这个类生成了:

好,接下来就需要生成我们最终想要的MainActivityBinding这个类了,具体代码就不细说了,直接贴出来:

此时将咱们自己的类给删除掉:

再编译一下:

此时再运行一正常啦,此时我们已经手动将之前我们写的MainActivityBinding类已经删掉改用processor自动生成了,这其实也就是butterknife最核心的做法,可以发现用它并不怎么影响性能,因为只是编译期间会费时一点,但是打包之后其实就是我们平常自己写的那个findViewById,只是说在我们调用ButterKnife.bind(this);用反射主动调用了一下我们生成的类而已,说实话在不了解其原理之前我对于butterknife使用是表示抗拒的,但通过自己实现一遍之后发现确实是个好东东,等于就是将我们双手给解放了,本质跟我们手动写也没啥大的区别。

不过还差最后一步,目前我们生成的代码是硬编码的,如下:

这里就需要用到process传过来的两个参数啦:

第一个参数表示总注解,目前我们只使用了BindView这个注解,所以这个集合只会有一个元素。

第二个参数可能程序中有多个processor,每个processor都对应一个RoundEnvironment。

接下来我们来改造一下,将其写活:

拿类来说明它们俩:

所以咱们可以遍历EnclosedElements,来获取注解:

具体怎么写活呢,直接贴代码就不多解释了,其实也就是细节处理:

为了防止生成的类跟我们写的类重名,所以规则加了一个“$”:

所以我们的bind()方法也得增加一个“$”:

再编译:

由于变成动态生成了,所以我们可以随意更改id之类的,最后我们来试一下:

再次编译:

至此!!关于butterknife的整个原理过程完全剖析完啦,过程还是挺麻烦的,但走一遍之后有种茅塞顿开的感觉~~

posted on 2019-03-22 23:55  cexo  阅读(548)  评论(0编辑  收藏  举报

导航