一. ButterKnife介绍
在Android编程过程中,我们会写大量的布局和点击事件,像初始view、设置view监听这样简单而重复的操作,这些代码繁琐而又不雅观,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
TextView tvSetName = findViewById(R.id.xxx);
tvSetName.setOnClickListener(
new
View.OnClickListener() {
@Override
public
void
onClick(View v) {
//xxxx
}
});
TextView tvSetAge = findViewById(R.id.xxx);
tvSetAge.setOnClickListener(
new
View.OnClickListener() {
@Override
public
void
onClick(View v) {
//xxxx
}
});
TextView tvSetArea = findViewById(R.id.xxx);
tvSetArea.setOnClickListener(
new
View.OnClickListener() {
@Override
public
void
onClick(View v) {
//xxxx
}
});
|
Activity中这种代码多了之后,很不雅观。
二. 使用简介
ButterKnife使用方法比较简单,主要包括以下步骤:
引用ButterKnife包
在onCreate里面bind(setContentView之后)
绑定各种事件
onDestroy里面解绑释放资源
三. ButterKnife源代码下载
直接git clone或者下载zip即可。
四. 编译
Android studio打开ButterKnife源代码
AndroidStudio->File->open->ButterKnife源代码路径->确认
Build->Rebuild Project
五. 生成的aar和jar包
生成的包主要有两个
butterknife-annotations-8.5.2-SNAPSHOT.jar
路径:butterknife-annotations->build->libs
butterknife-release.aar
路径: butterknife->build->outputs->aar
六. 其他应用引用自定义ButterKnife包
删除原来ButterKnife包引用,因为要使用自己编译的包
// compile 'com.jakewharton:butterknife:8.4.0'
拷贝文件
拷贝上面两个文件到自己项目app模块的libs 目录
添加aar的关联
打开app模块的build.gradle文件,添加:
-
12
compile fileTree(include: ['*.jar'], dir: 'libs')
compile(name: 'butterknife-release', ext: 'aar')
Build->Rebuild Project
七. 源代码分析
1. ButterKnife.bind(Activity target)过程
文件名:ButterKnife.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
static
final
Map<Class<?>, Constructor<?
extends
Unbinder>> BINDINGS =
new
LinkedHashMap<>();
public
static
Unbinder bind(
@NonNull
Activity target) {
Log.d(
"Sandy"
,
"ButterKnife bind.. target: "
+ target);
View sourceView = target.getWindow().getDecorView();
return
createBinding(target, sourceView);
}
private
static
Unbinder createBinding(
@NonNull
Object target,
@NonNull
View source) {
Class<?> targetClass = target.getClass();
if
(debug) Log.d(TAG,
"Looking up binding for "
+ targetClass.getName());
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) {
throw
new
RuntimeException(
"Unable to invoke "
+ constructor, e);
}
catch
(InstantiationException e) {
throw
new
RuntimeException(
"Unable to invoke "
+ constructor, e);
}
catch
(InvocationTargetException e) {
Throwable cause = e.getCause();
if
(cause
instanceof
RuntimeException) {
throw
(RuntimeException) cause;
}
if
(cause
instanceof
Error) {
throw
(Error) cause;
}
throw
new
RuntimeException(
"Unable to create binding instance."
, cause);
}
}
private
static
Constructor<?
extends
Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<?
extends
Unbinder> bindingCtor = BINDINGS.get(cls);
if
(bindingCtor !=
null
) {
if
(debug) Log.d(TAG,
"HIT: Cached in binding map."
);
return
bindingCtor;
}
String clsName = cls.getName();
if
(clsName.startsWith(
"android."
) || clsName.startsWith(
"java."
)) {
if
(debug) Log.d(TAG,
"MISS: Reached framework class. Abandoning search."
);
return
null
;
}
try
{
Log.d(
"Sandy"
,
"findBindingConsForClass: "
+ clsName +
" vindBinding name: "
+
clsName +
"_ViewBinding"
);
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName +
"_ViewBinding"
);
//noinspection unchecked
bindingCtor = (Constructor<?
extends
Unbinder>) bindingClass.getConstructor(cls, View.
class
);
if
(debug) Log.d(TAG,
"HIT: Loaded binding class and constructor."
);
}
catch
(ClassNotFoundException e) {
if
(debug) Log.d(TAG,
"Not found. Trying superclass "
+ cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
}
catch
(NoSuchMethodException e) {
throw
new
RuntimeException(
"Unable to find binding constructor for "
+ clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return
bindingCtor;
}
|
上面这段代码有几个注意点:
a. sourceView代表是DecorView,也就是我们窗口的顶级View。
b. findBindingConstructorForClass有个BINDINGS缓存,key是class,value是缓存的Unbinder对象,这样做可以加快bind速度。
因为每个类的ButterKnife注解在运行期间是不会变的,比如MainActivity有3个ButterKnife注解,那么它就是3个。除非有新的apk安装。
所以适合用缓存来实现。
c. findBindingConstructorForClass使用了递归的方法
这个方法使用了递归,不断调用父类,也就是
1
2
3
4
|
catch
(ClassNotFoundException e) {
if
(debug) Log.d(TAG,
"Not found. Trying superclass "
+ cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
}
|
那为什么要这么处理呢?
因为有些Activity没有ButterKnife的注解,但是它的父类可能有,比如BaseActivity。所以需要往上递归。那什么时候递归结束呢?
1
2
3
4
5
6
7
8
9
|
if
(bindingCtor !=
null
) {
if
(debug) Log.d(TAG,
"HIT: Cached in binding map."
);
return
bindingCtor;
}
String clsName = cls.getName();
if
(clsName.startsWith(
"android."
) || clsName.startsWith(
"java."
)) {
if
(debug) Log.d(TAG,
"MISS: Reached framework class. Abandoning search."
);
return
null
;
}
|
如果缓存里面找到了结果,那么结束,同时返回结果;
或者类名以"android."或者"java."开头,也结束,返回null;
以Activity为例,Activity的类名是android.app.Activity,所以你的MainActivity如果递归到Activity还没有找到ButterKnife注解,那就说明你的MainActivity是没有包含ButterKnife注解的。
d. 如果子Activity和父Activity都有ButterKnife注解怎么办?
答案是返回子Activity以及其对应的 Constructor<? extends Unbinder> bindingCtor对象
那它的父Activity如果也有ButterKnife注解怎么办?怎么解析父Activity的ButterKnife注解呢? 这个问题我们待会再讲。
记为问题1。
e. Constructor<? extends Unbinder> 是个什么东西?
调用ButterKnife.bind(Activity target)方法后会返回一个Unbinder对象,可以在onDestroy中调用unbind()方法,那个Unbinder是什么东西呢?这个问题待会再讲。
记为问题2.
f. clsName + "_ViewBinding"是什么类?
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
这个问题记为问题3.
2. ButterKnifeProcessor.java
这个类是ButterKnife里面很重要的一个类了,它继承自AbstractProcessor。
看来不懂的问题越来越多,那么有必要来学习下Java注解的知识。
八. Java注解
在分析ButterKnife代码前,需要了解Java的注解,需要了解Annotation Processor,因为ButterKnifer用到这个知识。
这里面很重要的一个知识点就是你可以编写一定的规则,让它在应用程序编译时执行你的规则,然后生成Java代码;并且生成的Java还可以参与编译。
九. 自己定义的注解框架
1. Eclipse实现
主要是参考这篇帖子完成的,大家可以参考这篇帖子:
主要说下不同的地方:
a. source folder的创建,直接File->New->source folder一直创建不成功,后面用另外一种方法创建成功了。
项目->右击->Properties->Java Build Path->Source->Add Folder->Create New Folder->输入resources/META-INF/services->finish->ok->ok
2. Android studio实现
AndroidStudio下面使用Java注解Processor
十. 调试自己的自定义框架
有个时候需要调试自己写的框架是否正常运行,下面介绍下调试:
1. 在项目gradle.properties里面添加
1
2
|
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8011
|
2.Edit Configureations
3. 增加远程调试
4. 启动远程调试
下面的控制台会出现下面的提示:
Connected to the target VM, address: 'localhost:8011', transport: 'socket'
5. 打断点
在Processor里面打上断点,比如init, process
6. 连上手机,项目根目录命令行下执行
gradle clean connectedCheck
十一. ButterKnife使用Java注解
理解了Java注解Processor之后,ButterKnife就比较好理解了。
首先它的ButterKnifeProcessor.java继承自AbstractProcessor,重写了init和process之类的方法。
也就是说它在编译的时候会被执行,生成辅助代码。
它的辅助代码生成到哪里了呢?
在我们自己的应用程序里面搜索_ViewBinding,就可以找到已经生成好的辅助类,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
public
class
xxxx_ViewBinding<T
extends
LoginCloudActivity>
extends
BaseActivity_ViewBinding<T> {
private
View view2131689593;
@UiThread
public
xxxx_ViewBinding(
final
T target, View source) {
super
(target, source);
View view;
target.mEtUser = Utils.findRequiredViewAsType(source, R.id.et_user,
"field 'mEtUser'"
, EditText.
class
);
target.mEtPwd = Utils.findRequiredViewAsType(source, R.id.et_pwd,
"field 'mEtPwd'"
, EditText.
class
);
view = Utils.findRequiredView(source, R.id.btn_login,
"method 'btn_login' and method 'btn_login_long'"
);
view2131689593 = view;
view.setOnClickListener(
new
DebouncingOnClickListener() {
@Override
public
void
doClick(View p0) {
target.btn_login();
}
});
view.setOnLongClickListener(
new
View.OnLongClickListener() {
@Override
public
boolean
onLongClick(View p0) {
return
target.btn_login_long();
}
});
}
@Override
public
void
unbind() {
T target =
this
.target;
super
.unbind();
target.mEtUser =
null
;
target.mEtPwd =
null
;
view2131689593.setOnClickListener(
null
);
view2131689593.setOnLongClickListener(
null
);
view2131689593 =
null
;
}
}
|
这个类在编译的时候会被自动生成,那么在运行的时候,它会被调用。
这个类的构造函数会去初始化那些控件,设置监听。
回到第七步 ButterKnife.bind()的过程
在createBinding的时候,它会初始化这个xxx_ViewBinding类,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private
static
Unbinder createBinding(
@NonNull
Object target,
@NonNull
View source) {
Class<?> targetClass = target.getClass();
if
(debug) Log.d(TAG,
"Looking up binding for "
+ targetClass.getName());
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);
...
}
|
那么就会走到xxx_ViewBinding的构造函数,那么就会初始化控件,同时也会设置监听。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
target.mEtUser = Utils.findRequiredViewAsType(source, R.id.et_user,
"field 'mEtUser'"
, EditText.
class
);
target.mEtPwd = Utils.findRequiredViewAsType(source, R.id.et_pwd,
"field 'mEtPwd'"
, EditText.
class
);
view = Utils.findRequiredView(source, R.id.btn_login,
"method 'btn_login' and method 'btn_login_long'"
);
view2131689593 = view;
view.setOnClickListener(
new
DebouncingOnClickListener() {
@Override
public
void
doClick(View p0) {
target.btn_login();
}
});
view.setOnLongClickListener(
new
View.OnLongClickListener() {
@Override
public
boolean
onLongClick(View p0) {
return
target.btn_login_long();
}
});
|
它的调用方式直接是target.mEtpwd,所以也就是说Activity的mEtpwd控件不能是private的,否则会引用不到。
参考网址: