前段時間在學習Dagger2,對它生成代碼的原理充滿了好奇。google了之后發現原來java原生就是支持代碼生成的。
通過Annotation Processor可以在編譯的時候處理注解,生成我們自定義的代碼,這些生成的代碼會和其他手寫的代碼一樣被javac編譯。注意Annotation Processor只能用來生成代碼,而不能對原來的代碼進行修改。
實現的原理是通過繼承AbstractProcessor,實現我們自己的Processor,然后把它注冊給java編譯器,編譯器在編譯之前使用我們定義的Processor去處理注解。
AbstractProcessor
AbstractProcessor是一個抽象類,我們繼承它需要實現一個抽象方法process,在這個方法里面去處理注解。然后它還有幾個方法需要我們去重寫。
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {...}
@Override
public Set<String> getSupportedAnnotationTypes() {...}
@Override
public SourceVersion getSupportedSourceVersion() {...}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {...}
}
init方法是初始化的地方,我們可以通過ProcessingEnvironment獲取到很多有用的工具類
getSupportedAnnotationTypes 這個方法指定處理的注解,需要將要處理的注解的全名放到Set中返回
getSupportedSourceVersion 這個方法用來指定支持的java版本
process 是實際處理注解的地方
在Java 7后多了 SupportedAnnotationTypes 和 SupportedSourceVersion 這個兩個注解用來簡化指定注解和java版本的操作:
@SupportedAnnotationTypes({"linjw.demo.injector.InjectView"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class InjectorProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {...}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {...}
注冊Processor
編寫完我們的Processor之后需要將它注冊給java編譯器
在src/main目錄下創建resources/META-INF/services/javax.annotation.processing.Processor文件(即創建resources目錄,在resources目錄下創建META-INF目錄,繼續在META-INF目錄下創建services目錄,最后在services目錄下創建javax.annotation.processing.Processor文件)。
在javax.annotation.processing.Processor中寫入自定義的Processor的全名,如果有多個Processor的話,每一行寫一個。
完成后 javax.annotation.processing.Processor 內容如下
$ cat javax.annotation.processing.Processor
linjw.demo.injector.InjectorProcessor
在安卓中自定義Processor
我以前在學習Java自定義注解的時候寫過一個小例子,它是用運行時注解通過反射簡化findViewById操作的。但是這種使用運行時注解的方法在效率上是有缺陷的,因為反射的效率很低。
基本上學安卓的人都知道有個很火的開源庫ButterKnife,它也能簡化findViewById操作,但它是通過編譯時注解生成代碼去實現的,效率比我們使用反射實現要高很多很多。
其實我對ButterKnife的原理也一直很好奇,下面就讓我們也用生成代碼的方式高效的簡化findViewById操作。
創建配置工程
首先在android項目中是找不到AbstractProcessor的,需要新建一個Java Library Module。
Android Studio中按File -> New -> New Module... 然后選擇新建Java Library, Module的名字改為libinjector。
同時在安卓中使用AbstractProcessor需要apt的支持,所以需要配置一下gradle:
1.在 project 的 build.gradle 的 dependencies 下加上 android-apt 支持
...
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
...
2.在 app 的 build.gradle 的開頭加上 "apply plugin: 'com.neenbedankt.android-apt'"
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
...
創建注解
我們在libinjector中創建注解InjectView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface InjectView {
int value();
}
這個是個修飾Field且作用于源碼的自定義注解。關于自定義注解的知識可以看看我以前寫的一篇文章《Java自定義注解和動態代理》。我們用它來修飾View成員變量并保持View的resource id,生成的代碼通過resource id使用findViewById注入成員變量。
創建InjectorProcessor
在libinjector中創建InjectorProcessor實現代碼的生成
@SupportedAnnotationTypes({"linjw.demo.injector.InjectView"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class InjectorProcessor extends AbstractProcessor {
private static final String GEN_CLASS_SUFFIX = "Injector";
private static final String INJECTOR_NAME = "ViewInjector";
private Types mTypeUtils;
private Elements mElementUtils;
private Filer mFiler;
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mTypeUtils = processingEnv.getTypeUtils();
mElementUtils = processingEnv.getElementUtils();
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(InjectView.class);
//process會被調用三次,只有一次是可以處理InjectView注解的,原因不明
if (elements.size() == 0) {
return true;
}
Map<Element, List<Element>> elementMap = new HashMap<>();
StringBuffer buffer = new StringBuffer();
buffer.append("package linjw.demo.injector;\n")
.append("public class " + INJECTOR_NAME + " {\n");
//遍歷所有被InjectView注釋的元素
for (Element element : elements) {
//如果標注的對象不是FIELD則報錯,這個錯誤其實不會發生因為InjectView的Target已經聲明為ElementType.FIELD了
if (element.getKind()!= ElementKind.FIELD) {
mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a FIELD", element);
}
//這里可以先將element轉換為VariableElement,但我們這里不需要
//VariableElement variableElement = (VariableElement) element;
//如果不是View的子類則報錯
if (!isView(element.asType())){
mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a View", element);
}
//獲取所在類的信息
Element clazz = element.getEnclosingElement();
//按類存入map中
addElement(elementMap, clazz, element);
}
for (Map.Entry<Element, List<Element>> entry : elementMap.entrySet()) {
Element clazz = entry.getKey();
//獲取類名
String className = clazz.getSimpleName().toString();
//獲取所在的包名
String packageName = mElementUtils.getPackageOf(clazz).asType().toString();
//生成注入代碼
generateInjectorCode(packageName, className, entry.getValue());
//完整類名
String fullName = clazz.asType().toString();
buffer.append("\tpublic static void inject(" + fullName + " arg) {\n")
.append("\t\t" + fullName + GEN_CLASS_SUFFIX + ".inject(arg);\n")
.append("\t}\n");
}
buffer.append("}");
generateCode(INJECTOR_NAME, buffer.toString());
return true;
}
//遞歸判斷android.view.View是不是其父類
private boolean isView(TypeMirror type) {
List<? extends TypeMirror> supers = mTypeUtils.directSupertypes(type);
if (supers.size() == 0) {
return false;
}
for (TypeMirror superType : supers) {
if (superType.toString().equals("android.view.View") || isView(superType)) {
return true;
}
}
return false;
}
private void addElement(Map<Element, List<Element>> map, Element clazz, Element field) {
List<Element> list = map.get(clazz);
if (list == null) {
list = new ArrayList<>();
map.put(clazz, list);
}
list.add(field);
}
private void generateCode(String className, String code) {
try {
JavaFileObject file = mFiler.createSourceFile(className);
Writer writer = file.openWriter();
writer.write(code);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 生成注入代碼
*
* @param packageName 包名
* @param className 類名
* @param views 需要注入的成員變量
*/
private void generateInjectorCode(String packageName, String className, List<Element> views) {
StringBuilder builder = new StringBuilder();
builder.append("package " + packageName + ";\n\n")
.append("public class " + className + GEN_CLASS_SUFFIX + " {\n")
.append("\tpublic static void inject(" + className + " arg) {\n");
for (Element element : views) {
//獲取變量類型
String type = element.asType().toString();
//獲取變量名
String name = element.getSimpleName().toString();
//id
int resourceId = element.getAnnotation(InjectView.class).value();
builder.append("\t\targ." + name + "=(" + type + ")arg.findViewById(" + resourceId + ");\n");
}
builder.append("\t}\n")
.append("}");
//生成代碼
generateCode(className + GEN_CLASS_SUFFIX, builder.toString());
}
}
注冊InjectorProcessor
在libinjector的src/main目錄下創建resources/META-INF/services/javax.annotation.processing.Processor文件注冊InjectorProcessor:
# 注冊InjectorProcessor
linjw.demo.injector.InjectorProcessor
使用InjectView注解
我們在Activity中使用InjectView修飾需要賦值的View變量并且用ViewInjector.inject(this);調用生成的掉初始化修飾的成員變量。這里有兩個Activity都使用了InjectView去簡化findViewById操作:
public class MainActivity extends AppCompatActivity {
@InjectView(R.id.label)
TextView mLabel;
@InjectView(R.id.button)
Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//使用findViewById注入被InjectView修飾的成員變量
ViewInjector.inject(this);
// ViewInjector.inject(this) 已經將mLabel和mButton賦值了,可以直接使用
mLabel.setText("MainActivity");
mButton.setText("jump to SecondActivity");
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}
});
}
}
public class SecondActivity extends Activity {
@InjectView(R.id.label)
TextView mLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
//使用findViewById注入被InjectView修飾的成員變量
ViewInjector.inject(this);
// ViewInjector.inject(this) 已經將mLabel賦值了,可以直接使用
mLabel.setText("SecondActivity");
}
}
工具類
在 AbstractProcessor.init 方法中我們可以獲得幾個很有用的工具類:
mTypeUtils = processingEnv.getTypeUtils();
mElementUtils = processingEnv.getElementUtils();
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
它們的作用如下:
Types
Types提供了和類型相關的一些操作,如獲取父類、判斷兩個類是不是父子關系等,我們在isView中就用它去獲取父類
//遞歸判斷android.view.View是不是其父類
private boolean isView(TypeMirror type) {
List<? extends TypeMirror> supers = mTypeUtils.directSupertypes(type);
if (supers.size() == 0) {
return false;
}
for (TypeMirror superType : supers) {
if (superType.toString().equals("android.view.View") || isView(superType)) {
return true;
}
}
return false;
}
Elements
Elements提供了一些和元素相關的操作,如獲取所在包的包名等:
//獲取所在的包名
String packageName = mElementUtils.getPackageOf(clazz).asType().toString();
Filer
Filer用于文件操作,我們用它去創建生成的代碼文件
private void generateCode(String className, String code) {
try {
JavaFileObject file = mFiler.createSourceFile(className);
Writer writer = file.openWriter();
writer.write(code);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Messager
Messager 顧名思義就是用于打印的,它會打印出Element所在的源代碼,它還會拋出異常。靠默認的錯誤打印有時很難找出錯誤的地方,我們可以用它去添加更直觀的日志打印
當用InjectView標注了非View的成員變量我們就會打印錯誤并拋出異常(這里我們使用Diagnostic.Kind.ERROR,這個打印會拋出異常終止Processor):
//如果不是View的子類則報錯
if (!isView(element.asType())){
mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a View", element);
}
例如我們如果在MainActivity中為一個String變量標注InjectView:
//在非View上使用InjectView就會報錯
@InjectView(R.id.button)
String x;
則會報錯:
符號: 類 ViewInjector
位置: 程序包 linjw.demo.injector
/Users/linjw/workspace/ProcessorDemo/app/src/main/java/linjw/demo/processordemo/MainActivity.java:22: 錯誤: is not a View
String x;
^
如果我們不用Messager去打印,生成的代碼之后也會有打印,但是就不是那么清晰了:
/Users/linjw/workspace/ProcessorDemo/app/build/generated/source/apt/debug/MainActivityInjector.java:7: 錯誤: 不兼容的類型: View無法轉換為String
arg.x=(java.lang.String)arg.findViewById(2131427415);
Element的子接口
我們在process方法中使用getElementsAnnotatedWith獲取到的都是Element接口,其實我們用Element.getKind獲取到類型之后可以將他們強轉成對應的子接口,這些子接口提供了一些針對性的操作。
這些子接口有:
- TypeElement:表示一個類或接口程序元素。
- PackageElement:表示一個包程序元素。
- VariableElement:表示一個屬性、enum 常量、方法或構造方法參數、局部變量或異常參數。
- ExecutableElement:表示某個類或接口的方法、構造方法或初始化程序(靜態或實例),包括注釋類型元素。
對應關系如下
package linjw.demo; // PackageElement
public class Person { // TypeElement
private String mName; // VariableElement
public Person () {} // ExecutableElement
public void setName (String name) {mName=name;} // ExecutableElement
}
Element的一些常用操作
獲取類名:
- Element.getSimpleName().toString(); // 獲取類名
- Element.asType().toString(); //獲取類的全名
獲取所在的包名:
- Elements.getPackageOf(Element).asType().toString();
獲取所在的類:
- Element.getEnclosingElement();
獲取父類:
- Types.directSupertypes(Element.asType())
獲取標注對象的類型:
- Element.getKind()
Demo地址
可以在這里查看完整代碼