
今天我們先給出 demo 的代碼,隨后會就 APT 技術進行詳細介紹
APT
目標通過自己視圖綁定功能來了解如何 APT 幫助我們在編譯期間自動生成代碼。在網上收集一些資料自己實現一下。
- 目標使用 APT 實現類似 Butter Knife 庫提供的通過注解實現視圖綁定的功能
最近使用 binding 實現視圖綁定功能。有點漸漸遠離了 ButterKnife ,不過這個庫還是給我留下美好的回憶,簡潔明了的 API 讓人印象深刻。
創(chuàng)建一個空 Android 項目,接下我們添加模塊到項目,每一個模塊在 APT 中實現不同功能。
創(chuàng)建模塊
-
創(chuàng)建 zi-annotation 注意這是一個 (java 模塊),在這個模塊中,可以創(chuàng)建注解 ZiViewBind
apt_create_java_module.JPG -
創(chuàng)建 zi-api (android 模塊)提供用戶可以調用 api ,這里實現 view 的綁定。
apt_create_android_module.JPG - 創(chuàng)建 zi-compiler(java 模塊) 編寫注解處理 。
| No. | 模塊名 | 模塊類型 | 說明 |
|---|---|---|---|
| 1 | zi-annotation | java 模塊 | 在這個模塊中,可以創(chuàng)建注解 |
| 2 | zi-api | Android 模塊 | 提供用戶可以調用 api |
| 3 | zi-compiler | java 模塊 | 編寫注解處理 |
Android Studio 在當我們創(chuàng)建好模塊自動在 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'
在主項目中添加剛剛創(chuàng)建好的幾個模塊,值得注意 zi-compiler 為 annotationProcessor
dependencies {
...
implementation project(':zi-annotation')
implementation project(':zi-api')
annotationProcessor project(':zi-compiler')
}
到此我們已經完成模塊創(chuàng)建、添加以及他們之間依賴關系,剩下的工作就是為這些模塊添加代碼。
zi-annotation 模塊
創(chuàng)建 ZiBindView 注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface ZiBindView {
}
@Target說明了Annotation所修飾的對象范圍
| 值 | 說明 |
|---|---|
| CONSTRUCTOR | 用于描述構造器 |
| FIELD | 用于描述域 |
| LOCAL_VARIABLE | 用于描述局部變量 |
| METHOD | 用于描述方法 |
| PACKAGE | 用于描述包 |
| PARAMETER | 用于描述參數 |
| TYPE | 用于描述類、接口(包括注解類型) 或enum聲明 |
zi-compiler 模塊
創(chuàng)建 ZiBindViewProcessor 和 ClassCreatorProxy
在代碼中不涉及的注解這里就不解釋了
@Retention定義了該Annotation被保留的時間長短:某些Annotation僅出現在源代碼中,而被編譯器丟棄;而另一些卻被編譯在class文件中;編譯在class文件中的Annotation可能會被虛擬機忽略,而另一些在class被裝載時將被讀?。ㄕ堊⒁獠⒉挥绊慶lass的執(zhí)行,因為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或內部類來說是有區(qū)別的。
另外,類加載(虛擬機加載)的時候需要類的名字是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();
}
}
- 返回用來創(chuàng)建新的類的 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 部分代碼,那么如何使用我們創(chuàng)建好 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 版本過高,需要我們自己手動創(chuàng)建,
- 首先將目錄從 Android 切換到 Project 視圖
- 在
zi-compiler目錄下創(chuàng)建如圖結構resources/META-INF/services文件目錄 - 然后在目錄下添加一個問題
file即可,在 file中添加如下內容
001.JPG
也就是我們剛剛創(chuàng)建 ZiBindViewProcessor文件包名加文件名
002.JPG