【天星技術團隊】從自定義Annotation、APT、JavaPoet再到Butterknife原理

天星技術團隊發表於2018-06-30

作者: 張德帥, 時間: 2018.6.30 星期六 天氣晴


一眨眼功夫又到週末,總覺得小日子過得不夠充實(其實是王者榮耀不好玩了...)。

【天星技術團隊】從自定義Annotation、APT、JavaPoet再到Butterknife原理

不過我可以跟Butterknife談情說愛(RTFSC:Read The Fucking Source Code),本篇文章便是“愛的結晶” 。

1、Butterknife是什麼?

【天星技術團隊】從自定義Annotation、APT、JavaPoet再到Butterknife原理

Android大神JakeWharton的作品(其它還有OKHttp、Retrofit等)。Butterknife使用註解代替findViewById()等,可讀性高、優雅、開發效率提高等。這裡就不再列舉Butterknife優點,相信各位老司機早就精通Butterknife使用。本篇主要學習Butterknife核心原始碼,萬丈高樓平地起,直接殺入原始碼難免無頭蒼蠅,一臉懵逼,文章從以下幾個部分聊起(瞎扯)。

  1. 自定義註解
  2. 反射解析註解
  3. APT解析註解
  4. JavaPoet生成Java原始檔
  5. 淺析Butterknife原始碼

(強行插入表情包)

image
來不及解釋了 快上車 滴... 學生卡。

Butterknife 基本使用姿勢

1、在App build.gradle配置依賴

dependencies {
    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}
複製程式碼

2、在Activity使用

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_name)
    TextView tvName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }
}

複製程式碼

2、註解

上面看到使用非常簡單,幾個註解就搞定之前一大堆findViewById()等程式碼,那麼它是怎麼做到的?答案是:butterknife會掃描這些自定義註解,根據註解資訊生成Java檔案,ButterKnife.bind(this)實際會執行自動生成的程式碼。紅色框選部分文章後面會詳細分析,可以瞭解到 在讀Butterknife原始碼之前得先回顧Java基礎-註解。

目錄:app\build\generated\source\apt\ (APT掃描解析 註解 生成的程式碼)。

image

註解

註解可以理解為程式碼的標識,不會對執行有直接影響。

1、內建註解

Java內建幾個常用註解,這部分標識原始碼,會被編譯器識別,提示錯誤等。

@Override 標記覆蓋方法

@Deprecated 標記為過時

@SuppressWarnings 忽略警告

2、元註解

假設我們要自定義一個註解,這時候就需要用元註解去宣告自定義註解,包括像註解作用域、生命週期等。

@Documented 可以被javadoc文件化。

@Target 註解用在哪

  1. CONSTRUCTOR 建構函式
  2. FIELD 域宣告
  3. LOCAL_VARIABLE 區域性變數
  4. METHOD 方法
  5. PACKAGE 包宣告
  6. PARAMETER 引數
  7. TYPE 類、介面

@Inherited

  1. 允許子類繼承父類註解

@Retention

  1. SOURCE:編譯時剔除
  2. CLASS:在CLASS檔案保留標識,執行時剔除
  3. RUNTIME 執行時保留標識

3、自定義註解

使用元註解建立自定義註解

//直到執行時還保留註解
@Retention(RetentionPolicy.RUNTIME)
//作用域 類
@Target(ElementType.TYPE)
public @interface BindV {
    int resId() default 0;
}

複製程式碼

4、解析註解

在程式碼中定義了標識,現在想拿到這些資訊,可以通過反射、APT獲取註解的值。

1、反射

通過反射解析註解(這裡也不闡述什麼是反射)

BindV.java 檔案

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BindV {
    int resId() default 0;
}
複製程式碼

MainActivity.java 檔案

@BindV(resId = R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        
        //這裡是舉個栗子反射當前類,設定佈局檔案,當然在實際中這部分程式碼可能是經過封裝的。
        Class clz = MainActivity.class;
        BindV bindV = (BindV) clz.getAnnotation(BindV.class);//拿到佈局檔案id
        setContentView(bindV.resId());
    }
}
複製程式碼

2、APT

APT(Annotation Processing Tool)註解處理器,是在編譯期間讀取註解,生成Java檔案。反射解析註解是損耗效能的,接下來通過APT來生成java原始碼,從而避免反射。

1、首先新建專案,再新建JavaLibrary。

【天星技術團隊】從自定義Annotation、APT、JavaPoet再到Butterknife原理

JavaLibrary的Gradle配置,要想Java識別註解處理器,需要註冊到META-INF,這裡使用auto-service這個庫實現自動註冊。

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

複製程式碼

2、新建BindLayout註解

package com.example.abstractprocessorlib;

public @interface BindLayout {
    int viewId();
}

複製程式碼

3、新建AbstractProcessor子類

package com.example.abstractprocessorlib;

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    private Types typesUtils;//型別工具類
    private Elements elementsUtils;//節點工具類
    private Filer filerUtils;//檔案工具類
    private Messager messager;//處理器訊息輸出(注意它不是Log工具)

    //init初始化方法,processingEnvironment會提供很多工具類,這裡獲取Types、Elements、Filer、Message常用工具類。
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        typesUtils = processingEnvironment.getTypeUtils();
        elementsUtils = processingEnvironment.getElementUtils();
        filerUtils = processingEnvironment.getFiler();
        messager = processingEnvironment.getMessager();
    }

    //這裡掃描、處理註解,生成Java檔案。
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //拿到所有被BindLayout註解的節點
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindLayout.class);
        for (Element element : elements) {
            //輸出警告資訊
            processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "element name:" + element.getSimpleName(), element);

            //判斷是否 用在類上
            if (element.getKind().isClass()) {
                //新檔名  類名_Bind.java
                String className = element.getSimpleName() + "_Bind";
                try {
                    //拿到註解值
                    int viewId = element.getAnnotation(BindLayout.class).viewId();

                    //建立檔案 包名com.example.processor.    
                    JavaFileObject source = filerUtils.createSourceFile("com.example.processor." + className);
                    Writer writer = source.openWriter();
                    
                    //檔案內容
                    writer.write("package com.example.processor;\n" +
                            "\n" +
                            "import android.app.Activity;\n" +
                            "\n" +
                            "public class " + className + " {  \n" +
                            "\n" +
                            "    public static void init(Activity activity){\n" +
                            "        activity.setContentView(" + viewId + ");\n" +
                            "    }\n" +
                            "}");
                    writer.flush();
                    //完成寫入
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }

    //要掃描哪些註解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationSet = new HashSet<>();
        annotationSet.add(BindLayout.class.getCanonicalName());
        return annotationSet;
    }

    //支援的JDK版本,建議使用latestSupported()
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

複製程式碼

2、在App的Gradle檔案 新增配置

dependencies {
    annotationProcessor project(path: ':AbstractProcessorLib')
    implementation project(path: ':AbstractProcessorLib') 
}
複製程式碼

3、在app\build\generated\source\apt\debug 目錄下,可以看到APT 生成的檔案。

【天星技術團隊】從自定義Annotation、APT、JavaPoet再到Butterknife原理

可以在Activity使用剛才生成的檔案

【天星技術團隊】從自定義Annotation、APT、JavaPoet再到Butterknife原理

假設遇到這個錯誤,可以參考修改Grandle配置

image

Gradle配置

image

1、JavaPoet

從上面APT可以看到拼接Java檔案是比較複雜的,好在Square開源了JavPoet這個庫,不然整個檔案全靠字串拼接... 有了這個庫生成程式碼,就像寫詩一樣。修改剛才process()方法

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindLayout.class);
    for (Element element : elements) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "element name:" +element.getSimpleName(), element);
        if (element.getKind().isClass()) {
            String className = element.getSimpleName() + "_Bind";
            try {
                int viewId = element.getAnnotation(BindLayout.class).viewId();
                
                //得到android.app.Activity這個類
                ClassName activityClass = ClassName.get("android.app", "Activity");
                
                //建立一個方法
                MethodSpec initMethod = MethodSpec.methodBuilder("init")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//修飾符
                        .addParameter(activityClass, "activity")//引數
                        .returns(TypeName.VOID)//返回值
                        .addStatement("activity.setContentView(" + viewId + ");")//方法體
                        .build();
                
                //建立一個類        
                TypeSpec typeSpec = TypeSpec.classBuilder(className)//類名 
                        .addModifiers(Modifier.PUBLIC)//修飾符
                        .addMethod(initMethod)//將方法加入到這個類
                        .build();
                        
                //建立java檔案,指定包名類        
                JavaFile javaFile = JavaFile.builder("com.example.processor", typeSpec)
                        .build();
                        
                javaFile.writeTo(filerUtils);
                
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    return false;
}
複製程式碼

Butterknife 原始碼分析

先到gayhub 下載原始碼,butterknife、butterknife-annotations、butterknife-compiler三個核心模組,也是主要閱讀的部分。

【天星技術團隊】從自定義Annotation、APT、JavaPoet再到Butterknife原理

1、首先來分析一下build\generated\source\apt**\MainActivity_ViewBinding.java 這個檔案生成大概過程,之前我們是用註解處理器生成程式碼,在butterknife-compiler模組的ButterKnifeProcessor.java類負責生成 ClassName_ViewBinding.java檔案。 過程如下:

  • 掃描註解資訊
  • 生成ClassName_ViewBinding.java檔案
  • Butterknife.init()方法找到對於ViewBinding檔案並執行繫結方法。

(為了方便快速閱讀程式碼,把註釋或程式碼強壯性判斷移除)首先是init方法,沒有過多複雜的,主要是工具類獲取。

 @Override 
 public synchronized void init(ProcessingEnvironment env) {
   super.init(env);
   elementUtils = env.getElementUtils();
   typeUtils = env.getTypeUtils();
   filer = env.getFiler();
   trees = Trees.instance(processingEnv);
 }
複製程式碼

getSupportedSourceVersion、getSupportedAnnotationTypes方法。

@Override 
public SourceVersion getSupportedSourceVersion() {
  return SourceVersion.latestSupported();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
  Set<String> types = new LinkedHashSet<>();
  for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
    types.add(annotation.getCanonicalName());
  }
  return types;
}

private Set<Class<? extends Annotation>> getSupportedAnnotations() {
  Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
  annotations.add(BindAnim.class);
  annotations.add(BindArray.class);
  ...
  return annotations;
}
複製程式碼

接下來重點是process方法


@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
  Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
  for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
    TypeElement typeElement = entry.getKey();
    BindingSet binding = entry.getValue();
    JavaFile javaFile = binding.brewJava(sdk, debuggable);
    try {
      javaFile.writeTo(filer);
    } catch (IOException e) {
      error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
    }
  }
  return false;
}
複製程式碼

分析findAndParseTargets(env)這行,找到這個private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env)這個方法核心部分

省略...
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
  // we don't SuperficialValidation.validateElement(element)
  // so that an unresolved View type can be generated by later processing rounds
  try {
    parseBindView(element, builderMap, erasedTargetNames);
  } catch (Exception e) {
    logParsingError(element, BindView.class, e);
  }
}
省略...
複製程式碼

繼續往下找 parseBindView()方法

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
    Set<TypeElement> erasedTargetNames) {
    
  //當前註解所在的類
  TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

  //校驗類、要繫結的欄位 修飾符 private static,不可以在Framework層使用已java. android.開始的包名  
  boolean hasError = isInaccessibleViaGeneratedCode(BindViews.class, "fields", element)
      || isBindingInWrongPackage(BindViews.class, element);
      
  // 要繫結的欄位是否View的子類、或介面
  TypeMirror elementType = element.asType();
  if (elementType.getKind() == TypeKind.TYPEVAR) {
    TypeVariable typeVariable = (TypeVariable) elementType;
    elementType = typeVariable.getUpperBound();
  }
  Name qualifiedName = enclosingElement.getQualifiedName();
  Name simpleName = element.getSimpleName();
  if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
    if (elementType.getKind() == TypeKind.ERROR) {
      note(element, "@%s field with unresolved type (%s) "
              + "must elsewhere be generated as a View or interface. (%s.%s)",
          BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
    } else {
      error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
          BindView.class.getSimpleName(), qualifiedName, simpleName);
      hasError = true;
    }
  }
  
  //校驗不通過
  if (hasError) {
    return;
  }

  // Assemble information on the field.
  //儲存View ID之間對映
  int id = element.getAnnotation(BindView.class).value();
  BindingSet.Builder builder = builderMap.get(enclosingElement);//從快取取出來
  Id resourceId = elementToId(element, BindView.class, id);
  if (builder != null) {//說明之前被繫結過
    String existingBindingName = builder.findExistingBindingName(resourceId);
    if (existingBindingName != null) {
      error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
          BindView.class.getSimpleName(), id, existingBindingName,
          enclosingElement.getQualifiedName(), element.getSimpleName());
      return;
    }
  } else {
    //構建要繫結View的對映關係,繼續看getOrCreateBindingBuilder方法資訊
    builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
  }

  String name = simpleName.toString();
  TypeName type = TypeName.get(elementType);
  boolean required = isFieldRequired(element);

  //我們Activity類有很多 BindView註解的控制元件,是以類包含欄位,這裡的builder相當於類,判斷如果類存在就往裡加欄位。
  builder.addField(resourceId, new FieldViewBinding(name, type, required));

  // Add the type-erased version to the valid binding targets set.
  erasedTargetNames.add(enclosingElement);
}



//直接看newBuilder
private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
      builder = BindingSet.newBuilder(enclosingElement);
      builderMap.put(enclosingElement, builder);
    }
    return builder;
  }
 
//相當於建立一個類,類名是一開始看到的 類名_ViewBinding 
static Builder newBuilder(TypeElement enclosingElement) {
  TypeMirror typeMirror = enclosingElement.asType();
  
  boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
  boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
  boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);

  TypeName targetType = TypeName.get(typeMirror);
  if (targetType instanceof ParameterizedTypeName) {
    targetType = ((ParameterizedTypeName) targetType).rawType;
  }
  
  String packageName = getPackage(enclosingElement).getQualifiedName().toString();
  String className = enclosingElement.getQualifiedName().toString().substring(
      packageName.length() + 1).replace('.', '$');
  ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
  boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
  
  return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}  



回到process方法,這行就是將剛掃描的資訊寫入到檔案
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);


複製程式碼

至此我們知道*_ViewBinding檔案生成過程,接下來看怎麼使用,從Butterknife.bind()方法檢視

public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();//拿到decorView
    return createBinding(target, sourceView);
}
複製程式碼

進入createBinding()方法

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName
    //拿到類_ViewBinding的構造方法
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);//執行構造方法。
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.", cause);
    }
  }
複製程式碼

繼續看findBindingConstructorForClass()方法,根據當前類先從快取找構造方法,沒有的話根據類名_ViewBinding找到構造方法。

 private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }
複製程式碼

//到這一步我們清楚,是先生成檔案,然後Butterknife.bind()方法關聯生成的檔案並執行構造。接下來看看生成的檔案構造做了什麼 //我在apt目錄下找到一個檔案SimpleActivity_ViewBinding.java檔案,程式碼量多 我簡化一下,直接看findRequireViewAsType()方法。

 @UiThread
  public SimpleActivity_ViewBinding(SimpleActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public SimpleActivity_ViewBinding(final SimpleActivity target, View source) {
    this.target = target;

    View view;
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
  }
  
  //繼續看findRequiredView
  public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
  }
  
  
  //findRequiredView()方法,可以看到其實還是使用findViewByID()查詢View只不過是自動生成。
  public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }
    String name = getResourceEntryName(source, id);
    throw new IllegalStateException("Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
        + " (methods) annotation.");
  }
複製程式碼

//以上就是Butterknife基本原理,第一部分apt掃描註解資訊生成檔案,第二部分Butterknife.bind方法找到對於檔案,執行構造,並執行findView獲取view。

推薦一篇文章:Android主專案和Module中R類的區別 https://www.imooc.com/article/23756

最後:其實在MVVM、Kotlin等出來之後,Butterknife熱度慢慢下降,但這根本不影響我們去了解 這如此優秀的框架。

炮友們有問題,請留言 感謝! 這是粉絲QQ群

【天星技術團隊】從自定義Annotation、APT、JavaPoet再到Butterknife原理

關於作者 Android-張德帥
  • 1996年,就讀於德國慕尼黑特種兵學校。
  • 1998年,在美國賓夕法尼亞大學心理系進修。
  • 2000年,加入海波突擊隊。
  • 2003年,攻破日本情報系統,獲取10份絕密檔案,令其戰爭陰謀破產。
  • 2005年,前往敘利亞執行任務,成功解救三千人質。
  • 2006年,獲得諾貝爾和平獎提名。
  • 2008年,參加美國總統選舉,以一票只差落選。
  • 2011年,被奧巴馬跪請回到海波突擊隊,同年擊斃拉登。
  • 2015年,被提名為全球最有影響力人物。
  • 2018年,放棄一生榮譽加入"天星技術團隊",持續更新文章。

參考資料

  1. Butterknife: https://github.com/JakeWharton/butterknife
  2. 《Java程式設計思想 第四版》: https://item.jd.com/10058164.html
  3. Auto-Service: https://github.com/google/auto-service (上面什麼都沒有,奇怪) 'com.google.auto.service:auto-service:1.0-rc2'
  4. Java註解處理器:https://race604.com/annotation-processing/
  5. JavaPoet 看這一篇就夠了:https://juejin.im/entry/58fefebf8d6d810058a610de/

相關文章