简单回顾一下ButterKnife使用BindView注解初始化View过程:
- 使用注解@BindView(R.id.main_title_view),并将要绑定的id传入
- 调用BufferKnife.bind(this)方法开始绑定
其实最核心的是ButterKnife使用了APT技术,也就是Annotation Processing Tool的简称,翻译成中文就是"注解处理工具":
-
它的作用是在编译阶段处理注解,生成相应的java文件,然后在运行阶段,通过反射调用那些生成的java类中的方法来进行绑定等操作
-
生成的Java文件内容很简单,就是把当前Activity或者View传入进来,然后findViewById找到对应View并给带有注释的那些View赋值,所以使用BindView注解的变量都要声明为public
public final class BindProxy$MainActivity { public BindProxy$MainActivity(MainActivity mainactivity) { mainactivity.mTitleView = (android.widget.TextView) mainactivity.findViewById(2131165289); } }
- 说简单一点就是以前在Activity的那些findViewBy代码,ButterKnife帮我们通过注解配置的方式,自动转移到生成的这些类里面了
- 所以BufferKnife.bind(this)的作用就是调用那些自动生成的代码
其实我们也可以省去APT生成代码那一步,直接代码运行时解析那些注解,然后通过反射赋值,也不需要固定带注解的变量都是public的,比如下面这样:
/**
* 使用注解代替FindViewById()
*/
private void autoFindViews() {
List<Field> fields = ClassUtil.getAllField(getClass());
for (Field field : fields) {
if (field.isAnnotationPresent(BindView.class)) {
BindView bindView = field.getAnnotation(BindView.class);
int viewId = bindView.value();
if (viewId > 0) {
//反射访问私有变量需要设置这个
field.setAccessible(true);
try {
View view = findViewById(viewId);
if (view != null) {
if (field.isAnnotationPresent(BindClick.class)) {
view.setOnClickListener(this);
}
field.set(this, view);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
那么为什么还有那么多人用BufferKnife,而不是自己用几行代码反射搞定呢?
这主要还是取舍问题,通过上面我们知道BufferKnife也不是特别完美的:
- 它需要把原来私有变量都变成public,违反了"地米特法则"
- 增加了APT生成类的过程,这意味着项目编译时间变长了
但是如果简单一点使用反射,则会消耗更多的系统资源,比较适合项目还比较小,使用反射对整个项目性能影响不大的情况,而BufferKnife则适合较大项目,在编译时处理好,运行时减少使用反射节提高性能
下面我们使用简单的项目来模拟BufferKnife解析注解绑定View过程
1、创建名叫"annotation"的Java Library类型Module:主要用于存放自定义的注解以及一些常量
-
build.gradle:
apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) } sourceCompatibility = "7" targetCompatibility = "7"
-
创建一个注解类型的java类:BindView.java
- ElementType.FIELD表示该注解只能用在变量上
- RetentionPolicy.CLASS表示该注解用于编译时解析
/** * 通过配置控件id,替换findViewById * * @author zhujie * @date 2019-09-13 * @time 08:32 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface BindView { int value(); }
-
创建一个常量类:Constant.java
/** * 相关常量定义 * * @author zhujie * @date 2019-09-14 * @time 06:23 */ public class Constant { /** * 生成类所在包名 */ public static final String GENERATE_PACKAGE = "com.agilezhu.annotationkit.generate"; /** * 生成类名固定前缀 */ public static final String GENERATE_CLASS_NAME_HEAD = "BindProxy$"; /** * 生成类完整类名开头:包名+类名固定前缀 */ public static final String GENERATE_CLASS_FULL_NAME_HEAD = GENERATE_PACKAGE + "." + GENERATE_CLASS_NAME_HEAD; }
2、创建名叫"compiler"的Java Library类型Module:主要用于存放解析注解的处理器
-
build.gradle:
apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':annotation') annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'//auto-service本身也是个注解处理器 implementation 'com.google.auto.service:auto-service:1.0-rc4'//注解 processor 类,并对其生成 META-INF 的配置信息 implementation 'com.squareup:javapoet:1.8.0' //通过类调用的形式来生成java代码,避免手动拼接字符串 } sourceCompatibility = "7" targetCompatibility = "7"
- auto-service:用于自动注册配置注解处理器processor 类,使用@AutoService(Processor.class)来标记注解处理器类;需要注意的是,不仅要implementation这个auto-service,还要annotationProcessor,因为auto-service里既包含AutoService注解,也包含处理该注解的解释器,少了其中一个都无法正常生成代码
- javapoet:这个主要用来通过调用方法来创建java类,相对于拼接字符串来说更加方便不容易出错
-
接着创建自定义的注解处理器BindProcessor.java:这个类主要用于解析用户标注的注解,然后生成自定义java处理类
/** * 处理绑定类型的注解 * * @author zhujie * @date 2019-09-13 * @time 08:29 */ @AutoService(Processor.class) public class BindProcessor extends AbstractProcessor { private Filer mFiler; //文件相关工具类:用于保存生成的java类文件 private Elements mElementUtils; //元素相关工具类:用于获取java类文件 private Messager mMessager;//用于打印日志 @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mFiler = processingEnv.getFiler(); mElementUtils = processingEnv.getElementUtils(); mMessager = processingEnv.getMessager(); } @Override public Set<String> getSupportedAnnotationTypes() { //返回该注解处理器能够处理哪些注解 Set<String> types = new LinkedHashSet<>(); types.add(BindView.class.getName()); return types; } @Override public SourceVersion getSupportedSourceVersion() { //返回当前注解处理器支持的java版本号 return SourceVersion.latest(); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //获取所有的源码文件 Set<? extends Element> elements = roundEnvironment.getRootElements(); for (Element element : elements) { if (!(element instanceof TypeElement)) {//判断是否class类 continue; } //转换成class类型 TypeElement typeElement = (TypeElement) element; //当前文件的类名 String classSimpleName = element.getSimpleName().toString(); //将要生成的java完整类名:BindProxy$+当前类名 String targetClassName = GENERATE_CLASS_NAME_HEAD + element.getSimpleName(); //创建方法(构造方法) MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("<init>") .addModifiers(Modifier.PUBLIC) .addParameter(ClassName.get(typeElement.asType()), classSimpleName.toLowerCase()); //获取当前类里所有元素 List<? extends Element> members = mElementUtils.getAllMembers(typeElement); //当前类里所有添加了BindView注释的元素 List<Element> annotationMembers = new ArrayList<>(); for (Element member : members) { BindView bindViewAnnotation = member.getAnnotation(BindView.class); if (bindViewAnnotation != null) { annotationMembers.add(member); String paramName = classSimpleName.toLowerCase(); //构造方法中添加初始化代码:findViewById bindMethodBuilder.addStatement( String.format( paramName + ".%s = (%s) " + paramName + ".findViewById(%s)" , member.getSimpleName() , ClassName.get(member.asType()).toString() , bindViewAnnotation.value())); } } //如果该类中没有我们自定义的注解,则不生成对应java处理类 if (annotationMembers.isEmpty()) { continue; } //创建类 TypeSpec bindProxyClass = TypeSpec.classBuilder(targetClassName) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(bindMethodBuilder.build()) .build(); //创建java文件 JavaFile bindProxyFile = JavaFile .builder(GENERATE_PACKAGE, bindProxyClass) .build(); try { //保存java类文件 bindProxyFile.writeTo(mFiler); } catch (Throwable e) { e.printStackTrace(); } } return false; } }
- 在init方法中可以通过ProcessingEnvironment获取相关工具类,供后面处理注解时使用
- getSupportedAnnotationTypes:是用来返回我们当前要处理哪些注解,必须重写该方法
- getSupportedSourceVersion:返回当前注解处理器支持的java版本,不重写该方法会出现警告
- process:用于解析注解,并生成对应的java文件
3、创建名叫"library"的Android Library类型Module:主要用于存放运行时调用生成的那些类的方法
-
build.gradle:
apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 28 buildToolsVersion "29.0.2" defaultConfig { minSdkVersion 15 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' //引入自定义的注解,这里使用api是为了引入当前library的项目可以也可以直接使用到自定义的注解 api project(':annotation') }
- api project(‘:annotation’):引入自定义的注解,这里使用api是为了引入当前library的项目可以也可以直接使用到自定义的注解
-
AnnotationKit.kt:通过反射获取生成的类,并调用构造方法,从而绑定当前Activity中的View
/** * 用于绑定组件 * * @author zhujie * @date 2019-09-13 * @time 18:32 */ class AnnotationKit { companion object { /** * 缓存构造方法 */ private val mCacheConstructor = HashMap<Class<*>, Constructor<*>?>() fun bind(activity: Activity) { //从缓存中读取构造方法 var constructor = mCacheConstructor[activity.javaClass] if (constructor == null) { synchronized(mCacheConstructor) { if (constructor == null) { try { val fullClassName = GENERATE_CLASS_FULL_NAME_HEAD + activity::class.java.simpleName //通过反射获取生成的类 val clazz = Class.forName(fullClassName) //获取构造方法 constructor = clazz.getConstructor(activity.javaClass) //存入缓存 mCacheConstructor[activity.javaClass] = constructor } catch (e: Throwable) { e.printStackTrace() } } } } //反射调用构造方法 constructor?.newInstance(activity) } } }
4、创建名叫"app"的Android Application类型Module:需要使用自定义的app项目
- build.gradle:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
//使用kapt处理注解处理器
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.agilezhu.annotationkit"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//引入自定义注解library
implementation project(':library')
//使用kapt编译注解处理器
kapt project(':compiler')
}
- MainActivity.kt:使用注解@BindView(R.id.main_title_view),并调用AnnotationKit.bind(this)完成绑定操作
class MainActivity : AppCompatActivity() {
@BindView(R.id.main_title_view)
lateinit var mTitleView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//调用生成的代码,初始化view
AnnotationKit.bind(this)
if (mTitleView != null) {
Toast.makeText(this, "BindView成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "BindView失败", Toast.LENGTH_SHORT).show()
}
}
}
- 需要注意的是,我这个测试项目中使用了kapt,而不是apt,是因为项目中使用了kotlin,所以需要kotlin的apt,也就是kotlin-kapt
- 编译后生成的代码位于:app/build/generated/source/kapt/debug/目录下的package:com.agilezhu.annotationkit.generate
- 生成的代码:BindProxy$MainActivity.java
package com.agilezhu.annotationkit.generate; import com.agilezhu.annotationkit.MainActivity; public final class BindProxy$MainActivity { public BindProxy$MainActivity(MainActivity mainactivity) { mainactivity.mTitleView = (android.widget.TextView) mainactivity.findViewById(2131165289); } }
其他问题
- 如果出现以下警告:
则在根目录下的gradle.properties文件中添加下面内容就就可以:[kapt] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: com.agilezhu.compiler.BindProcessor (NON_INCREMENTAL), com.google.auto.service.processor.AutoServiceProcessor (NON_INCREMENTAL).
#apt增量编译,加速编译效果 kapt.incremental.apt=true
- 如果出现编译失败:
在gradle.properties文件中添加下面这段内容:Execution failed for task ':lib:common:kaptDebugKotlin'. > A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask$KaptExecutionWorkAction > java.lang.reflect.InvocationTargetException (no error message)
kapt.incremental.apt = false kapt.include.compile.classpath=false kapt.use.worker.api=false
- 如果出现找不到
AbstractProcessor
等类的情况,请删除该Module,重新创建Java or Kotlin Library
类型项目,即使其他类型Module把build.gradle
配置改成一模一样也不行!