写一个 ButterKnife

ButterKnife 很多人都用过,能节省很多代码,最多的就是省去了很多 findViewById 语句。接下来自己写一个,就叫 BBKnife 吧。

分析

在使用 ButterKnife 时,需要书写下面的类似代码,以一个 Activity 为例

ExampleActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
class ExampleActivity extends Activity {
@BindView(R.id.title) TextView title;
@BindView(R.id.subtitle) TextView subtitle;
@BindView(R.id.footer) TextView footer;

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}

在编译后,会自动生成一个和 Activity 同名的 ExampleActivity$ViewBinder 的辅助类文件,并且生成 findViewById 相关的代码

ExampleActivity$ViewBinder.java

1
2
3
4
5
6

public void bind(ExampleActivity activity) {
activity.subtitle = (android.widget.TextView) activity.findViewById(2130968578);
activity.footer = (android.widget.TextView) activity.findViewById(2130968579);
activity.title = (android.widget.TextView) activity.findViewById(2130968577);
}

那么 BBKnife 库要做的事情就很清楚了,生成辅助类。

动手开写

步骤:

  1. 创建注解
  2. 编译期间处理注解
  3. 生成辅助类
  4. 调用辅助类

0x00 创建工程

首先创建一个新的工程

然后创建一个 module , 选择 java Library。

0x01 创建注解

这个注解作用于类的属性上面,包含一个整型的参数,类似于 @BindView(R.id.title)

1
2
3
4
5
6
7
8
9
/**
* Created by hanks on 2016/7/31.
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}

0x02 编译期间处理注解 && 生成辅助类

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 编译期间处理注解
* Created by hanks on 2016/7/31.
*/
@SupportedAnnotationTypes("xyz.hanks.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindViewProcessor extends AbstractProcessor {
private Messager messager;
public static final String SUFFIX = "$ViewBinder";

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Map<String, List<VariableElement>> map = new HashMap<>(); // key 是类名,value 是该类的注解元素
// 遍历 BindView 注解的所有元素
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
if (element == null || !(element instanceof VariableElement)) {
continue;
}
// 给属性添加的注解
VariableElement variableElement = (VariableElement) element;
// 获取属性所在的类名
String className = element.getEnclosingElement().getSimpleName().toString();
List<VariableElement> variableElementList = map.get(className);
if (variableElementList == null) {
variableElementList = new ArrayList<>();
map.put(className, variableElementList);
}
variableElementList.add(variableElement);
}

// 生成辅助类
generate(map);
return true;
}

private void generate(Map<String, List<VariableElement>> map) {
if (null == map || map.size() == 0) {
return;
}
for (String className : map.keySet()) {
List<VariableElement> variableElementList = map.get(className);
if (variableElementList == null || variableElementList.size() <= 0) {
continue;
}
// 获取包名
String packageName = variableElementList.get(0).getEnclosingElement().getEnclosingElement().toString();
StringBuilder builder = new StringBuilder()
.append("package ").append(packageName).append(";\n\n")
.append("public class ").append(className).append(SUFFIX).append("{\n") // open class
.append(" public void bind(Object target) {\n")
.append(" ").append(className).append(" activity = (").append(className).append(")target;\n");

for (VariableElement variableElement : variableElementList) {
BindView bindView = variableElement.getAnnotation(BindView.class);
log(bindView.toString());
builder.append(" activity.").append(variableElement.getSimpleName().toString()).append("=(").append(variableElement.asType()).append(")activity.findViewById(").append(bindView.value()).append(");\n");
}
builder.append(" }\n}\n");
// write the file
try {
String bindViewClassName = packageName + "." + className + SUFFIX;
JavaFileObject source = processingEnv.getFiler().createSourceFile(bindViewClassName);
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
log(e.getMessage());
}
}
}

private void log(String msg) {
messager.printMessage(Diagnostic.Kind.WARNING, msg);
}

}

注意点:

  • 添加 @SupportedAnnotationTypes @SupportedSourceVersion 注解, 原因: AbstractProcessor 中做了相关校验(看 AbstractProcessor 源码)。
  • 打印消息是由 processingEnv.getMessager().printMessage 或者输出日志文件,原因:编译期间做的处理,不能使用 System.out 或者 Log.i
  • 处理注解的时候需要获取类名或者包名,需要注意获取的是全路径还是简单名称。
  • 依照需要生成辅助类文件。

在 main 下新建 resources > META_INF > services 目录,创建 javax.annotation.processing.Processor 文件,javac 会自动检查和读取 javax.annotation.processing.Processor 中的内容,并且注册 BindViewProcessor 作为注解处理器。

0x03 调用辅助类

当执行 BBKnife.bind(activity) 的时候调用我们生成的辅助类,辅助类内部进行 findViewById 从而进行注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BBKnife {
// 调用我们生成的辅助类
public static void bind(Object view){
try {
String cla = view.getClass().getName()+BindViewProcessor.SUFFIX;
Class clazz = Class.forName(cla);
Object instance = clazz.newInstance();
Method bind = clazz.getMethod("bind",Object.class);
bind.invoke(instance,view);
} catch (Exception e) {
e.printStackTrace();
}
}
}

导出 bbknife.jar

在 build/libs 目录下有自动导出的 jar 文件,

使用

复制到 app 下的 libs 进行引用。

app 下的 build.gradle

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
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // 使用 apt

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // 添加引用
}
}

android {
compileSdkVersion 24
buildToolsVersion "24.0.1"

defaultConfig {
applicationId "xyz.hanks.bbknifeproject"
minSdkVersion 15
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 设置java 版本
compileOptions{
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.1.1'
}

在 MainActivity 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class MainActivity extends AppCompatActivity {

@BindView(R.id.bb_button) Button mButton;
@BindView(R.id.bb_image) ImageView mImage;
@BindView(R.id.bb_text) TextView mText;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

BBKnife.bind(this);

mButton.setText("hanks.xyz");
mText.setText("hanks.xyz");
mImage.setImageResource(R.mipmap.ic_launcher);
}
}

看最后生成的辅助类 MainActivity$ViewBinder.java

1
2
3
4
5
6
7
8
9
10

public class MainActivity$ViewBinder{
public void bind(Object target) {
MainActivity activity = (MainActivity)target;
activity.mButton=(android.widget.Button)activity.findViewById(2131427412);
activity.mImage=(android.widget.ImageView)activity.findViewById(2131427414);
activity.mText=(android.widget.TextView)activity.findViewById(2131427413);
}
}

参考链接

https://blog.stablekernel.com/the-10-step-guide-to-annotation-processing-in-android-studio

文章来自: https://hanks.pub