從ButterKnife看APT相關技術

1、 背景

​ 很早以前,大量書寫findViewById的時候,有人就開始思考,如何將開發者從繁雜的重複的勞動中釋放出來,因而,最初的大神jakewharton編寫了一個ButterKnife框架,使用Java的APT技術,實現這個功能,讓咱們一塊兒來看看這個的實現。java

2、源碼分析

自定義註解

咱們先認識一下ButterKnife的目錄結構,通常來講APT框架只須要三個包就能夠了,一個註解包(java-library),一個api包(項目的入口),一個compiler包(註解使用)。android

咱們如今來講annotation包的相關東西:ButterKnife經常使用的註解包括BindView這個,是綁定findviewbyid的,咱們從這個入手,咱們看下他的形態:api

@Retention(RUNTIME) 
@Target(FIELD) // TYPE: 類或接口; FIELD: 成員變量; METHOD: 方法;
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}
複製代碼

其中註解標準的形式:@interface這個就不用多說了。緩存

一、RetentionPolicy.SOURCE:註解只保留在源文件,當Java文件編譯成class文件的時候,註解被遺棄;bash

二、RetentionPolicy.CLASS:註解被保留到class文件,但jvm加載class文件時候被遺棄,這是默認的生命週期;三、RetentionPolicy.RUNTIME:註解不只被保存到class文件中,jvm加載class文件以後,仍然存在; 這3個生命週期分別對應於:Java源文件(.java文件) ---> .class文件 ---> 內存中的字節碼。明確生命週期長度 SOURCE < CLASS < RUNTIME ,因此前者能做用的地方後者必定也能做用。通常若是須要在運行時去動態獲取註解信息,那隻能用 RUNTIME 註解;若是要在編譯時進行一些預處理操做,好比生成一些輔助代碼,就用 CLASS註解;若是隻是作一些檢查性的操做,好比 @Override**和 **@SuppressWarnings,則可選用 SOURCE 註解。數據結構

這裏以前是class,不知道在哪一個版本就改爲了RunTime,可是咱們只在APT階段使用,效果差很少app

接受類型:框架

  • 全部基本類型(int,float,boolean,byte,double,char,long,short)
  • String
  • Class
  • enum
  • Annotation,註解能夠嵌套

初始化和APT

​ ButterKnife在初始化的時候使用了緩存技術。源碼在ButterKnife這個文件中。SDK入口方法是:jvm

ButterKnife.bind(this);
複製代碼

最後都會進入bind方法中,咱們看下這裏面幹了什麼:ide

public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    if (constructor == null) {
      return Unbinder.EMPTY;
    }
      return constructor.newInstance(target, source);
  }
複製代碼

純看代碼也能看的出來,就是找到class(通常是Activity,在這裏將butterknife與activity綁定),經過這個class的名字找到一個生成的類,最後反射一下建立實例(這是個常規操做,幾乎全部APT的框架都存在這種實現,反射調用經過原className建立的一個文件)。這裏很簡單,咱們直接進findBindingConstructorForClass這裏看這裏作了什麼優化。

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      return bindingCtor;
    }
   bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }
複製代碼

能夠看到裏面作了一個優化,就是加入了BINDINGS,他是一個map,保證只加載一次。這裏也很簡單,就是找到一個命名爲classname+__ViewBinding的文件,並加載他的構造函數,且這個文件是繼承Unbinder,咱們看下Unbinder這個是什麼東西:

public interface Unbinder {
  @UiThread void unbind();
  Unbinder EMPTY = () -> { };
}
複製代碼

這裏面實際上就一個方法,解綁操做,這個就不用說了,確定是爲了優化相關的操做。看到這裏初始化就沒啥了,後面就是APT的部門了,若是咱們也要寫APT這樣的框架,這種流程上的操做仍是十分流水線且簡單的。

APT:咱們只須要繼承AbstractProcessor這個類,並添加@AutoService(Processor.class)這個來實現註解處理器的註冊,註冊到 javac 後,在項目編譯時就能執行註解處理器了,他是SPI的一種實現。這個在後面會引伸一下。

咱們繼承AbstractProcessor,須要關心這幾個實現:

初始化:init():註解處理器運行掃描源文件時,以獲取元素(Element)相關的信息,維度:類、成員變量、方法

解析類:process() 處理解析相關流程

其它:getSupportedXXX(),一些配置:指定 java 版本,指定待解析的註解

咱們須要關心的就是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) { }
    }
    return false;
  }
複製代碼

第一句就是掃描相關的代碼,findAndParseTargets,咱們經過這個方法,找咱們關心的bindview,先將掃描獲得的註解相關信息保存到builderMap,最後對這些信息進行從新整理返回一個以TypeElement爲 key 、BindingSet爲 value 的 Map,其中TypeElement表明使用了 ButterKnife 的類,即 Activity、Fragment等。這個步驟就是爲了生成文件前的準備。用來存儲要生成類的基本信息以及註解元素的相關信息。

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
       try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
