Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android注解-編譯時生成代碼 (APT)

Android注解-編譯時生成代碼 (APT)

編輯:關於Android編程

Android注解越來越引領潮流,比如 Dagger2, ButterKnife, EventBus3 等,他們都是注解類型,而且他們都有個共同點就是編譯時生成代碼,而不是運行時利用反射,這樣大大優化了性能;而這些框架都用到了同一個工具就是:APT(Annotation Processing Tool ),可以在代碼編譯期解析注解,並且生成新的 Java 文件,減少手動的代碼輸入。

今天我們要自己實現的就是類似ButterKnife的簡單的view初始化和點擊事件;

先看下整個項目的目錄結構:
圖片名稱

inject :API module用來把生成的文件與控件相關聯 viewinject-annotation :注解module viewinject-compiler : 用來生成java文件module

先從最簡單入手,注解moudle:
1.創建名字為viewinject-annotation的java類型module
2.該module只有兩個類:

1.BindView用來對成員變量進行注解,並且接收一個 int 類型的參數

 * Created by JokAr on 16/8/6.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

2.OnClick對方法進行注解,接收一個或一組 int 類型參數,相當於給一組 View 指定點擊響應事件。

/**
 * Created by JokAr on 16/8/6.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] value();
}

注解module就完成了,下面看看API module

1.首先創建一個Android moudle 的inject,然後創建interface

/**
 * Created by JokAr on 16/8/6.
 */
public interface Inject {

    void inject(T host, Object object, Provider provider);
}
/**
 * Created by JokAr on 16/8/6.
 */
public interface Provider {
    Context getContext(Object object);

    View findView(Object object, int id);
}

因為我們需要生成的文件是這麼寫的:

public class MainActivity$$ViewInject implements Inject {
  @Override
  public void inject(final MainActivity host, Object source, Provider provider) {
    host.textView = (TextView)(provider.findView(source, 2131427412));
    host.button1 = (Button)(provider.findView(source, 2131427413));
    View.OnClickListener listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.click();
      }
    } ;
    provider.findView(source, 2131427412).setOnClickListener(listener);
  }
}

當然這個生成文件是根據自己需求生成,然後需要一個類來關聯自己的activity類與生成的類:

/**
 * Created by JokAr on 16/8/6.
 */
public class ViewInject {
    private static final ActivityProvider activityProvider = new ActivityProvider();

    private static final ViewProvider viewProvider = new ViewProvider();
    private static final ArrayMap injectMap = new ArrayMap<>();

    public static void inject(Activity activity) {
        inject(activity, activity, activityProvider);
    }

    public static void inject(View view) {
        inject(view, view);
    }

    private static void inject(Object host, View view) {
        inject(host, view, viewProvider);
    }

    private static void inject(Object host, Object object, Provider provider) {
        String className = host.getClass().getName();
        try {
            Inject inject = injectMap.get(className);

            if (inject == null) {
                Class aClass = Class.forName(className + "$$ViewInject");
                inject = (Inject) aClass.newInstance();
                injectMap.put(className, inject);
            }
            inject.inject(host, object, provider);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

使用方法就是:

ViewInject.inject(this);
host 表示注解 View 變量所在的類,也就是注解類 object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找 provider 是一個接口,定義了不同對象(比如 Activity、View 等)如何去查找目標 View,項目中分別為 Activity、View 實現了 Provider 接口(具體實現參考項目代碼) 為了提高效率,避免每次注入的時候都去找 Inject 對象,用一個 Map 將第一次找到的對象緩存起來,後面用的時候直接從 Map 裡面取。

API module類就完成了

再看viewinject-compilermodule:
首先創建名為iewinject-compiler的Java module ,然後在該module的buile.gradle加上一些依賴:

compile project(':viewinject-annotation')
compile 'com.squareup:javapoet:1.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'
Javapoet是square一個工具,提供了各種 API 讓你用各種姿勢去生成 Java 代碼文件,避免了徒手拼接字符串的尴尬。 auto-service 主要用於注解 Processor,對其生成 META-INF 配置信息。

首先創建ViewInjectProcesser類:

/**
 * Created by JokAr on 16/8/8.
 */
@AutoService(Processor.class)
public class ViewInjectProcesser extends AbstractProcessor {
    private Filer mFiler; //文件相關的輔助類
    private Elements mElementUtils; //元素相關的輔助類
    private Messager mMessager; //日志相關的輔助類

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        mAnnotatedClassMap = new TreeMap<>();
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {

        return false;
    }


     /**
     * 指定使用的 Java 版本。通常返回SourceVersion.latestSupported()。
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    /**
     * 指定哪些注解應該被注解處理器注冊
     * @return 
     */
    @Override
    public Set getSupportedAnnotationTypes() {
        Set types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }


}
用 @AutoService 來注解這個處理器,可以自動生成配置信息。 在 init() 可以初始化拿到一些實用的工具類。
這裡涉及到了Element 元素,借用一下別人的分析:
這裡寫圖片描述

這個類的的基本內容就完成了,
現在創建BindViewField類,來解析BindView注解類來獲取用該注解的相關信息

/**
 * Created by JokAr on 16/8/8.
 */
public class BindViewField {
    private VariableElement mVariableElement;
    private int mresId;

    public BindViewField(Element element) throws IllegalArgumentException{
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s",
                    BindView.class.getSimpleName()));
        }
        mVariableElement = (VariableElement) element;

        BindView bindView = mVariableElement.getAnnotation(BindView.class);
        mresId = bindView.value();
        if (mresId < 0) {
            throw new IllegalArgumentException(
                    String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
                            mVariableElement.getSimpleName()));
        }
    }

   /**
     * 獲取變量名稱
     * @return
     */
    public Name getFieldName() {
        return mVariableElement.getSimpleName();
    }

    /**
     * 獲取變量id
     * @return
     */
    public int getResId() {
        return mresId;
    }

    /**
     * 獲取變量類型
     * @return
     */
    public TypeMirror getFieldType() {
        return mVariableElement.asType();
    }
}

