前言
在 Android 开发中,对于View
的绑定和事件响应回调,相信大家都使用过大名鼎鼎的 Butterknife。
笔者也是 Butterknife 忠实使用者,但是在长期的使用过程中,却也发现 Butterknife 还是存在一些缺陷的,或者是说用着爽之余,却总是存在一丝芥蒂。
Butterknife 旨在消除findViewById()
这种无趣却又必须的重复性代码,结合注解通过编译期注入代码方式自动为我们注入这些重复性代码。然而,现在大家很多都已经熟知 Butterknife 的实现原理:其实就是使用注解处理器在源文件编译时根据相关注解提取出所需的内容,然后生成一个新的 Java 文件,完成注入事件功能。
借助注解处理器,Butterknife 很好的实现了相应的事件注入功能,然而,也是因为注解处理器,使得其对注入事件的成员或函数的访问权限具备一定的要求(事件注入成员或函数访问权限不能为private
,因为事件注入代码生成在同包下的一个类中),无法像手写代码一样,可以是任意权限。
总结一下,笔者认为 Butterknife 具备两个相对来说比较明显的缺点:
- 事件注入成员和函数不能使用私有访问权限,封装性不彻底。
- 事件注入会经过一个反射调用生成类过程,在 Android 领域,一涉及到反射,总是会担心其性能受到限制(其实对绝大多数情况,一点反射对性能来说应该是影响不大的,毕竟 Butterknife 作为一个老牌框架,必须的缓存机制还是存在的,除了第一次反射加载生成类,后续使用都无需再次反射获取,因为已经缓存了)。
就像是 Jake Wharton 大神只是为了消除 findViewById()
这类重复性代码,就撸出了 Butterknife,我在使用过程中,觉得上面两个地方是可以消除的,做到真正的像手写代码一致的效果,于是就尝试撸出了 AsmButterknife.
原理
AsmButterknife 实现的功能与 Butterknife 完全一致,只是我认为其更接近 Butterknife 的实现意图,去除手动书写findViewById()
以及其他事件响应注册,通过注解在编译期间自动完成事件注入。之所以说更接近,是因为使用 AsmButterknife 生成的类文件与手动书写findViewById()
和其他事件响应生成的字节码完全相同,只是在编译期间,自动注入findViewById()
和事件响应这些字节码到源文件中。
因此,AsmButterknife 的实现原理其实就是 字节码注入,在 Java 编译器将源文件编译成 class 文件后,通过扫描类文件的相关注解信息,完成对应事件的字节码注入,其生成的字节码与手动书写生成的字节码是一样的,所以,在源码中使用@BindView
,@OnClick
这些注解的时候,对注入的类成员与事件响应函数的访问权限完全与手动书写代码的一致(即可以使用private
,package
,protected
,public
修饰),也无需使用Butterknife.bind()
方法去加载事件注入类,而只需通过注解指定事件注入方法,具体操作看下文。
简而言之,使用 AsmButterknife,就相当于手动书写了这些事件响应代码,只是采用了与 Butterknife 相似的注解方式。
下载
首先,配置 classpath:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.whyn:asmbutterknife-plugin:<latest-version>'
}
}
最新版本请查看:asmbutterknife-plugin
然后,apply 到你的 module:
apply plugin: 'com.whyn.plugin.asmbutterknife'
使用方法
AsmButterknife 意在实现与 Butterknife 相同的功能,因此其接口会最大限度与 Butterknife 保持一致。上文已经提及,由于底层实现的不同,AsmButterknife 无需手动去加载事件注入类,即Butterknife.bind()
函数无需使用,而其他的注解@BindView
,@OnClick
等会保持与 Butterknife 相同的命名与作用,在 Butterknife 中怎么使用,在 AsmButterknife 中就怎么使用。
下面介绍具体使用方法
- 注入事件,比如注入
findViewById
和OnClickListener
:
static class ViewHolderTest extends RecyclerView.ViewHolder {
@BindView(R.id.item)
private TextView tv; //note that we can use private modifier
@ViewInject //default value: @ViewInject(ViewInject.ViewHolder)
public ViewHolderTest (View item) {
super(item);
this.tv.setText("with @ViewInject,event bytecodes will be inject below super(item)"); //correct,no NullpointerException
}
@OnClick(R.id.item)
private void onClick() { //note that we can use private method
Toast.makeText(this.tv.getContext(), (String) this.tv.getTag(), Toast.LENGTH_SHORT).show();
}
}
rebuild
一下,看下生成的类文件:
红色方框中的代码就是注入的字节码反编译后的 Java 源码。
- 注入
Activity
:注入Activity
也可以使用上面的@ViewInject(ViewInject.ViewHolder)
方法,但是需要手动在适当的地方调用,比如:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
private TextView mTextView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View view = this.getWindow().getDecorView();
this.inject(view); //you have to call the view inject method
}
@ViewInject
private void inject(@NonNull View view){ //View must be the first argumnet
//leave it empty,bytecode will be injected into this method
}
@OnClick(R.id.tv)
private void onTextViewClick() {
Toast.makeText(this, "onTextViewClick", Toast.LENGTH_SHORT).show();
}
}
如果注入的是Activity
,那么更简单的方式如下:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
private TextView mTextView;
@Override
@ViewInject(ViewInject.ACTIVITY) //specify Activity
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//with @ViewInject(ViewInject.Activity),bytcode will be injected after setContentView
}
@OnClick(R.id.tv)
private void onTextViewClick() {
Toast.makeText(this, "onTextViewClick", Toast.LENGTH_SHORT).show();
}
}
rebuild
一下,看下生成的类文件:
红色方框中的代码就是注入的字节码反编译后的 Java 源码。可以看到,如果@ViewInject
指定的是Activity
,那么事件字节码会注入到onCreate
的setContentView
方法下面。
TODO
目前功能只完成了@BindView
和@OnClick
( ╯□╰ ),其他功能后续会不定时添加。