利用编译时注解实现一个ButterKnife

ButterKnife 这个框架真的很好用,简化了大量的代码,在效率上和手写的代码相比几乎没有损失。虽然会用 ButterKnife,同时 ButterKnife 的原理也要明白。

于是,找到 ButterKnife 最早的 1.0 版本,学习一下它的原理,并模仿一下。

最终效果

在 Activity 中使用注解绑定控件

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends AppCompatActivity {

@InjectView(R.id.tv)
public TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Views.inject(this);
mTextView.setText("TextView");
}
}

框架根据注解生成的代码如下

1
2
3
4
5
public class MainActivity$$ViewInjector {
public static void inject(MainActivity activity) {
activity.mTextView = (android.widget.TextView) activity.findViewById(2131427413);
}
}

项目结构

  • app
  • annotation
  • compiler
  • api

app

就是我们的android项目

annotation

定义注解,InjectView 就放在这个 module 中

compiler

注解解析器,生成代码

api

定义 Butterkinfe 的 API,比如我们常用的 Butterknife.bind()

annotation

在 Android Studio 点击 File - New Module - Java Library 新建一个 Java Module

build.gradle 内容如下

1
2
3
4
apply plugin: 'java'

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

然后定义一个注解

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface InjectView {
int value();
}

compiler

有了注解,接着就需要一个注解解析器,去解析注解然后生成代码。

同样的,建立一个 Java Library,build.gradle 的定义如下

1
2
3
4
5
6
7
8
9
apply plugin: 'java'

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

dependencies {
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile project(':annotation')
}

在这里引入了 Google 的 auto-service 依赖。它可以用来帮助生成 META-INF 文件

接着就是创建注解解析器,实现 process 方法

1
2
3
4
5
6
7
@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
return false;
}
}

这个方法中,主要做了几件事

  1. 找到所有使用 InjectView 注解的类
  2. 解析 InjectView 注解的信息
  3. 根据信息生成代码

api

创建一个 Android Library,建立一个 Views

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import android.app.Activity;

import java.lang.reflect.Method;

public class Views {

private Views() {
// No instances.
}

public static void inject(Activity activity) {
try {
Class<?> injector = Class.forName(activity.getClass().getName() + "$$ViewInjector");
Method inject = injector.getMethod("inject", activity.getClass());
inject.invoke(null, activity);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Unable to inject views for activity " + activity, e);
}
}
}

因为,我们生成代码的类名是 Activity的类名+$$ViewInjector 。比如一个 Activity 叫 HelloActivity,那么生成的代码的类名就是 HelloActivity$$ViewInjector。因此这里利用反射,根据类名的规则找到我们生成的类,调用 inject() 方法。

当然,也可以不使用反射,直接调用生成好的类 HelloActivity$$ViewInjector.inject(Activity activity),这样也是可以的。但是有个弊端,每次注解完毕只会,都需要 rebuild 一次 project,等代码生成好之后,才能调用 HelloActivity$$ViewInjector.inject(Activity activity)。这样也挺麻烦了,还是直接用注解吧。

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

android {
compileSdkVersion 24
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.okada.viewinject"
minSdkVersion 14
targetSdkVersion 24
versionCode 1
versionName "1.0"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

}

dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:24.2.1'
compile project(':annotation')
apt project(':compiler')
compile project(':api')
}

同时在项目的 build.gradle 定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // 加上这句话
}
}

allprojects {
repositories {
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

现在可以使用注解了

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends AppCompatActivity {

@InjectView(R.id.tv)
public TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Views.inject(this);
mTextView.setText("TextView");
}
}

然后点击 Build - Rebuild Project,就可以在 app - build - generated - source - apt 文件夹中看到生成的代码了

项目源码

https://github.com/okadaNana/ViewInject

参考来源