創建OnClickMethod類來解析使用OnClick注解的方法,獲取相關信息

public class OnClickMethod {
    private ExecutableElement mExecutableElement;
    private int[] resIds;
    private Name mMethodName;

    public OnClickMethod(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.METHOD) {
            throw new IllegalArgumentException(
                    String.format("Only methods can be annotated with @%s",
                            OnClick.class.getSimpleName()));
        }

        mExecutableElement = (ExecutableElement) element;

        resIds = mExecutableElement.getAnnotation(OnClick.class).value();

        if (resIds == null) {
            throw new IllegalArgumentException(String.format("Must set valid ids for @%s",
                    OnClick.class.getSimpleName()));
        } else {
            for (int id : resIds) {
                if (id < 0) {
                    throw new IllegalArgumentException(String.format("Must set valid id for @%s",
                            OnClick.class.getSimpleName()));
                }
            }
        }
        mMethodName = mExecutableElement.getSimpleName();
        List parameters = mExecutableElement.getParameters();

        if (parameters.size() > 0) {
            throw new IllegalArgumentException(
                    String.format("The method annotated with @%s must have no parameters",
                            OnClick.class.getSimpleName()));
        }
    }

    /**
     * 獲取方法名稱
     * @return
     */
    public Name getMethodName() {
        return mMethodName;
    }

    /**
     * 獲取id數組
     * @return
     */
    public int[] getResIds() {
        return resIds;
    }
}

然後重點就是生成Java代碼文件的類:

/**
 * Created by JokAr on 16/8/8.
 */
public class AnnotatedClass {

    private TypeElement mTypeElement;
    private ArrayList mFields;
    private ArrayList mMethods;
    private Elements mElements;

    public AnnotatedClass(TypeElement typeElement, Elements elements) {
        mTypeElement = typeElement;
        mElements = elements;
        mFields = new ArrayList<>();
        mMethods = new ArrayList<>();
    }

    public String getFullClassName() {
        return mTypeElement.getQualifiedName().toString();
    }

    public void addField(BindViewField field) {
        mFields.add(field);
    }

    public void addMethod(OnClickMethod method) {
        mMethods.add(method);
    }

    public JavaFile generateFile() {
        //generateMethod
        MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mTypeElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtil.PROVIDER,"provider");

        for(BindViewField field : mFields){
            // find views
            injectMethod.addStatement("host.$N = ($T)(provider.findView(source, $L))",
                    field.getFieldName(),
                    ClassName.get(field.getFieldType()), field.getResId());
        }

        for(OnClickMethod method :mMethods){
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
                    .addMethod(MethodSpec.methodBuilder("onClick")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(TypeName.VOID)
                            .addParameter(TypeUtil.ANDROID_VIEW, "view")
                            .addStatement("host.$N()", method.getMethodName())
                            .build())
                    .build();
            injectMethod.addStatement("View.OnClickListener listener = $L ", listener);
            for (int id : method.getResIds()) {
                // set listeners
                injectMethod.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
            }
        }

        //generaClass
        TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewInject")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJET, TypeName.get(mTypeElement.asType())))
                .addMethod(injectMethod.build())
                .build();

        String packgeName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();

        return JavaFile.builder(packgeName, injectClass).build();
    }
}

具體的可以看javapoet的API,然後我們需要完善ViewInjectProcesser類,增加:

private Map mAnnotatedClassMap;

 @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        mAnnotatedClassMap.clear();

        try {
            processBindView(roundEnv);
            processOnClick(roundEnv);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            error(e.getMessage());
        }

        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            try {
                annotatedClass.generateFile().writeTo(mFiler);
            } catch (IOException e) {
                error("Generate file failed, reason: %s", e.getMessage());
            }
        }
        return true;
    }

    private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {

        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField bindViewField = new BindViewField(element);
            annotatedClass.addField(bindViewField);
        }
    }

    private void processOnClick(RoundEnvironment roundEnv) throws IllegalArgumentException {
        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            OnClickMethod onClickMethod = new OnClickMethod(element);
            annotatedClass.addMethod(onClickMethod);
        }
    }
private void error(String msg, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
    }

實際使用

在項目的根目錄的build.gradle添加:

 classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

在項目的主module的build.gradle添加:

apply plugin: 'com.neenbedankt.android-apt'

compile project(':viewinject-annotation')
compile project(':inject')
apt project(':viewinject-compiler')

在自己的activity類使用:

/**
 * Created by JokAr on 16/8/8.
 */
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.textView)
    TextView textView;
    @BindView(R.id.button1)
    Button button1;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInject.inject(this);

    }

    @OnClick(R.id.textView)
    public void click() {
        Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show();
    }


}

點擊makeProject 就編譯完成後就可以在主項目module的/build/generated/source/apt/debug 目錄下看到生成的java類文件了

一個學習級的apt項目就完成了。

項目源碼

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved