Android 經常使用開源框架源碼解析 系列 (五)Butterknife 註解庫

1、前言
做者
JakeWharton 
 
做用
  • 依賴動態注入框架
  • 減小findView/setListener 類初始化代碼,減小工做量
 
2、簡單使用
    一、導入庫 
implementation 'com.jakewharton:butterknife:8.5.1'
implementation 'com.jakewharton:butterknife-compiler:8.5.1'
    二、實例代碼:
@BindView(R.id.TextView_1)
TextView TextView1;
@OnClick(R.id.button_1)
void OnClick(View view) {
    textView1.setText("");
}
 
setContentView(R.layout.butterknife_layout);
//必須在setContentView繪製好佈局以後調用 不然找不到對應的id對象 產生空指針
ButterKnife.bind(this);
 
3、技術歷史
一、早期注入框架技術
     反射機制
在Activity中使用反射機制完成註解庫的注入早期
    缺陷
在Activity runtime運行時大量加載反射注入框架完成依賴注入,會影響App的運行性能,形成UI卡頓,產生更多的臨時對象增長內存損耗
    
    二、現今注入框架技術
    APT
編譯時解析技術,註解注入框架
    區別與舊技術
編譯時生成非運行時生成
 
4、依賴注入框架基礎
    A、註解
 
分類
  •     普通註解
     一、@Override :當前的方法的定義必定要覆蓋其父類的方法
     二、@Deprecated :使用該註解編譯器會出現警告信息
     三、@SuppressWarnings :忽略編譯器提示的警告信息
 
  •     元註解
定義:
    用來註解其餘註解的註解
    一、@ Documented:這個註解應該被Java Document這個工具所記錄
    二、@ Target:代表註解的使用範圍
    三、@ Retention:描述註解的生命週期
    四、@ Inherited:代表註解能夠繼承的,這個註解應該被用於class的子類
 
實例:
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
 
public @interface metaTest {
    public String getTest();
}
 
一、@Documented:代表這個註解應該被Java Document工具所記錄的
 
二、@Target:傳入ElementType. xxx
 
  •     TYPE :表示用來描述類或是接口的 
  •     FIELD : 表示用來描述成員域
  •     METHOD :表示用來描述方法
  •     PARAMETER :表示用來描述參數
  •     CONSTRUCTOR :表示用來描述構造器
  •     LOCAL_VARIABLE :表示用來描述局部變量
  •     PACKAGE :表示用來描述包
 
如下是很是用類型:
 ANNOTATION_TYPE,
TYPE_PARAMETER,
TYPE_USE
 
三、@Retention 描述註解生命週期
 
SOURCE :註解將被編譯器所丟棄,原文件會被保留
CLASS :註解在Class文件中使用,有可能會被JVM本身所拋棄,是編譯時生成綁定代碼的
RUNTIME:註解只在運行時有效
    ps:運行時經過反射獲取註解的內容
 
四、@Inherited:表示該註解能夠繼承
五、@interface :代表這個metaTest 是一個自定義的註解,以及配合上面4個元註解對其進行解釋
  •     自定義註解
@BindView
@Retention( CLASS   //使用該註解代表會在class文件中保留,在runtime時是不存在的
@Target( FIELD )     //該註解用來修飾 域變量
public @interface BindView {
  @IdRes // 
    ps:經過自定義註解完成對變量的註解、對註解的註解
     int value();
}
   
    B、APT工做原理-編譯時
注意⚠️:
  • APT不是經過運行時經過反射機制處理註解的!!!!!!
  • 整個註解處理器是運行在本身的Java虛擬機當中的!
 
Annotation Processor 註解處理器
Javac 工具;編譯時掃描、處理註解;須要註冊註解處理器
 
每個處理器都是 繼承於AbstractProcessor 抽象類 (使用的時候須要繼承並實現其內部的方法)
 
abstract AbstractProcessor :內幾個比較重要的函數
 
  init方法 :會被註解處理工具調用,傳入ProcessingEnvironment 提供不少經常使用的工具類共使用
 
  interface ProcessingEnvironment{
        Elements 工具類 ,掃描的全部java原文件的,element 表明程序中的元素也就說java原代碼
        Types :獲取原代碼中的類型元素信息, Typs 處理Type Element當中一些所想得到的信息
        Filter :建立文件所用
    }
 
abstract process() : 重要級別堪比main 函數,方法的入口,每一個處理器主函數入口!
使用:自定義註解器須要實現其方法
做用: 掃描、評估、處理註解工做,並生成須要的java代碼
 
set<String> getSupportedAnnotationTypes () 
    返回所支持的註解的類型
 
SourceVersion getSupportedSourceVersion()
    用來指定所使用的java版本
 
APT 流程
如何生成 字節碼文件?
 
一、生命 註解的生命週期是 Class
二、繼承AbstractProcessor 類 (編譯時編譯器會掃描須要處理的註解)
三、調用AbstractProcessor  的 Process 方法對註解進行處理,動態生成綁定事件和控件的java代碼
 
*.java  ———input file———> Parse and Enter  ———解析———> Annotation Processing (APT解析工具進行解析,不能加入、刪除java方法)  
                                                                                                                            |
                                                                                                                            |
                                                                  編譯成class文件    ——————  生成java代碼
 
 
C、反射機制
目標
    經過開源的process庫生成 java 代碼
    
    反射
反射機制容許在運行時發現和使用類的信息
 
反射的做用
    一、判斷任意一個對象所屬的類
    二、構造任意一個類的對象
    三、判斷任意一個類所具備的成員變量和方法 (經過反射甚至能夠調用provate方法)
    四、調用任意一個對象的方法
 
反射的缺陷
    一、JVM沒法對反射部分的代碼進行優化,形成性能的損失
    二、反射會形成大量的 臨時對象,進而形成大量的Gc,從而形成卡頓
 
簡單示例:
//定義一個含有Runtime 運行時註解的 註解,經過反射和運行時獲取它的註解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface metaTest {
    int value() default 100;
}
//經過反射獲取到運行時的metaTest的註解
public class ReflactMain {
    @metaTest(10)
    public int age;
 
