深刻了解註解及Android APT

註解是什麼

註解是一種能被添加到java代碼中的元數據,類、方法、變量、參數和包均可以用註解來修飾。註解對於它所修飾的代碼並無直接的影響。java

一、什麼是元註解

用於對註解類型進行註解的註解類,稱之爲元註解。JDK1.5中提供了4個標準元註解。android

@Target: 描述註解的使用範圍,說明被它所註解的註解類可修飾的對象範圍 @Retention: 描述註解保留的時期,被描述的註解在它所修飾的類中可保留到什麼時候 @Documented: 描述在使用Javadoc工具爲類生成幫助文檔時是否要保留其註解信息 @Inherited: 使被它修飾的註解修飾的註解類的子類能繼承到註解markdown

二、元註解@Target的取值及其含義

@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Event {
}
複製代碼

@Target描述的是註解的使用範圍,攜帶的值爲枚舉,做用是標明它修飾的註解能夠用在哪些地方。好比上述例子中的@Event只能做用於屬性和方法上app

ElementType的取值和意義以下:框架

public enum ElementType {
	//做用在類上
	TYPE,	
	
	//做用在屬性上
	FIELD,	
	
	//做用在方法上
	METHOD,	
	
	//做用在參數上
	PARAMETER,	
	
	//做用在構造器上
	CONSTRUCTOR,	
	
	//做用在局部變量上
	LOCAL_VARIABLE,	
	
	//做用在註解上
	ANNOTATION_TYPE,
	
	//做用在包名上
	PACKAGE,	
	private ElementType(){...}
}
複製代碼

注意:每一個註解能夠跟n個ElementType關聯。當無指定時,註解可用於任何地方。ide

三、元註解@Retention的取值及其含義

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Event {
}
複製代碼

@Retention描述的是註解的存在時期。如上述例子中@Event爲運行時註解,在源碼,字節碼及運行時皆存在工具

RetentionPolicy取值和意義以下:gradle

public enum RetentionPolicy {
	//源碼時註解, 只在源碼中存在,編譯後便不存在了
	SOURCE, 
	
	//編譯時註解,源碼和編譯時存在,運行時不存在
	CLASS,
	
	//運行時註解,源碼,編譯時,運行時都存在
	RUNTIME;
	
	private RetentionPolicy(){...}
}
複製代碼

注意:每一個註解只能和一個RetentionPolicy關聯。當無指定時,默認爲RetentionPolicy.CLASSui

四、其餘元註解介紹

@Documented: 類和方法的Annotation在缺省狀況下是不出如今javadoc中的。若是使用@Documented修飾該註解,則表示它能夠出如今javadoc中。this

@Inheried: 當使用該註解的類有子類時,註解在子類仍然存在。經過反射其子類可得到父類相同的註解

五、自定義註解的參數

public @interface Person {
	public String name();
	//默認值
	int age() default 18;
	int[] array();
}
複製代碼

註解可以攜帶的參數類型有:基本數據類型,String, Class, Annotation, enum

註解的使用

註解目前比較常見的使用場景有

a、編譯時動態檢查,好比某參數的取值只能爲某些int值,如顏色。則可使用編譯時註解在編譯時對參數進行檢查

b、編譯時動態生成代碼,使用註解處理器在編譯時生成class文件。如ButterKnife實現

c、運行時動態注入,用註解實現IOC,許多框架將原有配置文件xml改爲註解用的即是IOC注入。

一、編譯時註解-Apt註解處理器使用

下面以實際案例講解。案例目標: 實現註解綁定控件,效果以下

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    public TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        Toast.makeText(this, tv.getText().toString(), Toast.LENGTH_SHORT).show();
    }
    
}
複製代碼

一、工程結構

在這裏插入圖片描述

  1. app模塊, 一個android模塊,是demo的主模塊,內容是demo的演示部分MainActivity
  2. annotation模塊,一個java Library模塊,用於放置註解
  3. compile模塊,一個java Library模塊,註解處理器主要實如今這個模塊中實現
  4. library模塊,一個android Library模塊,配合註解處理器生成的代碼,實現註解綁定控件功能

app模塊內容和上述實現目標一致,相信都看得懂。下面逐一介紹其餘模塊

二、annotation模塊

註解模塊存放註解,本案例中的註解爲@BindView。因爲要編譯期獲取註解,生成相關代碼,因此該註解爲編譯時代碼(@Retention(RetentionPolicy.CLASS);又由於要做用在屬性上,因此該註解的做用目標爲@Target(ElementType.FIELD);而且@BindView具備一個參數表明控件id,類型爲int。由此可得出以下註解聲明

//做用在屬性上
@Target(ElementType.FIELD)
//編譯時註解
@Retention(RetentionPolicy.CLASS)
public @interface BindView {

    //傳遞參數,此處爲控件id
    int value();

}
複製代碼

三、compile模塊

一、本模塊要使用註解處理器,首先在build.gradle中引入相關庫,build.gradle內容以下

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    
    //google出品,註解處理器庫
    compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
	
	//javapoet用於生成java類
    implementation 'com.squareup:javapoet:1.10.0'
    implementation project(':annotation')
}

sourceCompatibility = "7"
targetCompatibility = "7"
複製代碼

註解類:

第一步:掃描出代碼中被註解的屬性及其對應的activity,存放到map中

//做用是聲明註解處理器
@AutoService(Processor.class)
//聲明生成代碼是基於java1.7
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//聲明註解處理器支持的註解
@SupportedAnnotationTypes("com.sq.annotation.BindView")
public class ButterKnifeProcessor extends AbstractProcessor {

	//用於打印日誌
    private Messager mMessager;
	
	//存放activity和activity內註解的控件
    private Map<TypeElement, List<VariableElement>> mTargetMap;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    	//得到被BindView註解的全部元素
        Set<Element> views = (Set<Element>) roundEnvironment.getElementsAnnotatedWith(BindView.class);
        if (views != null && views.size() > 0) {
            //將activity和對應的註解的控件放到map中
            mTargetMap = new HashMap<>();
            for (Element view : views) {
                if (view instanceof VariableElement) {
                    //得到所屬類元素,即Activity
                    TypeElement activityElement = (TypeElement) view.getEnclosingElement();
                    if (mTargetMap.get(activityElement) == null) {
                        ArrayList targetList = new ArrayList<VariableElement>();
                        targetList.add(view);
                        mTargetMap.put(activityElement, targetList);
                    } else {
                        mTargetMap.get(activityElement).add((VariableElement) view);
                    }
                }
            }

            //遍歷對應activity
            if (mTargetMap.size() > 0) {
                for (Map.Entry<TypeElement, List<VariableElement>> entry : mTargetMap.entrySet()) {
                    String activityName = entry.getKey().getSimpleName().toString();
                    mMessager.printMessage(Diagnostic.Kind.NOTE,"activity類名爲:" + activityName);
                    for (VariableElement view : entry.getValue()) {
                        mMessager.printMessage(Diagnostic.Kind.NOTE, "被註解的屬性爲: " + view.getSimpleName().toString());
                    }
                    //爲每個activity生成代碼
                    generateCode(entry.getKey(), entry.getValue());
                }
            }
        }
        return false;
    }

}
複製代碼

上述代碼,打印出來的日誌爲: 注: activity類名爲:MainActivity 注: 被註解的屬性爲: tv

第二步:生成代碼 因爲是爲activity綁定控件,生成的代碼以下:

public class MainActivity$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }
}
複製代碼

其中,ViewBinder是接口,其代碼放於library模塊中,代碼以下:

public interface ViewBinder<T> {
    void bind(T target);
}
複製代碼

一般在生成代碼前,首先也是要先想明白生成的代碼是怎樣的,先有模板再開始寫生成的邏輯。

如下開始寫generateCode()方法內容