複製代碼
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
       // 首先要注意,此時element是VariableElement類型的,即成員變量enclosingElement是當前元素的父類元素,通常就是咱們使用ButteKnife時定義的View類型成員變量所在的類,能夠理解爲以前例子中的MainActivity
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
 // 進行相關校驗,不是重點,此處主要爲一些限定性驗證
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);
    TypeMirror elementType = element.asType();
   .......
 // 得到元素使用BindView註解時設置的屬性值,即 View 對應的xml中的id
    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();
   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) {return; }
    } else {
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }
  }
複製代碼

看起來很繁瑣,可是內部最核心的就是builder.addField(resourceId, new FieldViewBinding(name, type, required));只須要關注id,元素名,類型就行,最後生成的文件也就這幾個主要元素,其餘都是一些校驗,這些校驗踩坑纔多了天然就知道了。這裏面還存在不少AST,JavaC相關的知識,建議這個階段只作瞭解,好比elementToId方法:

private Id elementToId(Element element, Class<? extends Annotation> annotation, int value) {
    JCTree tree = (JCTree) trees.getTree(element, getMirror(element, annotation));
    if (tree != null) {
      rScanner.reset();
      tree.accept(rScanner);
      if (!rScanner.resourceIds.isEmpty()) {
        return rScanner.resourceIds.values().iterator().next();
      }
    }
    return new Id(value);
  }
複製代碼

通常都是直接獲取到id,可是這裏又經過JCTree重新獲取了一遍,理論上值是相同的(這個你們能夠試驗一下)。

好了,看到這裏,你們確定疑惑,好像只處理R.id,對於ButterKnife特有的R2.id是怎麼處理的呢,就在下面

這裏的Id是後面的獲取的核心,

private static final ClassName ANDROID_R = ClassName.get("android", "R");
  private static final String R = "R";
Id(int value, @Nullable Symbol rSymbol) {
    this.value = value;
    if (rSymbol != null) {
    //這裏默認取出R(在這裏就拋棄了R2的概念),classname是包名.R.id這種數據
      ClassName className = ClassName.get(rSymbol.packge().getQualifiedName().toString(), R,
          rSymbol.enclClass().name.toString());
      String resourceName = rSymbol.name.toString();
      //將要使用的核心,code,將code拼成包名.R.id.XXX
      this.code = className.topLevelClassName().equals(ANDROID_R)
        ? CodeBlock.of("$L.$N", className, resourceName)
        : CodeBlock.of("$T.$N", className, resourceName);
      this.qualifed = true;
    } else {
      this.code = CodeBlock.of("$L", value);
      this.qualifed = false;
    }
  }
複製代碼

最後在BindingSet中生成了這個文件相關的東西,能夠看到新建的文件名,這個就是以後建立的文件名:_ViewBinding

static ClassName getBindingClassName(TypeElement typeElement) {
    String packageName = getPackage(typeElement).getQualifiedName().toString();
    String className = typeElement.getQualifiedName().toString().substring(
            packageName.length() + 1).replace('.', '$');
    return ClassName.get(packageName, className + "_ViewBinding");
  }
複製代碼

我看看下findviewbyid

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
    if (binding.isSingleFieldBinding()) {
      FieldViewBinding fieldBinding = requireNonNull(binding.getFieldBinding());
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());
      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
        if (requiresCast) {
          builder.add("($T) ", fieldBinding.getType());
        }
        builder.add("source.findViewById($L)", binding.getId().code);//這裏是核心,用上面的id裏面的code直接替換就變成咱們的想要的樣子,這裏的code就是android.R.color.transparent這種樣式
      } else {
        builder.add("$T.find", UTILS);//構造了butterknife.internal.utils這個類
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          builder.add("AsType");
        }
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }
複製代碼

java代碼生成

​ butterknife生成代碼使用的是JavaPoet,這個不強制,你也能夠用字符串寫。使用Javapoet的好處就是他能夠構造文件系統,生成的java程序是符合語法邏輯的。僅此而已。

JavaFile brewJava(int sdk, boolean debuggable) {
    TypeSpec bindingConfiguration = createType(sdk, debuggable);//代碼生成相關
    return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }
複製代碼

這東西沒啥好說的,就是javapoet的使用。這種代碼看起來其實沒啥意思,就是javapoet的使用,咱們能夠直接看代碼最後的生成,反推javapoet的寫法,

public DrawerToggleActivity_ViewBinding(DrawerToggleActivity target, View source) {
    this.target = target;
    target.textview = Utils.findRequiredViewAsType(source, R.id.drawer_layout, "field 'drawerLayout'", DrawerLayout.class);
    Context context = source.getContext();
    target.guideColor = ContextCompat.getColor(context, R.color.half_transparent);
  }
複製代碼

代碼生成的比較統一。

3、源碼拓展

AutoService是google提供的框架,其源碼比較簡單:

public class AutoServiceProcessor extends AbstractProcessor //能夠看到autoservice也是一個APT,就是套娃 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {
      return processImpl(annotations, roundEnv);
    } catch (Exception e) {
      return true;}
    }
 private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (roundEnv.processingOver()) {
      generateConfigFiles();
    } else {
      processAnnotations(annotations, roundEnv);
    }
    return true;
  }
  
private void generateConfigFiles() {//就是一個建立文件的過程
    Filer filer = processingEnv.getFiler();
    for (String providerInterface : providers.keySet()) {
      String resourceFile = "META-INF/services/" + providerInterface;    
      try {
        SortedSet<String> allServices = Sets.newTreeSet();
        try {     
          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
              resourceFile);        
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());     
          allServices.addAll(oldServices);
        } catch (IOException e) {       
          log("Resource file did not already exist.");
        }   
        allServices.addAll(newServices);
        log("New service file contents: " + allServices);
        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
            resourceFile);
        OutputStream out = fileObject.openOutputStream();
        ServicesFiles.writeServiceFile(allServices, out);
        out.close();       
      } catch (IOException e) {      
        return;
      }
    }
  }
複製代碼

在官方文檔上介紹的使用@AutoService()修飾後,會生成META-INF/services/javax.annotation.processing.Processor文件夾生成文件,這種實現就是JAVA原生的ServiceLoaderSPI的是一種實現,所謂SPI,即Service Provider Interface,用於一些服務提供給第三方實現或者擴展,能夠加強框架的擴展或者替換一些組件,他能夠自動加載這個類了

4、源碼延伸

咱們學習了butterknife的實現,那咱們有兩個問題:

1.若是實現相同的功能,咱們還能怎麼作

2.若是學習了相似的寫法,咱們能寫出來什麼樣的功能。

相關功能的實現(其餘方式構建findViewById)

1.kotlin的實現

kotlin是使用kotlin-android-extensions這個技術實現的,

使用:

import kotlinx.android.synthetic.main.xml文件的名稱.*
複製代碼

咱們經過反編譯能夠其實咱們的源碼又多了一些東西:

private HashMap _$_findViewCache;
   protected void onCreate(@Nullable Bundle savedInstanceState) {
      ...
      ((TextView)this._$_findCachedViewById(id.helloTv)).setText((CharSequence)"Hello Kotlin!");
   }
   public View _$_findCachedViewById(int var1) {
      if(this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }
      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if(var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }
      return var2;
   }
   public void _$_clearFindViewByIdCache() {
      if(this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }
   }
複製代碼

這個只能說在編譯期層面使用插件(kotlin-android-extensions)上都作了相關的操做,最終的代碼其實不是咱們看到的那樣(具體實現沒有看過,插件代碼太多了)。

2.AST的實現

AST是抽象語法樹,計算機內存的一種樹狀數據結構,便於計算機理解和閱讀。咱們能夠經過修改抽象語法樹對源碼的結構作增刪改查,這個技術普遍應用於Lint上,這個的實現有興趣能夠研究一下,源碼參考Lint相關實現。

3.ASM的實現

和上面的AST的機制差很少,他構建的是class的結構,ASM操做起來比較麻煩,須要考慮的東西也不少,這個的實現有興趣能夠研究一下,源碼參考ByteX.

相似思想的實現(使用APT,咱們能作什麼)

Android中有個LocalBroadcast的東西,也就是本地廣播,咱們怎麼經過利用本地廣播來構形成EventBus的外表。咱們能夠利用APT的實現。

首先咱們先構造初始化的方法,學習Butterknife同樣,

private static Unbinder createBroadCast(@NonNull Object activity) {
        Class<?> targetClass = activity.getClass();
        if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
        Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
        if (constructor == null) {
            return Unbinder.EMPTY;
        }
        try {
            return constructor.newInstance(activity);
        } 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);
        }
    }
    @Nullable
    @CheckResult
    @UiThread
    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;
        }
        try {
            Class<?> bindingClass = cls.getClassLoader().loadClass(clsName +BROCASE_SUFFIX);         
            bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls);
            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;
    }
複製代碼

能夠看到代碼差很少,核心就是APT的process方法裏面的東西了,

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mProxyMap.clear();
        Set<? extends Element> elesWithBind = roundEnv.getElementsAnnotatedWith(LocalBind.class);
        for (Element element : elesWithBind) {
            checkAnnotationValid(element, LocalBind.class);
            ExecutableElement variableElement = (ExecutableElement) element;
            //class type
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //full class name
            String fqClassName = classElement.getQualifiedName().toString();
            LocalProxyInfo proxyInfo = mProxyMap.get(fqClassName);
            if (proxyInfo == null) {
                proxyInfo = new LocalProxyInfo(elementUtils, classElement);//構造生成類
                mProxyMap.put(fqClassName, proxyInfo);
            }
            LocalBind bindAnnotation = variableElement.getAnnotation(LocalBind.class);
            proxyInfo.setValus(bindAnnotation.value());
            proxyInfo.injectVariables.put(fqClassName, variableElement);
        }
        for (String key : mProxyMap.keySet()) {
            LocalProxyInfo proxyInfo = mProxyMap.get(key);
            try {
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
                        proxyInfo.getProxyClassFullName(),
                        proxyInfo.getTypeElement());
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode());//寫入文件
                writer.flush();
                writer.close();
            } catch (IOException e) {
                error(proxyInfo.getTypeElement(),
                        "Unable to write injector for type %s: %s",
                        proxyInfo.getTypeElement(), e.getMessage());
            }

        }
        return true;
    }
複製代碼

能夠看到,很簡單的代碼,先根據類名,參數等構造出文件生成器LocalProxyInfo:

public LocalProxyInfo(Elements elementUtils, TypeElement classElement) {
        this.typeElement = classElement;
        PackageElement packageElement = elementUtils.getPackageOf(classElement);
        String packageName = packageElement.getQualifiedName().toString();
        //classname
        String className = ClassValidator.getClassName(classElement, packageName);
        this.packageName = packageName;
        this.proxyClassName = className + PROXY;
    }
複製代碼

而proxyInfo.generateJavaCode()則是執行寫入文件的入口了

public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code. Do not modify!\n");
        builder.append("package ").append(packageName).append(";\n\n");
        builder.append("import com.longshihan.broca_api.*;\n");
        builder.append("import android.content.IntentFilter;\n");
        builder.append("import android.support.v4.content.LocalBroadcastManager;\n");
        builder.append("import android.content.BroadcastReceiver;\n");
        builder.append("import android.content.Context;\n");
        builder.append("import android.content.Intent;\n");
        builder.append("import android.support.annotation.UiThread;\n");
        builder.append("import ").append(typeElement.getQualifiedName()).append(" ;\n");
        builder.append('\n');
        builder.append("public class ").append(proxyClassName).append(" implements Unbinder");
        builder.append(" {\n");
        builder.append("private LocalReceiver localReceiver;\n");
        builder.append("private LocalBroadcastManager localBroadcastManager;\n");
        builder.append(" private IntentFilter intentFilter;\n");
        builder.append(" private ").append(typeElement.getQualifiedName()).append(" host;\n");
        generateCMethods(builder);
        generateMethods(builder);
        generateDMethods(builder);
        builder.append('\n');
        builder.append("}\n");
        return builder.toString();
    }
複製代碼

我這裏代碼比較簡單,直接用string生成代碼就好了。固然,其餘方式也行。

到這裏一個功能就實現了,如今看下效果,猜也能猜出大概。

源文件MainActivity:

@LocalBind({"123", "345","4586"})
    public void getBroadcastService(Intent intent) {
        Log.d("打印Action", intent.getAction());
        switch (intent.getAction()){
            case "123":
                break;
            case "345":
                break;
            case "4586":
                break;

        }
    }
複製代碼

生成的文件(build/generated/source/apt/debug/包名/MainActivity$BroadcastInject):

public class MainActivity$BroadcastInject implements Unbinder {
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
 private IntentFilter intentFilter;
 private com.longshihan.aopbrocad.MainActivity  host;
@UiThread
 public  MainActivity$BroadcastInject(com.longshihan.aopbrocad.MainActivity host) {
     localBroadcastManager = LocalBroadcastManager.getInstance(host);
     intentFilter = new IntentFilter();
    this.host=host;
    intentFilter.addAction("123");
    intentFilter.addAction("345");
    intentFilter.addAction("4586");
    localReceiver = new LocalReceiver();
     localBroadcastManager.registerReceiver(localReceiver, intentFilter);
  }
public class LocalReceiver extends BroadcastReceiver{
 @Override
    public void onReceive(Context context, Intent intent) {
       host.getBroadcastService(intent);
      }
}
 @Override
public void unbind(){
 localBroadcastManager.unregisterReceiver(localReceiver);
}
}
複製代碼

OK ,寫到這裏你們對於APT的使用也瞭然於心了吧,對於相關應用也有不少發散思惟之處,好比Ali的ARouter,Dagger等等,這個能夠交到你們以後學習了。

相關文章
相關標籤/搜索