    public static void main(String[] args) {
        ReflactMain mian = new ReflactMain();
        metaTest testInterface = null;
        try {
            //一、首先獲取到類的Class 類型
            Class clazz = mian.getClass();
             //二、經過class類型獲取到對應的field 對象
            Field field = clazz.getField("age」);
            //三、經過field、method的getAnnotation方法獲取到註解的方法
            testInterface = field.getAnnotation(metaTest.class);
             //四、直接能夠經過註解內定義的方法獲取註解內的值
            System.out.println("==:" + testInterface.value());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}
 
解析:經過反射得到field 這個變量,再經過內部的getAnnotation()方法獲取到註解的方法
 
 
5、ButterKnife的工做原理
 
經過APT註解處理器生成Java代碼以後,再編譯成.class文件
 
    一、編譯的時候掃描註解,並作相應的處理,生成java代碼,生成java代碼時是調用javapoet庫生成的
        ps:同時經過其餘類@Bind/@Click這類註解 在編譯的時候動態生成須要的java文件;生成java文件後,編譯器會對應的生成Class文件
    二、調用ButterKnife.bind( this )的時候,將Id與對應的上下文綁定在一塊兒
        ps:完成findViewById、setOnClickListene 等過程 
 
ButterKnifeProcessor extends AbstractProcessor 
 
幾個輔助的方法 init() 、getSupportedAnnotationTypes()、getSupportedAnnotations()
@Override 
public synchronized void init(ProcessingEnvironment env) {
  •    該方法會在初始化的時候調用一次,用來獲取一些輔助的工具類
  •    經過synchronized關鍵字保證獲取到的對象都是單例的
  •    ProcessingEnvironment 做爲參數能夠提供幾個有用的工具類
 
    //BK process在運行的時候會掃描Java 原文件,每個java原文件的每個獨立的部分就是一個element,而後經過elementUtils對每個element進行解析
elementUtils = env.getElementUtils();
    //用於處理TypeElement 
typeUtils = env.getTypeUtils();
    //建立生成的輔助文件所用
filer = env.getFiler();
}
//返回所支持的註解類型
@Override public Set<String> getSupportedAnnotationTypes() {
  Set<String> types = new LinkedHashSet<>();
  for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
    types.add(annotation.getCanonicalName());
  }
  return types;
}
 
//確認butterknife 定義了哪些註解可使用
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
  Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
  annotations.add(BindArray.class);
  annotations.add(BindBitmap.class);
  annotations.add(BindBool.class);
  annotations.add(BindColor.class);
  annotations.add(BindDimen.class);
  annotations.add(BindDrawable.class);
  annotations.add(BindFloat.class);
  annotations.add(BindInt.class);
  annotations.add(BindString.class);
  annotations.add(BindView.class);
  annotations.add(BindViews.class);
  annotations.addAll(LISTENERS);
  return annotations;
}
 
重要方法:
拿到全部的註解信息存儲到一個Map<TypeElement,BindingSet>集合當中,
而後遍歷map集合作相應的處理,最後生成需求的代碼
 
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);
    try {
      javaFile.writeTo(filer);
    } catch (IOException e) {
      error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
    }
  }
  return false;
}
 
 
findAndParseTargets(): 針對每個自定義好的註解
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
  Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
  Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
  scanForRClasses(env);
    …
// Process each @BindView element.以BindView 爲例子:
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
  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) {
如下內容:拿到註解信息並驗證是不是xx的子類,集中處理註解所須要的內容並保存到一個map集合當中
 
//建立一個原文件所對應的對象的element元素所對應的類型
   TypeElementenclosingElement = (TypeElement) element.getEnclosingElement();
//判斷是否在被註解的屬性上,若是是private 或是static 修飾的註解就會返回一個hasError false值 ,
同時包名是以android 或是java開頭的也會出錯
boolean hasError =  isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
    || isBindingInWrongPackage(BindView.class, element);
        ...
//一些父類的信息用TypeElement獲取不到須要經過TypeMirror 來獲取,並經過asType()驗證是不是需求的子類
TypeMirror elementType = element.asType();
     ...
//判斷裏面的元素是不是view 以及其子類或是是不是接口 ,若是不是view的繼承類沒有意義再往下進行
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
    ...
}
   
//獲取要綁定的View的 Id ,經過getAnnotation(BindView.class).value
int id = element.getAnnotation(BindView.class).value();
//傳入TypeElement的值,根據所在的元素查找Build 
BindingSet.Builder builder = builderMap.get(enclosingElement);
//若是相應的build已經存在了
if (builder != null) {
  String existingBindingName = builder.findExistingBindingName(getId(id));
  if (existingBindingName != null) {
   //若是name 不爲空 ,則說明已經被綁定過了就會報錯並返回
    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 {
   //建立一個新的builder
  builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
    
    //經過addField()方法添加到集合當中
builder.addField(getId(id), new FieldViewBinding(name, type, required));
 
}
 
//將Id 和 View 進行綁定,傳入到一個map集合中
addField————>getOrCreateViewBindings()————>getOrCreateViewBindings():
private ViewBinding.Builder getOrCreateViewBindings(Id id) {
  ViewBinding.Builder viewId = viewIdMap.get(id);
  if (viewId == null) {
    viewId = new ViewBinding.Builder(id);
    viewIdMap.put(id, viewId);
  }
  return viewId;
}
 
//生成java代碼 ,返回一個JavaFile對象用來轉編譯成java代碼
JavaFile brewJava(int sdk) {
  return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
      .addFileComment("Generated code from Butter Knife. Do not modify!")
      .build();
}
// com.squareup.javapoet 第三方庫來生成java代碼 生成所須要的類型
private TypeSpec createType(int sdk) {
   //二、經過這個庫的Builder 內部類方法構建須要的屬性
  TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
      .addModifiers(PUBLIC);
   //三、判斷屬性是不是final類型的,若是是就添加final的修飾符
  if (isFinal) {
    result.addModifiers(FINAL);
  }
    //四、將綁定的集合set 集中到一塊兒,綁定的集合是否爲null
if (parentBinding != null) {
    //五、添加一些父類的信息,好比父類的類名
  result.superclass(parentBinding.bindingClassName);
} else {
    //六、添加父類以上 UNBINDER這個接口做爲屬性添加進去
  result.addSuperinterface(UNBINDER);
}
//七、當前是否已經有了TargetField,Activity的成員變量有沒有被綁定到
if (hasTargetField()) {
    //八、有的話給targetTypeName添加一個private屬性
  result.addField(targetTypeName, "target", PRIVATE);
}
//九、當前控件是不是View或是View的子類 ?或是 Activity ?or Dialog?
    //是的話就添加相應的構造方法
if (isView) {
  result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
  result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
  result.addMethod(createBindingConstructorForDialog());
}
//十、若是本身的構造方法不須要View的參數的話,就必需要添加一個須要View參數的構造方法!!
if (!constructorNeedsView()) {
  result.addMethod(createBindingViewDelegateConstructor());
}
        …
}
//十一、將註解換算成 java
代碼 好比findViewById,將註解生成所須要的Java代碼
createBindingConstructor(sdk)); 
 
ps:
    判斷是否有監聽,若是有就會將View設置成final;
    遍歷ViewBindings把綁定過的View都遍歷一遍,而後調用addViewBinding()最終 生成findViewById()函數
private MethodSpec createBindingConstructor(int sdk) {
     ...
   //十二、是否有方法綁定
if (hasMethodBindings()) {
    //1三、有方法綁定的狀況,添加targetTypeName類型的參數,並設置爲final類型修飾
  constructor.addParameter(targetTypeName, "target", FINAL);
} else {
   //1四、無方法綁定的狀況,依然添加一個targetTypeName類型的參數,區別與有方法綁定的就是不須要final修飾
  constructor.addParameter(targetTypeName, "target");
}
    //1五、有註解的View
if (constructorNeedsView()) {
   //1六、存在已經添加了註解的View的話,就須要給其添加一個View類型的source參數
  constructor.addParameter(VIEW, "source");
} else {
    //1七、不存在已經添加了註解的View的話,就須要給其添加一個View類型的context參數
  constructor.addParameter(CONTEXT, "context");
}
    
    //1八、若是調用了@OnTouch註解的話,須要添加一個SUPPRESS_LINT註解
if (hasOnTouchMethodBindings()) {
  constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
      .addMember("value", "$S", "ClickableViewAccessibility")
      .build());
}
    
    //1九、經過addStatement添加成員變量的方法
if (hasTargetField()) {
  constructor. addStatement("this.target = target");
  constructor. addCode("\n");
}
    
   //20、核心方法
for (ViewBinding binding : viewBindings) {
  addViewBinding(constructor, binding);
}
addViewBinding():
 
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    
//2一、經過該方法優化場景,告訴用戶這裏有View須要綁定 target.l 不能獲取private 修飾的成員變量!!
FieldViewBinding fieldBinding = binding.getFieldBinding();
CodeBlock.Builder builder = CodeBlock.builder()
    .add(" target.$L = ", fieldBinding.getName());
    …
   //2二、這裏就是把findViewById添加到代碼中
    if (!requiresCast && !fieldBinding.isRequired()) {
     builder.add("source.findViewById($L)", binding.getId().code);
    }
}
相關文章
相關標籤/搜索