private void generateCode(TypeElement activityElement, List<VariableElement> viewElements) {

        //用於得到activity類名在javapoet中的表示
        ClassName className = ClassName.get(activityElement);

        //生成的類實現的接口
        TypeElement viewBinderType = mElementUtils.getTypeElement("com.sq.library.ViewBinder");
        //實現的接口在javapoet中的表示
        ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBinderType), className);

        //bind方法參數,即MainActivity target
        ParameterSpec parameterSpec = ParameterSpec.builder(className, "target", Modifier.FINAL).build();
        //方法聲明:public void bind(final MainActivity target)
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(parameterSpec);
        //方法體
        for (VariableElement viewElement : viewElements) {
            //獲取屬性名
            String fieldName = viewElement.getSimpleName().toString();
            //獲取@BindView註解的值
            int annotationValue = viewElement.getAnnotation(BindView.class).value();
            //target.tv = target.findViewById(R.id.tv);
            String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
            //加入方法內容
            methodBuilder.addStatement(methodContent, "target", "target", annotationValue);
        }

        //生成代碼
        try {
            JavaFile.builder(className.packageName(),
                    TypeSpec.classBuilder(className.simpleName() + "$ViewBinder")
                            .addSuperinterface(typeName)
                            .addModifiers(Modifier.PUBLIC)
                            .addMethod(methodBuilder.build())
                            .build())
                    .build()
                    .writeTo(mFiler);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
複製代碼

四、library模塊

library模塊的做用是配合compile模塊生成的代碼,提供給app模塊使用。實現的是MainActivity中ButterKnife.bind(this)以及compile模塊生成的MainActivity&MainActivityViewBinder實現的ViewBinder接口 ButterKnife類代碼以下:

public class ButterKnife {

    public static void bind(Activity activity) {

        try {
        	//找到對應activity的ViewBinder類,調用bind方法並將activity做爲參數傳入
            Class viewBinderClass = Class.forName(activity.getClass().getName() + "$ViewBinder");
            ViewBinder viewBinder = (ViewBinder) viewBinderClass.newInstance();
            viewBinder.bind(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
複製代碼

五、app模塊使用compile模塊

build.gradle配置以下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.sq.aptdemo"
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation project(':annotation')
    implementation project(':library')
    //引用註解處理模塊的方式以下:
    annotationProcessor project(":compile")
}
複製代碼

如何觸發? 在這裏插入圖片描述 make Module 'app'即可觸發編譯,使得compile模塊開始執行。

查看生成的代碼: 在這裏插入圖片描述 運行app模塊後運行正常,控件成功和id綁定。

至此,使用apt註解處理器生成代碼完成控件注入開發完成。

二、運行時註解實現控件注入

案例目標,效果以下:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //注入控件
        InjectUtils.bind(this);
        Toast.makeText(this, tv.getText().toString(), Toast.LENGTH_SHORT).show();
    }
}
複製代碼

能夠看出,使用時和利用編譯時註解生成代碼並沒有差異,不過這裏的屬性TextView 能夠是私有成員。由於注入使用的是反射實現的。

一、工程結構

在這裏插入圖片描述 比使用編譯時註解少了compile模塊。下面一一介紹。

二、annotation模塊

註解模塊內容依然是存放註解,這裏作演示,只用了一個BindView註解

@Target(ElementType.FIELD)
//運行時註解
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}
複製代碼

三、library模塊

public class InjectUtils {

    public static void bind(Activity target) {
        //獲取activity的Class
        Class activityClass = target.getClass();
        //獲取到activity全部屬性
        Field[] fields =  activityClass.getDeclaredFields();
        if (fields != null) {
            //遍歷全部屬性,找到有註解的屬性
            for (Field field : fields) {
                field.setAccessible(true);
                BindView annotation = field.getAnnotation(BindView.class);
                if (annotation != null) {
                    //獲取到註解帶的id
                    int id = annotation.value();
                    //找到id對應的view
                    View targetView = target.findViewById(id);
                    try {
                        //設置屬性的值爲對應的view,完成綁定
                        field.set(target, targetView);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
複製代碼

至此,便完成了控件注入。 能夠看到實際上運行時註解實現控件注入相對簡單些,但因爲這種方式使用了反射,運行效率上相對差一些。

相關文章
相關標籤/搜索