banner
jzman

jzman

Coding、思考、自觉。
github

コンパイル時アノテーションの詳細とButterKnifeの実装

PS:人は自己暗示を受け入れることに非常に積極的な生物です。あなたが自分に消極的な暗示を与えれば、簡単に落ち込んでしまいますが、逆に自分に積極的な暗示を与えれば、あなたも積極的になれるでしょう。

今日はコンパイル時のアノテーションに関する知識を見てみましょう。手動で実践すれば、Dagger、ARouter、ButterKnife などのコンパイル時アノテーションを使用したフレームワークを理解しやすくなり、その内部のソースコードの実装も理解しやすくなるでしょう。内容は以下の通りです:

  1. コンパイル時と実行時のアノテーション
  2. アノテーション処理器 APT
  3. AbstractProcessor
  4. Element と Elements
  5. カスタムアノテーション処理器
  6. カスタムアノテーション処理器の使用

コンパイル時と実行時のアノテーション#

まず、コンパイル時と実行時の違いを理解しましょう:

  1. コンパイル時:コンパイラがソースコードを機械が認識できるコードに翻訳するプロセスを指します。Java では、Java ソースコードを JVM が認識できるバイトコードファイルにコンパイルするプロセスです。
  2. 実行時:JVM がメモリを割り当て、バイトコードファイルを解釈して実行するプロセスを指します。

メタアノテーション@Retentionは、アノテーションがコンパイル時か実行時かを決定します。その設定可能な戦略は以下の通りです:

public enum RetentionPolicy {
    SOURCE,  // コンパイル時に破棄され、ソースコード内にのみ存在
    CLASS,   // デフォルトの戦略で、実行時に破棄され、classファイル内にのみ存在
    RUNTIME  // コンパイル時にアノテーション情報がclassファイルに記録され、実行時にも保持され、リフレクションを通じてアノテーション情報を取得可能
}

コンパイル時アノテーションと実行時アノテーションは、上記の違いに加えて、実装方法も異なります。コンパイル時アノテーションは一般的にアノテーション処理器(APT)を通じて実装され、実行時アノテーションは一般的にリフレクションを通じて実装されます。

アノテーションとリフレクションに関する詳細は、以下の 2 つの記事を参照してください:

APT とは#

APT(Annotation Processing Tool)は、javac が提供するアノテーションを処理するためのツールで、コンパイル時にアノテーションをスキャンして処理します。簡単に言えば、APT を使用してアノテーションおよびそのアノテーションの位置情報を取得し、これらの情報を使用してコンパイラがコードを生成します。コンパイル時アノテーションは、APT を通じてアノテーション情報を使用してコードを生成し、特定の機能を実現します。典型的な例として ButterKnife、Dagger、ARouter などがあります。

AbstractProcessor#

AbstractProcessorProcessorを実装したアノテーション処理器の抽象クラスで、アノテーション処理器を実装するにはAbstractProcessorを継承して拡張する必要があります。主なメソッドの説明は以下の通りです:

  • init:Processor を初期化します。ProcessingEnvironmentのパラメータからツールクラスElementsTypesFiler、およびMessagerなどを取得できます。
  • getSupportedSourceVersion:使用する Java バージョンを返します。
  • getSupportedAnnotationTypes:処理するすべてのアノテーション名を返します。
  • process:指定されたすべてのアノテーションを取得して処理します。

process メソッドは、他のクラスが生成されなくなるまで、実行中に複数回実行される可能性があります。

Element と Elements#

Elementは XML のタグに似ており、Java ではElementはプログラム要素(クラス、メンバー、メソッドなど)を表します。各Elementは特定の構造を表し、Elementオブジェクトの操作には visitor または getKind () メソッドを使用して判断し、具体的に処理します。Elementのサブクラスは以下の通りです:

  • ExecutableElement
  • PackageElement
  • Parameterizable
  • QualifiedNameable
  • TypeElement
  • TypeParameterElement
  • VariableElement

上記の要素構造は以下のコード構造に対応します:

// PackageElement
package manu.com.compiler;

// TypeElement
public class ElementSample {
    // VariableElement
    private int a;
    // VariableElement
    private Object other;

    // ExecutableElement
    public ElementSample() {
    }
    // メソッドパラメータVariableElement
    public void setA(int newA) {
    }
    // TypeParameterElementはパラメータ化された型を表し、ジェネリックパラメータで使用されます
}

ElementsElementを処理するためのツールクラスで、インターフェースのみを提供し、具体的には Java プラットフォームが実装しています。

カスタムコンパイルアノテーション処理器#

以下に APT を使用してアノテーション@Bindを実装し、ButterKnife のアノテーション@BindViewを模倣します。サンプルプロジェクトの構造は以下の通りです:

image

api モジュールでアノテーション@Bindを以下のように定義します:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
    int value();
}

