1. はじめに
APT(アノテーション処理ツール)は、アノテーション処理ツールです。これはコンパイル時の注釈を処理し、コンパイル中に .java ファイルを生成するために使用されます。これにより、繰り返しのコード記述を軽減できます。ButterKnife、Dagger、EventBus、AndroidAnnotation などの一般的なフレームワークはすべて apt を使用します。以下では、ButterKnife と同様の、bindView 関数と bindingOnClick 関数を実装する簡単な例を使用します。
2. 導入プロセス
まずプロジェクトの全体構造を見てください。
プロジェクトには、app、myannotation、processor の3 つのモジュールが含まれています。このうち、myannotation はカスタム アノテーションであり、processor はアノテーション プロセッサです。なぜ 2 つのモジュールを作成するのでしょうか? apt は AbstractProcessor クラスを継承する必要があるため、このクラスは新しいモジュール -> Java ライブラリで取得する必要があり、コンパイル時アノテーション プロセッサはコンパイル時にのみ動作し、apk パッケージ化には参加しないため、プロセッサは独立したモジュールになります。 。また、カスタム アノテーションはアプリとプロセッサに同時に提供する必要があるため、myannotation も独立したモジュールです。
1. カスタム注釈
新しい Java ライブラリ myannotation を作成し、2 つのカスタム アノテーション クラスを作成します。
/***
* 自定义BindView注解
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
/**
* 自定义onclick注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
int value();
}
上記は、BindView と OnClick の 2 つのアノテーションを定義しています。BindView のオブジェクトはメンバー変数 (FIELD)、OnClick のオブジェクトはメソッド (METHOD) であり、両方のライフサイクルはコンパイル時 (CLASS) です。
注: これら 2 つのクラスには中国語の文字は含まれないはずですが、便宜上、ここに中国語の注記を追加しました。コンパイル時にGBK マップ不可能な文字のエンコードでエラーが発生するか、次のコードを build.gradle ファイルに追加して問題を解決します。
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
アノテーションの詳細な分析については、ここを参照してください。
2. アノテーションプロセッサを実装する
新しい Java ライブラリ: プロセッサを作成します。myannotation に頼ってください。
2.1 AbstractProcessor を継承する MyProcessor クラスを追加します。
public class MyProcessor extends AbstractProcessor {
private Messager messager;
private Elements elementUtils;
private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//获取message、elements
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//定义支持的注解类型为 自定义的BindView、OnClick
HashSet<String> supportedType = new LinkedHashSet<>();
supportedType.add(BindView.class.getCanonicalName());
supportedType.add(OnClick.class.getCanonicalName());
return supportedType;
}
@Override
public SourceVersion getSupportedSourceVersion() {
//指定使用的Java版本,通常这里返回最新支持版本
return SourceVersion.latestSupported();
}
}
通常、次の 4 つのメソッドを実装する必要があります。
- init() : 初期化、ElementUtils と Messager を取得します。ElementUtils はコード作成時にパッケージ名やその他の情報を取得するために使用され、Messager はコンパイル時に関連情報を出力するために使用されます。
- getSupportedAnnotationTypes() : サポートされている注釈のリストを返します。ここには、定義した注釈 BindView と OnClick が追加されています。
- getSupportedSourceVersion() : 使用する Java バージョンを指定します。通常は、サポートされている最新のバージョンがここで返されます。
- process() : アノテーションを処理し、関連する Java コードを生成するために使用されるキー メソッド。
process() メソッドの完全なコードを見てみましょう。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//输出打印信息
messager.printMessage(Diagnostic.Kind.NOTE, "processing...");
mProxyMap.clear();
//获取所有被BindView注解修饰的元素
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
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(elementUtils, classElement);
mProxyMap.put(fullClassName, proxy);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int id = bindAnnotation.value();
proxy.putElement(id, variableElement);
}
//获取所有被OnClick注解修饰的元素
Set<? extends Element> onClickElement = roundEnv.getElementsAnnotatedWith(OnClick.class);
for (Element element :
onClickElement) {
TypeElement classElement = (TypeElement) element.getEnclosingElement();
ExecutableElement executableElement = (ExecutableElement) element;
String fullClassName = classElement.getQualifiedName().toString();
ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
if (proxy == null) {
proxy = new ClassCreatorProxy(elementUtils, classElement);
mProxyMap.put(fullClassName, proxy);
}
OnClick bindAnnotation = element.getAnnotation(OnClick.class);
int id = bindAnnotation.value();
proxy.putOnclickElement(id, executableElement);
}
// //通过遍历mProxyMap,创建java文件
for (String key : mProxyMap.keySet()) {
ClassCreatorProxy proxyInfo = mProxyMap.get(key);
try {
messager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName());
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName() + "error");
}
}
messager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
return true;
}
Map<String, ClassCreatorProxy> mProxyMap はカスタム アノテーションを含むクラスのコレクションです。String パラメーターはクラスの完全なクラス名によって識別され、ClassCreatorProxy はコードの生成に使用されるプロキシ クラスです。まずアノテーションによって変更された要素を取得し、次に要素から完全なクラス名を取得します。クラスが既に mProxyMap に存在する場合は、ClassCreatorProxy を取り出します。それ以外の場合は、完全なクラス名と新しく作成した ClassCreatorProxy を mProxyMap に保存します。最後に、アノテーション情報を ClassCreatorProxy クラスに保存します。
这里有两段逻辑一样的代码分别处理了BindView和OnClick注解。
mProxyMap を走査した直後に、createSourceFile メソッドによって Java ファイルが作成され、ClassCreatorProxy のgenerateJavaCode メソッドによってそのファイルにコードが書き込まれます。
上記は MyProcessor の基本的な動作です。次に、コードを生成するクラス ClassCreatorProxy を見てください。
2.2コード生成: ClassCreatorProxy
文字列のスプライシングを実現する方法は次のとおりです。ロジックは比較的単純で、コードを直接貼り付けます。
public class ClassCreatorProxy {
private Elements elements;
private TypeElement typeElement;
private String mPakageName;
private String mBindingClassName;
private Map<Integer, VariableElement> mVariableElementMap = new HashMap();
private Map<Integer, ExecutableElement> onClickMap = new HashMap();
public ClassCreatorProxy(Elements elementUtils, TypeElement classElement) {
this.elements = elementUtils;
this.typeElement = classElement;
PackageElement packageElement = elements.getPackageOf(typeElement);
mPakageName = packageElement.getQualifiedName().toString();
String className = classElement.getSimpleName().toString();
mBindingClassName = className + "_ViewBinding";
}
public void putElement(int id, VariableElement element) {
mVariableElementMap.put(id, element);
}
public void putOnclickElement(int id, ExecutableElement element) {
onClickMap.put(id, element);
}
//创建java代码
public String generateJavaCode() {
//StringBuilder 速度大于StringBuffer ,线程不安全
StringBuilder sb = new StringBuilder();
sb.append("package ").append(mPakageName).append(";\n\n");
//sb.append("import com.jia.reflectionandannotation.*;\n");
sb.append("\n");
sb.append("public class ").append(mBindingClassName).append(" {\n");
generateBindViewMethod(sb);
generateBindOnclickMethod(sb);
// generateOnclickMethod2(sb);
sb.append(" \n}");
return sb.toString();
}
//添加bindView方法
public void generateBindViewMethod(StringBuilder sb) {
sb.append("\tpublic void bindView(" + typeElement.getQualifiedName() + " activity){\n");
for (int id :
mVariableElementMap.keySet()) {
VariableElement variableElement = mVariableElementMap.get(id);
//获取view的类型:TextView、Button。。。
String viewType = variableElement.asType().toString();
//获取view的名字
String viewName = variableElement.getSimpleName().toString();
//生成 activity.textView=(TextView)activity.findViewById(id);
sb.append("\t\tactivity." + viewName + " = (" + viewType + ")activity.findViewById(" + id + ");\n");
}
sb.append("\t}");
}
private void generateBindOnclickMethod(StringBuilder sb) {
sb.append("\n\tpublic void bindOnClick(final " + typeElement.getQualifiedName() + " activity){\n");
for (int id :
onClickMap.keySet()) {
ExecutableElement executableElement = onClickMap.get(id);
String methodName = executableElement.getSimpleName().toString();//方法的名字
sb.append("\t\tactivity.findViewById(" + id + ").setOnClickListener(new android.view.View.OnClickListener() {\n" +
"\t\t\t@Override\n" +
"\t\t\tpublic void onClick(android.view.View view) {\n" +
"\t\t\t\tactivity." + methodName + "();\n" +
"\t\t\t}\n" +
"\t\t});\n");
}
sb.append("\t}");
}
public String getProxyClassFullName() {
return mPakageName + "." + mBindingClassName;
}
public TypeElement getTypeElement() {
return typeElement;
}
}
mVariableElementMap と onClickMap を使用して、BindView 注釈と OnClick 注釈をそれぞれ保存します。2 つの要素タイプは VariableElement と ExecutableElement であり、それぞれ FIELD と METHOD を変更するためのアノテーションに対応していることに注意してください。特定の要素分析については、 こちら を参照してください。
putElement() は BindView 注釈の保存に使用され、
putOnclickElement() は OnClick 注釈の保存に使用され、
generateBindViewMethod() メソッドを使用して BindView 注釈を走査し、注釈値 (id) と注釈付き変数名、およびスプライシングを取得します。 findviewbyid コードを生成します。OnClick アノテーションは同じであり、bindView() メソッドと bindingOnClick() メソッドは最終的に xxx_ViewBinding クラスで生成されます。
*或者用javapoet来生成代码,这里就不作介绍了。
2.3 自動サービスの依存関係を追加する
implementation 'com.google.auto.service:auto-service:1.0-rc2'
//gradle3.4.1以上必须多加这一句,否则不会执行apt注解器
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc2'
最後に、MyProcessor の前に注釈を追加して、このクラスが注釈の処理に使用されることを示します。
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
....
注意!别忘了加annotationProcessor这一句!gradle3.1.2以下不需要加,高版本必须加,否则就算加了AutoService
也不会处理注解!我这里用的是3.4.1,具体看自己版本。
3. コードを生成する
app build.gradle の下に依存関係を追加します。
annotationProcessor project(path: ':processor')
implementation project(path: ':myannotation')
注意:processor module使用的是annotationProcessor,否则编译时会报错。
MainActivity でのアノテーションの使用はバターナイフに似ています (笑):
public class MainActivity extends AppCompatActivity {
@BindView(R.id.text1)
TextView text1;
@OnClick(R.id.button)
void onButtonClick() {
Toast.makeText(this, "onButtonClick", Toast.LENGTH_SHORT).show();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindViewUtils.bindView(this);
text1.setText("this is apt sample");
}
}
注釈を追加した後、rebuildProject は、対応する MainActivity_ViewBinding クラスが generatedJava->com.example.xxx の下に生成されていることを確認できます。
4 番目に、生成されたコードを呼び出します
上記に BindViewUtils.bindView(this) メソッドがあることがわかりますが、これは実際には単純なリフレクション呼び出しです。
public class BindViewUtils {
public static void bindView(Activity activity) {
Class clazz = activity.getClass();
try {
Class<?> bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method bindView = bindViewClass.getMethod("bindView", activity.getClass());
bindView.invoke(bindViewClass.newInstance(),activity);
Method bindOnClick = bindViewClass.getMethod("bindOnClick", activity.getClass());
bindOnClick.invoke(bindViewClass.newInstance(),activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
リフレクションは MainActivity_ViewBinding クラスを取得し、内部で bindingView() メソッドと bindingOnClick() メソッドを呼び出します。
5. 結果
オリジナルの helloworld はこれに適したサンプルとなり、ボタンをクリックすると Toast onButtonClick もポップアップします。また、Butterknife の @OnCheckedChanged、@OnPageChange、@OnItemClick などの原則も同様であるため、1 つずつ実装するわけではありません。