註解是一種能被添加到java代碼中的元數據,類、方法、變量、參數和包均可以用註解來修飾。註解對於它所修飾的代碼並無直接的影響。java
用於對註解類型進行註解的註解類,稱之爲元註解。JDK1.5中提供了4個標準元註解。android
@Target: 描述註解的使用範圍,說明被它所註解的註解類可修飾的對象範圍 @Retention: 描述註解保留的時期,被描述的註解在它所修飾的類中可保留到什麼時候 @Documented: 描述在使用Javadoc工具爲類生成幫助文檔時是否要保留其註解信息 @Inherited: 使被它修飾的註解修飾的註解類的子類能繼承到註解markdown
@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
@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注入。
下面以實際案例講解。案例目標: 實現註解綁定控件,效果以下
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();
}
}
複製代碼
app模塊內容和上述實現目標一致,相信都看得懂。下面逐一介紹其餘模塊
註解模塊存放註解,本案例中的註解爲@BindView。因爲要編譯期獲取註解,生成相關代碼,因此該註解爲編譯時代碼(@Retention(RetentionPolicy.CLASS);又由於要做用在屬性上,因此該註解的做用目標爲@Target(ElementType.FIELD);而且@BindView具備一個參數表明控件id,類型爲int。由此可得出以下註解聲明
//做用在屬性上
@Target(ElementType.FIELD)
//編譯時註解
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
//傳遞參數,此處爲控件id
int value();
}
複製代碼
一、本模塊要使用註解處理器,首先在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模塊的做用是配合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();
}
}
}
複製代碼
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模塊。下面一一介紹。
註解模塊內容依然是存放註解,這裏作演示,只用了一個BindView註解
@Target(ElementType.FIELD)
//運行時註解
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
}
複製代碼
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();
}
}
}
}
}
}
複製代碼
至此,便完成了控件注入。 能夠看到實際上運行時註解實現控件注入相對簡單些,但因爲這種方式使用了反射,運行效率上相對差一些。