compiler モジュールは Java モジュールで、Google の auto-service を導入して META-INFO 下の関連ファイルを生成し、javapoet を使用して Java ファイルをより簡単に作成します。カスタムアノテーション処理器BindProcessorは以下の通りです:

// META-INF/services/javax.annotation.processing.Processorファイルを生成するために使用
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
// Javaファイルを作成するために使用
implementation 'com.squareup:javapoet:1.12.1'
/**
 * BindProcessor
 */
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {
    private Elements mElements;
    private Filer mFiler;
    private Messager mMessager;

    // 特定のクラスに対応するBindModelを保存
    private Map<TypeElement, List<BindModel>> mTypeElementMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        print("init");
        // Processorを初期化

        mElements = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        print("getSupportedSourceVersion");
        // 使用するJavaバージョンを返す
        return SourceVersion.RELEASE_8;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        print("getSupportedAnnotationTypes");
        // 処理するすべてのアノテーション名を返す
        Set<String> set = new HashSet<>();
        set.add(Bind.class.getCanonicalName());
        set.add(OnClick.class.getCanonicalName());
        return set;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        print("process");
        mTypeElementMap.clear();
        // 指定されたClass型のElementを取得
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Bind.class);
        // 条件に合うElementを保存
        for (Element element : elements) {
            // Elementに対応するクラスの完全修飾名を取得
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            print("process typeElement name:" + typeElement.getSimpleName());
            List<BindModel> modelList = mTypeElementMap.get(typeElement);
            if (modelList == null) {
                modelList = new ArrayList<>();
            }
            modelList.add(new BindModel(element));
            mTypeElementMap.put(typeElement, modelList);
        }

        print("process mTypeElementMap size:" + mTypeElementMap.size());

        // Javaファイル生成
        mTypeElementMap.forEach((typeElement, bindModels) -> {
            print("process bindModels size:" + bindModels.size());
            // パッケージ名を取得
            String packageName = mElements.getPackageOf(typeElement).getQualifiedName().toString();
            // Javaファイルのファイル名を生成
            String className = typeElement.getSimpleName().toString();
            String newClassName = className + "_ViewBind";

            // MethodSpec
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.bestGuess(className), "target");
            bindModels.forEach(model -> {
                constructorBuilder.addStatement("target.$L=($L)target.findViewById($L)",
                        model.getViewFieldName(), model.getViewFieldType(), model.getResId());
            });
            // typeSpec
            TypeSpec typeSpec = TypeSpec.classBuilder(newClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(constructorBuilder.build())
                    .build();
            // JavaFile
            JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
                    .addFileComment("AUTO Create")
                    .build();

            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        return true;
    }

    private void print(String message) {
        if (mMessager == null) return;
        mMessager.printMessage(Diagnostic.Kind.NOTE, message);
    }
}

ここでBindModelはアノテーション@Bindの情報を簡単にラップしたものです:

/**
 * BindModel
 */
public class BindModel {
    // メンバー変数Element
    private VariableElement mViewFieldElement;
    // メンバー変数の型
    private TypeMirror mViewFieldType;
    // ViewのリソースID
    private int mResId;

    public BindModel(Element element) {
        // Elementがメンバー変数であることを検証
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException("element is not FIELD");
        }
        // メンバー変数Element
        mViewFieldElement = (VariableElement) element;
        // メンバー変数の型
        mViewFieldType = element.asType();
        // アノテーションの値を取得
        Bind bind = mViewFieldElement.getAnnotation(Bind.class);
        mResId = bind.value();
    }

    public int getResId(){
        return mResId;
    }

    public String getViewFieldName(){
        return mViewFieldElement.getSimpleName().toString();
    }

    public TypeMirror getViewFieldType(){
        return mViewFieldType;
    }
}

bind モジュールで生成するファイルを作成します:

/**
 * 初期化
 */
public class BindKnife {
    public static void bind(Activity activity) {
        // activityの完全修飾クラス名を取得
        String name = activity.getClass().getName();
        try {
            // リフレクションを使用してActivityを生成し、注入
            Class<?> clazz = Class.forName(name + "_ViewBind");
            clazz.getConstructor(activity.getClass()).newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

もちろん、ButterKnife は生成したオブジェクトをキャッシュしており、毎回新しいオブジェクトを生成することはありません。リフレクションを使用していますが、実行時アノテーションに比べてリフレクションのテストが大幅に減少するため、性能も実行時アノテーションよりも良好です。これがコンパイル時アノテーションと実行時アノテーションの違いです。

カスタムアノテーション処理器の使用#

使用方法は ButterKnife に似ています。以下のようになります:

public class MainActivity extends AppCompatActivity{
   
    @Bind(R.id.tvData)
    TextView tvData;

    @Override
     public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindKnife.bind(this);
        tvData.setText("data");
    }
}

コンパイル時アノテーションを理解することは、すぐに新しいものを作ることにはならないかもしれませんが、他のフレームワークを見る際に非常に役立ち、問題を解決する際の選択肢が増えます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。