我所知道的偷懒的方式
前言
在现今的软件开发过程中,软件开发人员将更多的精力投入在了重复的相似劳动中。特别是在如今特别流行的 MVC 架构模式中,软件各个层次的功能更加独立,同时代码的相似度也更加高。所以我们需要寻找一种来减少软件开发人员重复劳动的方法,让程序员将更多的精力放在业务逻辑以及其他更加具有创造力的工作上。
服务端的技术发展了很多年,有很多值得可以借鉴的地方。有的时候,在跟后台沟通的时候,发现他们在设计好数据库
和表结构
的时候,经常可以一键生成常见的功能(增、删、改、查)。后来知道,因为相识程度非常的高。所以他们经常以代码来生成代码。就这样完成了一站式
的功能。
更多时候,我在考虑,为何他们不优先考虑封装呢?
- 可能会经常发生变动。
- 人员因素吧
Android 的一种"偷懒"
Android
中有种模板编程的偷懒方式。但不是今天的主角。今天的主角是Annotation Processor
(注解处理器)。 在最近查阅源码的时候。发现很多框架都会使用到Annotation Processor
比如说,ButterKnife
、Dagger
、ARouter
等等。所以理解Annotation Processor
(注解处理器)的原理,是一个Android
程序员必须具备的技能。
1. Annotation Processor
Annotation Processor
直译成中文就是(注解处理器),就是能够对注解进行处理。既然能够对注解进行处理,那么先定义一个简单的注解。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
}
2. 使用注解
定义完注解后,我们会想着在什么时候使用它呢? 还记得上述的核心是为了偷懒,那么
在Android
存在有重复性非常高而且难度极地的代码,就是对控件的获取和处理。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView1=(TextView) findViewById(R.id.tvHello);
textView1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
}
这样的模板代码不仅很浪费时间,而且代码的美观程度也大大的降低了。那么我们现在尝试着用自己定义的注解去处理。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tvHello)
TextView tvHello;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@OnClick(R.id.tvHello)
public void changeText2() {
Toast.makeText(this, "2222", Toast.LENGTH_LONG).show();
}
}
上述的代码,就会比较清晰。不会有获取控件的过程的代码。声明即使用了。那么仅仅这样打上一个注解,能够自动的获取控件吗?答案是不可能的。我们还缺少一步编写注解处理器(其实就是编写遇到这个注解改怎么处理)。
3. 编写注解处理器
xx01 最终生成的类
在编写注解处理器之前,先尝试着去编写一个类。这个类的职责就是获取控件。
public final class MainActivity$ViewBinding {
public MainActivity$ViewBinding(MainActivity target, View source) {
if(target == null) return;
if(source == null) return;
target.tvHello = (android.widget.TextView)source.findViewById(2131165275);
}
public MainActivity$ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
}
上面的这个类,就是需要注解处理器
帮我们自动生成的模板代码。我们必须知道自己需要什么代码,才能控制注解处理器
生成我们想要的代码。那么观察一下最终生成的代码,其中有几个需要注意的:
- 需要遍历所有需要处理的注解。在本文中我们只关心
BindView
- 当我们遍历出所有带有
BindView
的注解的时候,我们需要为这个字段赋值。而赋值的操作,我们是通过属性直接赋值(默认的访问权限是包访问权限)
target.tvHello = (android.widget.TextView)source.findViewById(2131165275);
所以这个由注解处理器
帮我生成的类,需要跟目标类在同级包下。
- 这个类的名字是按照一定格式生成的,在本文中它是由目标类+$ViewBinding。
到这里,需要注意的点都讲完了,接下开始来编写注解处理器
的核心代码。
xx02 项目目录的划分
在知道了我们最终需要生成什么代码之后,就需要对整个目录进行规划一下,因为这个工具不仅仅是当前这个项目会被使用。很有很多项目将对其引用。你可能会将其发布到JCenter
上。
根据上面的图,我们分别建立3
个module
。其中2
个是java libary
和android libary
最终的目录结构是这样的
image.png然后我们将刚才编写的BindView
注解放入inject-annotation
模块中
xx03 编写注解处理器
在划分话目录结构后,我们可以编写注解处理器的核心代码了,也就意味着,我们需要把它的代码放置在inject-compiler
中。
需要在inject-compiler
下的build.gradle
导入几个库
implementation 'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
api 'com.squareup:javapoet:1.10.0'
implementation project(':inject-annotation')
介绍一下这几个库,
-
auto-service
是帮助生成META-INF/services/javax.annotation.processing
文件中的内容 -
javapoet
更加面向对象的输出代码 -
inject-annotation
需要处理的注解。
在添加完这些东西之后,我们就需要创建一个注解处理器了
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
HashMap<TypeElement, List<Element>> datas = new HashMap<>();
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
TypeElement originalType = (TypeElement) element.getEnclosingElement();
if (datas.get(originalType) == null) {
List<Element> elements = new ArrayList<>();
datas.put(originalType, elements);
}
List<Element> dd = datas.get(originalType);
dd.add(element);
}
for (Map.Entry<TypeElement, List<Element>> entry : datas.entrySet()) {
TypeElement key = entry.getKey();
List<Element> value = entry.getValue();
createFile(key, value);
}
return false;
}
核心代码大概是上面的,就是遍历类中的注解元素,包装成一个Map
数据的数据结构。
然后将构建后的数据结构,进行处理,按照之间MainActivity$ViewBinding
的所预想的规则进行输出。
private MethodSpec createCustomerConstructor2View(TypeElement originalType, List<Element> elements) {
ParameterSpec targetParamSpec = ParameterSpec.builder(TypeName.get(originalType.asType()), "target").build();
MethodSpec.Builder constructor1 = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(targetParamSpec)
.addParameter(mViewParameterSpec)
.addStatement("if(target == null) return")
.addStatement("if(source == null) return");
for (Element element : elements) {
String variateName = element.getSimpleName().toString();
String variateType = element.asType().toString();
int resId = element.getAnnotation(BindView.class).value();
constructor1.addStatement("target.$L = ($N)source.findViewById($L)", variateName, variateType, resId);
}
return constructor1.build();
}
这样我们就完成了注解处理器的编写,当你编写后重新Rebuild Project
后,如果不出意外的话,你可以找到下面这个文件。
xx03 调试Annotation Processor
(注解处理器)
如果你在编写注解处理器
可能不是你预想,那么断点调试就变得非常的重要了。那么接下来介绍如何调试注解处理器
.
- 打开
Edit Configurations
新建一个Remote Configurations
- 在
Terminal
中运行下面命令
image.png./gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac
-
需要断点的地方,下断点。
-
Debug
刚才添加的Configurations
5.它就可以正常的断点了
image.pngxx04 调用生成的代码
当我们完成了注解处理器
的编写,也正常的生成了MainActivity$ViewBinding
文件后,我们就需要调用所生成的类,让它来帮我们完成控件的注入。因为其中会涉及到对Activity
的引用,所以这个包是android libary
。也就意味着,接下来的代码需要在inject-core
模块中编写。
我们在进入一个Activity
或者Fragment
的时候,能够调用inject
方法,而inject
的实现如下:
public static void inject(Activity activity) {
String name = activity.getClass().getSimpleName();
String packName = activity.getPackageName();
String fullName = name + "$ViewBinding";
try {
Class targetClass = activity.getClassLoader().loadClass(packName + "." + fullName);
Constructor constructor = targetClass.getConstructor(activity.getClass());
constructor.newInstance(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
就是通过反射,然后实例化刚才的对象。这样的话,我们就完成了控件的注入了。
4. 发布到JCenter
上
因为我们项目中存在有jar
和aar
格式的包,所以需要分别上传。在编写上传任务之前,需要先在最外层的build.gradle
添加plugin
,请注意插件的版本。不然会出现一些奇奇怪怪的问题。
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
- 编写
jar
格式的gradle
,在最外层新建jar_publish.gradle
文件,然后在:inject-compiler
的build.gradle
中 apply。
- 编写
aar
格式的gradle
,在最外层新建aar_publish.gradle
文件,然后在:inject-core
的build.gradle
中 apply。
然后依次上传即可。