Android 架构师之路 APT (1)

今天我们先给出 demo 的代码,随后会就 APT 技术进行详细介绍
APT
目标通过自己视图绑定功能来了解如何 APT 帮助我们在编译期间自动生成代码。在网上收集一些资料自己实现一下。
- 目标使用 APT 实现类似 Butter Knife 库提供的通过注解实现视图绑定的功能
最近使用 binding 实现视图绑定功能。有点渐渐远离了 ButterKnife ,不过这个库还是给我留下美好的回忆,简洁明了的 API 让人印象深刻。
创建一个空 Android 项目,接下我们添加模块到项目,每一个模块在 APT 中实现不同功能。
创建模块
-
创建 zi-annotation 注意这是一个 (java 模块),在这个模块中,可以创建注解 ZiViewBind
apt_create_java_module.JPG
-
创建 zi-api (android 模块)提供用户可以调用 api ,这里实现 view 的绑定。
apt_create_android_module.JPG
- 创建 zi-compiler(java 模块) 编写注解处理 。
No. | 模块名 | 模块类型 | 说明 |
---|---|---|---|
1 | zi-annotation | java 模块 | 在这个模块中,可以创建注解 |
2 | zi-api | Android 模块 | 提供用户可以调用 api |
3 | zi-compiler | java 模块 | 编写注解处理 |
Android Studio 在当我们创建好模块自动在 setting 文件中写入了这些模块
rootProject.name='zi application'
include ':app'
include ':zi-annotation'
include ':zi-api'
include ':zi-compiler'
处理依赖关系
- build.gradle(zi-compiler)
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.10.0'
implementation project(':zi-annotation')
}
sourceCompatibility = "7"
targetCompatibility = "7"
- 注意
zi-compiler
依赖于zi-annotation
模块
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.10.0'
在主项目中添加刚刚创建好的几个模块,值得注意 zi-compiler 为 annotationProcessor
dependencies {
...
implementation project(':zi-annotation')
implementation project(':zi-api')
annotationProcessor project(':zi-compiler')
}
到此我们已经完成模块创建、添加以及他们之间依赖关系,剩下的工作就是为这些模块添加代码。
zi-annotation 模块
创建 ZiBindView 注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface ZiBindView {
}
@Target
说明了Annotation所修饰的对象范围
值 | 说明 |
---|---|
CONSTRUCTOR | 用于描述构造器 |
FIELD | 用于描述域 |
LOCAL_VARIABLE | 用于描述局部变量 |
METHOD | 用于描述方法 |
PACKAGE | 用于描述包 |
PARAMETER | 用于描述参数 |
TYPE | 用于描述类、接口(包括注解类型) 或enum声明 |
zi-compiler 模块
创建 ZiBindViewProcessor 和 ClassCreatorProxy
在代码中不涉及的注解这里就不解释了
@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
注解处理器需要继承于 AbstractProcessor ,其中部分代码写法基本是固定的。
public class ZiBindViewProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
getSupportedAnnotationTypes()
返回支持的注解类型
getSupportedSourceVersion()
返回支持的源码版本
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
//获取类名
supportTypes.add(ZiBindView.class.getCanonicalName());
return supportTypes;
}
这里,getName()返回的是虚拟机里面的class的表示,而getCanonicalName()返回的是更容易理解的表示。其实对于大部分class来说这两个方法没有什么不同的。但是对于array或内部类来说是有区别的。
另外,类加载(虚拟机加载)的时候需要类的名字是getName。
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
收集信息
谓信息收集,就是根据我们声明,得到对应 Element,然后注解进行收集所需的信息,这些信息用于后期生产对象
生产代理类(在编译时,将文本生产为类的生产代理类 ClassCreatorProxy
针对每一个都会生产一个注解类
初始化
public interface ProcessingEnvironment {
Elements getElementUtils();
Types getTypeUtils();
Filer getFiler();
Locale getLocale();
Messager getMessager();
Map<String, String> getOptions();
SourceVersion getSourceVersion();
}
ZiBindViewProcessor 完整代码
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({"zidea.example.com.zi_compiler.ZiBindViewProcessor"})
@AutoService(Processor.class)
public class ZiBindViewProcessor extends AbstractProcessor {
//跟日志相关的辅助类
private Messager mMessager;
//跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
private Elements mElementUtils;
private Map<String,ClassCreatorProxy> mProxyMap = new HashMap<>();
//利用 ProcessingEnvironment 对象获取 Elements
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mMessager = processingEnvironment.getMessager();
mElementUtils = processingEnvironment.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
//获取类名
supportTypes.add(ZiBindView.class.getCanonicalName());
return supportTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
// 收集信息
// 所谓信息收集,就是根据我们声明,得到对应 Element,然后注解进行收集所需的信息,这些信息用于后期生产对象
// 生产代理类(在编译时,将文本生产为类的生产代理类 ClassCreatorProxy,针对每一个都会生产一个注解类
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//输出信息
mMessager.printMessage(Diagnostic.Kind.NOTE,"processing...");
//根据注解获取所需信息
//用来获取,注解所修饰的Element对象,getElementsAnnotatedWith 用于获取注解 ZiBindView 的 Element 对象
Set<? extends Element> elements =roundEnvironment.getElementsAnnotatedWith(ZiBindView.class);
//遍历说有在类中,添加了 ZiBindView 类
for (Element element : elements){
VariableElement variableElement = (VariableElement) element;
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
if(proxy == null){
proxy = new ClassCreatorProxy(mElementUtils,classElement);
mProxyMap.put(fullClassName,proxy);
}
ZiBindView bindAnnotation = variableElement.getAnnotation(ZiBindView.class);
int id = bindAnnotation.value();
proxy.putElement(id,variableElement);
}
for(String key:mProxyMap.keySet()){
ClassCreatorProxy proxyInfo = mProxyMap.get(key);
JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(),proxyInfo.generateJavaCode2()).build();
try {
javaFile.writeTo(processingEnv.getFiler());
}catch (IOException e){
e.printStackTrace();
}
}
mMessager.printMessage(Diagnostic.Kind.NOTE,"process finish ...");
return true;
}
}
for(String key:mProxyMap.keySet()){
ClassCreatorProxy proxyInfo = mProxyMap.get(key);
JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(),proxyInfo.generateJavaCode2()).build();
try {
javaFile.writeTo(processingEnv.getFiler());
}catch (IOException e){
e.printStackTrace();
}
}
- 返回用来创建新的类的 filer
ClassCreatorProxy 完整代码
public class ClassCreatorProxy {
//绑定的类名称
private String mBindingClassName;
//绑定的包名称
private String mPackageName;
private TypeElement mTypeElement;
private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();
public ClassCreatorProxy(Elements elementUtils, TypeElement classElement){
this.mTypeElement = classElement;
PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
String packageName = packageElement.getQualifiedName().toString();
String className = mTypeElement.getSimpleName().toString();
this.mPackageName = packageName;
this.mBindingClassName = className + "_ViewBinding";
}
public void putElement(int id, VariableElement element){
mVariableElementMap.put(id,element);
}
public String generateJavaCode(){
StringBuilder builder = new StringBuilder();
builder.append("package ").append(mPackageName).append(";\n\n");
builder.append("import com.example.gavin.apt_library.*;\n");
builder.append('\n');
builder.append("public class ").append(mBindingClassName);
builder.append(" {\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
private void generateMethods(StringBuilder builder) {
builder.append("public void bind(" + mTypeElement.getQualifiedName() + " host ) {\n");
for (int id : mVariableElementMap.keySet()) {
VariableElement element = mVariableElementMap.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));\n");
}
builder.append(" }\n");
}
public String getProxyClassFullName() {
return mPackageName + "." + mBindingClassName;
}
public TypeElement getTypeElement() {
return mTypeElement;
}
//JavaPoet是提供于自动生成java文件的构建工具类框架,使用该框架可以方便根据我们所注解的内容在编译时进行代码构建。
public TypeSpec generateJavaCode2() {
TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
.addModifiers(Modifier.PUBLIC)
.addMethod(generateMethods2())
.build();
return bindingClass;
}
private MethodSpec generateMethods2() {
ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(host, "host");
for (int id : mVariableElementMap.keySet()) {
VariableElement element = mVariableElementMap.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
methodBuilder.addCode("host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
}
return methodBuilder.build();
}
public String getPackageName(){
return mPackageName;
}
}
zi-api 模块
public class ZiBindViewTools {
public static void bind(Activity activity) {
Class clazz = activity.getClass();
try {
Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
我们通过
### 使用 Api
我们已经完成 APT 部分代码,那么如何使用我们创建好 ZiBindView 呢?具体使用方法和 butterKnife 基本一样。
```java
public class MainActivity extends AppCompatActivity {
@ZiBindView(value = R.id.main_activity_textView)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ZiBindViewTools.bind(this);
textView.setText("Zidea");
}
}
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
但是当我们运行时候可能还无法生存代码,这是因为使用 gradle 版本过高,需要我们自己手动创建,
- 首先将目录从 Android 切换到 Project 视图
- 在
zi-compiler
目录下创建如图结构resources/META-INF/services
文件目录 - 然后在目录下添加一个问题
file
即可,在 file中添加如下内容
001.JPG
也就是我们刚刚创建 ZiBindViewProcessor文件包名加文件名
002.JPG