靜態注解動態注解


寫在前面:本文是實際工作中學習成果,記為筆記

目錄

  1. 背景
  2. 什么是注解
  3. 注解實戰:動態注解
  4. 注解實戰:靜態注解
  5. 注解處理器
  6. 調試注解器
  7. 注解的問題
  8. 總結
1. 背景

最近有些時間,突然對注解有些興趣,很早之前也做過一些關於注解的學習,我的第一篇博客 Sqlla: 數據庫操作從未如此簡單里面使用到了動態注解技術,是當時學習代理和retrofit時的情況下實現的,后續又迭代了幾版。最近的三方庫大量使用靜態注解的方式,出於學習和納為己用的目的,打算系統的學習一下,這篇文章就是學習筆記。

2. 什么是注解

一切使用@interface聲明的類就是注解

所有的注解繼承於Annotation類,好比所有的類繼承與Object一樣。

public @interface IntentKey /*extends Annotation*/ { } 

注解用來標記類,屬性,方法,參數,局部變量,包。

@IntentKey private String name; 

注解有三大基本屬性:

  1. Retention作用域
  2. Target標識目標
  3. Value常量數據值

完整的注解:可以看出Retention和Target本身也是注解。

@Retention(RetentionPolicy.SOURCE) @Target(ElementType.FIELD) public @interface IntentKey { String value() default ""; String intentKey(); } 

Retention的取值:源碼級別,jar類級別,運行時級別

public enum RetentionPolicy { SOURCE, CLASS, RUNTIME } 

第一個只作用於編譯期,打包之后就沒有了。比如@Override
第二個作用於jar包類,打包之后還存在,如Android的@Nullable,方便編譯期跨包做lint檢查
第三個作用於運行期,在運行時可通過反射取得這個注解的信息,如retrofit的@GET注解

@IntentKey(value="abc", intentKey="name") private String name; @IntentKey(intentKey="name2") private String name2; 

技巧:當只有一個value的時候,使用時value可以省略。如果有default值,可以不填。

坑點:

public @interface Names { String[] names1(); Object[] names2(); } 

如上,names1是合法的,names2是非法的。原因是注解的值必須是常量,支持的類型如下:

8中基本數據類型,String,Class,Annotation及子類,枚舉 以及它們的數組

3. 注解實戰:動態注解

動態注解,就是運行時注解。

注解聲明:

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface DynamicIntentKey { String value(); } 

標記類:

public class MyActivity extends AppCompatActivity { // 標記 @DynamicIntentKey("key_name") private String dynamicName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my); // 注入 DynamicUtil.inject(this); // 這里使用值 Toast.make(this, "name = " + dynamicName, Toast.SHORT).show(); } } 

DynamicUtil.inject(this)負責將值注入

public class DynamicUtil { public static void inject(Activity activity) { Intent intent = activity.getIntent(); // 反射 for (Field field : activity.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(DynamicIntentKey.class)) { // 獲取注解 DynamicIntentKey annotation = field.getAnnotation(DynamicIntentKey.class); String intentKey = annotation.value(); // 讀取實際的IntentExtra值 Serializable serializable = intent.getSerializableExtra(intentKey); if (serializable == null) { if (field.getType().isAssignableFrom(String.class)) { serializable = ""; } } try { // 插入值 boolean accessible = field.isAccessible(); field.setAccessible(true); field.set(activity, serializable); field.setAccessible(accessible); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } } 

可以看到解析動態注解使用的是反射的方式,人們常說,反射會有效率問題,后面會驗證是或不是。
但是我們能看到優點:

對於使用的地方而言,非常簡單了,省去了getXXXExtra的代碼,如果這個Intent很多的時候,注解的方式優勢就非常明細,添加和刪除intent都不需要去改動onCreate的方法,而且inject過程可以放到super類里完成。

這個注解就特別像butterknife對view的注入,而且早期的butterknife確實是使用的同樣的動態注解的方式。可是后來,靜態注解出現了,如燎原之火般席卷而來。接下來是靜態注解。

4. 注解實戰:靜態注解

靜態注解,就非常好理解了,在編譯期解釋注解,並做一些生成操作。三種作用域都可以用來標示靜態注解。
注解聲明:

@Target(ElementType.FIELD) @Retention(RetentionPolicy.SOURCE) public @interface StaticIntentKey { String value(); } 

標記類:

public class MyActivity extends AppCompatActivity { // 標記 @StaticIntentKey("key_name") private String staticName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my); // 靜態注入 StaticUtil.inject(this); // 這里使用值 Toast.make(this, "name = " + staticName, Toast.SHORT).show(); } } 

上面為止和動態注入都是一模一樣的。
下面是靜態注入的核心實現:

public class StaticUtil { public static void inject(Activity activity) { com.alpha.staticinject.StaticMapper.bind(activity); } } 

???com.alpha.staticinject.StaticMapper是個什么東西?
這個不是東西,他就是靜態注解的核心:代碼生成。這個類就是靜態注解處理器在編譯時期生成的一個輔助類。
下面兩個類就是生成的類:

public final class StaticMapper { public static final void bind(Activity activity) { if (activity instanceof IntentActivity) { IntentActivity$Binder binder = new IntentActivity$Binder(); binder.bind((IntentActivity) activity); } } } 
public final class IntentActivity$Binder { public static final void bind(IntentActivity activity) { Intent intent = activity.getIntent(); if (intent.hasExtra("key_name")) { activity.staticName = (String) intent.getSerializableExtra("key_name"); } } } 

這就是注入值的過程。但是這兩個類是怎么樣生成的呢?

5. 注解處理器

Java在1.7加入了Processor類,用於處理編譯時注解。通過這個類我們可以在某些目錄下生成某些類,這些類可以輔助我們開發,讓我們更關注業務邏輯。

public interface Processor { Set<String> getSupportedOptions(); Set<String> getSupportedAnnotationTypes(); SourceVersion getSupportedSourceVersion(); void init(ProcessingEnvironment var1); // 處理類在這里完成 boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2); Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4); } 

編寫一個自定義注解的四條步驟:

  1. 創建一個java library:intent-apt (這是我項目的名字)
  2. 編寫一個IntentProcessor繼承與Processor的抽象類AbstractProcessor。
  3. 將IntentProcessor作為一種服務放入標記到jar包中的META_INF文件夾中
  4. 在其他項目中,如android項目中使用 annotationProcessor project(':intent-apt')引入依賴

走一遍:

  1. 創建java library很簡單,不細說
  2. 創建IntentProcessor
public class IntentProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) { // 在這里寫你要生成的類 } } 

但是這個IntentProcessor現在還沒有什么核心內容,下一步我們給他添加一些東西。
要注解器能好用,我們得先要有注解:
1>. 提供注解的library


 
注解library

2>. 在intent-apt中引入這個library

dependencies {
    ...
    implementation project(':intent-key') } 

3>. 上面描述的靜態注解生成過程是如下這樣的:
(1) 獲取需要注解的類F,這里是所有有StaticIntentKey標記的類。
(2) 針對每一個類F,生成同包下的F$Binder類,用於實際的綁定
(3) 將所有類合起來生成一個StaticMapper的集線器類,用於分發綁定

完整的類如下:

public class IntentProcessor extends AbstractProcessor { private TypeName activityClassName = ClassName.get("android.app", "Activity").withoutAnnotations(); private TypeName intentClassName = ClassName.get("android.content", "Intent").withoutAnnotations(); @Override public SourceVersion getSupportedSourceVersion() { // 支持java1.7 return SourceVersion.RELEASE_7; } @Override public Set<String> getSupportedAnnotationTypes() { // 只處理 StaticIntentKey 注解 return Collections.singleton(StaticIntentKey.class.getCanonicalName()); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) { // StaticMapper的bind方法 MethodSpec.Builder method = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addParameter(activityClassName, "activity"); // 查找所有的需要注入的類描述 List<InjectDesc> injectDescs = findInjectDesc(set, re); // log一下 System.out.println(injectDescs); for (int i1 = 0; i1 < injectDescs.size(); i1++) { InjectDesc injectDesc = injectDescs.get(i1); // 創建需要注解的類的Java文件,如上面所述的 IntentActivity$Binder TypeName injectedType = createInjectClassFile(injectDesc); TypeName activityName = typeName(injectDesc.activityName); // $T導入類型 // 生成綁定分發的代碼 method.addCode((i1 == 0 ? "" : " else ") + "if (activity instanceof $T) {\n", activityName); method.addCode("\t$T binder = new $T();\n", injectedType, injectedType); method.addCode("\tbinder.bind((IntentActivity) activity);\n", activityName, activityName); method.addCode("}"); } // 創建StaticMapper類 createJavaFile("com.alpha.staticinject", "StaticMapper", method.build()); return false; } /** * 創建Java文件 * @param pkg 包名 * @param classShortName 類的簡短名,如java.lang.String的簡短名就是String * @param method 方法列表 */ private void createJavaFile(String pkg, String classShortName, MethodSpec... method) { TypeSpec.Builder builder = TypeSpec.classBuilder(classShortName) .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (MethodSpec spec : method) { builder.addMethod(spec); } TypeSpec clazzType = builder.build(); try { JavaFile javaFile = JavaFile.builder(pkg, clazzType) .addFileComment(" This codes are generated automatically. Do not modify!") .indent(" ") .build(); // write to file javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } /** * 生成需要注解的類 如 IntentActivity 會生成 IntentActivity$Binder * * @param injectDesc 注解類的描述信息 * @return 如IntentActivity$Binder */ private TypeName createInjectClassFile(InjectDesc injectDesc) { ClassName activityName = className(injectDesc.activityName); ClassName injectedClass = ClassName.get(activityName.packageName(), activityName.simpleName() + "$Binder"); MethodSpec.Builder method = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addParameter(activityName, "activity"); // $T導入作為類,$N導入作為純值,$S導入作為字符串 method.addStatement("$T intent = activity.getIntent()", intentClassName); for (int i = 0; i < injectDesc.fieldNames.length; i++) { TypeName fieldTypeName = typeName(injectDesc.fieldTypeNames[i]); method.addCode("if (intent.hasExtra($S)) {\n", injectDesc.intentNames[i]); method.addCode("\tactivity.$N = ($T) intent.getSerializableExtra($S);\n", injectDesc.fieldNames[i], fieldTypeName, injectDesc.intentNames[i]); method.addCode("}\n"); } // 生成最終的XXX$Binder文件 createJavaFile(injectedClass.packageName(), injectedClass.simpleName(), method.build()); return injectedClass; } // 查找所有的需要注入的類描述 private List<InjectDesc> findInjectDesc(Set<? extends TypeElement> set, RoundEnvironment re) { Map<TypeElement, List<String[]>> targetClassMap = new HashMap<>(); // 先獲取所有被StaticIntentKey標示的元素 Set<? extends Element> elements = re.getElementsAnnotatedWith(StaticIntentKey.class); for (Element element : elements) { // 只關心類別是屬性的元素 if (element.getKind() != ElementKind.FIELD) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field"); continue; } // 此處找到的是類的描述類型 // 因為我們的StaticIntentKey的注解描述是field,所以closingElement元素是類 TypeElement classType = (TypeElement) element.getEnclosingElement(); System.out.println(classType); // 對類做緩存,避免重復 List<String[]> nameList = targetClassMap.get(classType); if (nameList == null) { nameList = new ArrayList<>(); targetClassMap.put(classType, nameList); } // 被注解的值,如staticName String fieldName = element.getSimpleName().toString(); // 被注解的值的類型,如String,int String fieldTypeName = element.asType().toString(); // 注解本身的值,如key_name String intentName = element.getAnnotation(StaticIntentKey.class).value(); String[] names = new String[]{fieldName, fieldTypeName, intentName}; nameList.add(names); } List<InjectDesc> injectDescList = new ArrayList<>(targetClassMap.size()); for (Map.Entry<TypeElement, List<String[]>> entry : targetClassMap.entrySet()) { String className = entry.getKey().getQualifiedName().toString(); System.out.println(className); // 封裝成自定義的描述符 InjectDesc injectDesc = new InjectDesc(); injectDesc.activityName = className; List<String[]> value = entry.getValue(); injectDesc.fieldNames = new String[value.size()]; injectDesc.fieldTypeNames = new String[value.size()]; injectDesc.intentNames = new String[value.size()]; for (int i = 0; i < value.size(); i++) { String[] names = value.get(i); injectDesc.fieldNames[i] = names[0]; injectDesc.fieldTypeNames[i] = names[1]; injectDesc.intentNames[i] = names[2]; } injectDescList.add(injectDesc); } return injectDescList; } /** * 快速獲取一個類的TypeName結構 * * @param className 類完整名,如java.lang.String,也可能是int, boolean */ private TypeName typeName(String className) { return className(className).withoutAnnotations(); } /** * 快速獲取一個類的ClassName結構,ClassName是TypeName的子類 * * @param className 類完整名,如java.lang.String,也可能是int, boolean */ private ClassName className(String className) { // 基礎類型描述符 if (className.indexOf(".") <= 0) { switch (className) { case "byte": return ClassName.get("java.lang", "Byte"); case "short": return ClassName.get("java.lang", "Short"); case "int": return ClassName.get("java.lang", "Integer"); case "long": return ClassName.get("java.lang", "Long"); case "float": return ClassName.get("java.lang", "Float"); case "double": return ClassName.get("java.lang", "Double"); case "boolean": return ClassName.get("java.lang", "Boolean"); case "char": return ClassName.get("java.lang", "Character"); default: } } // 手動解析 java.lang.String,分成java.lang的包名和String的類名 String packageD = className.substring(0, className.lastIndexOf('.')); String name = className.substring(className.lastIndexOf('.') + 1); return ClassName.get(packageD, name); } /** * 需要注解的類的描述信息 如 IntentActivity */ public static class InjectDesc { // IntentActivity public String activityName; // {staticName, staticAge} public String[] fieldNames; // {java.lang.String, java.lang.Integer} public String[] fieldTypeNames; // {key_name, key_age} public String[] intentNames; @Override public String toString() { return "InjectDesc{" + "activityName='" + activityName + '\'' + ", fieldNames=" + Arrays.toString(fieldNames) + ", intentNames=" + Arrays.toString(intentNames) + '}'; } } } 

StringBuilder一行一行寫生成的類的代碼非常無味,還容易出錯,所以上面用到了square公司出品的javapoet, 方便生成類。
添加javapoet

dependencies {
    ...
    implementation 'com.squareup:javapoet:1.9.0' } 
  1. 想java系統注冊服務,這個服務注冊應該見過,在編寫自定義Gradle插件的時候也需要這么做。


     
    注冊服務

javax.annotation.processing.Processor 文件的內容如下:
com.icourt.intent_apt.IntentProcessor

實際就是這個IntentProcessor的完整類名。

當然,這么寫很不優雅,不一定記得住,所以google做了一個注解,方便快速的幫我們生成這個文件,只需在IntentProcessor頭部注解一下

// AutoService本身就是一個靜態注解,他在build/META-INF文件夾下生成了一個service指定文件 @AutoService(Processor.class) public class IntentProcessor extends AbstractProcessor { ... } 

添加這個注解需要加入依賴

dependencies {
    // google auto service注解庫 implementation 'com.google.auto.service:auto-service:1.0-rc2' } 

它生成的文件在build下面


 
AutoService

這兩種方法只需要一種就可以,最終會打包到jar里面。

  1. 在需要提供編譯時注解的項目(比如app)的build.gradle文件的dependences節點下添加
dependencies {
    ...
    annotationProcessor project(':intent-apt') } 

intent-apt項目本身完整的build.gradle

apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.google.auto.service:auto-service:1.0-rc2' implementation 'com.squareup:javapoet:1.9.0' implementation project(':intent-key') } sourceCompatibility = "1.7" targetCompatibility = "1.7" 

好,自此,一個完整的靜態注解器就完成了,可以馬上用來實驗。只需要build就可以使用,他會在你的目標項目(如app)的build下生成相關的類:


 
生成類的路徑
6. 調試注解器

有的時候,生成過程出錯了,但我不確定是哪一步出錯了,想要定位,有兩種方法:

  1. 日志定位,方便快捷
// 1. sout System.out.println(xxx) // 2. messager processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field"); 
  1. 斷點調試

有三步要配置:
第一步 配置Debug后台服務
在gradle.properties文件中加入下面兩句話,然后sync一下項目(或者在控制台執行./gradlew --daemon),會開啟一個遠程debug_server

org.gradle.daemon=true org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 

第二步 配置Remote Debugger
在AS中創建一個RemoteDebugger

 
 

 
 

 
 

屬性和參數和上一步的配置一樣,(不修改就行)

 

 
開啟debug

切換到剛才創建的'運行配置',點擊debug按鈕。

第三步 執行編譯過程

在需要的位置打開斷點,在控制台輸入

./gradlew assembleDebug

或者,帶清除功能的編譯

./gradlew clean assembleDebug

不出意外就會走到打開的斷點,然后盡情的調試。

7. 注解的問題

(1) 性能問題:
1000次運行的效果對比

DynamicUtil.inject(this); StaticUtil.inject(this); 

(2) 代碼量:
注解對於業務邏輯的代碼整體減少了,但是靜態注解會產生 生成代碼,這些代碼會占用方法數,也增加編譯時間。

(3) 錯誤定位:
注解的錯誤定位會很麻煩,如果注解處理器出了bug,較難定位。

(4) 閱讀性:
注解會整體影響閱讀性,出現邏輯斷層。

8. 總結

本文圍繞兩點展開:

  1. 使用注解(注)
  2. 編寫注解(解)

使用注解非常有優勢,讓開發人員優化代碼,關注業務。但是編寫注解處理器本身是個技術活,好的處理器對使用者而言會事半功倍,但也不要濫用,做好錯誤處理。



作者:南國生紅豆
鏈接:https://www.jianshu.com/p/aeb93a3d33d1
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM