1. 程式人生 > >註解(Annotation)自定義註解(四)--編譯期註解解析講解和手寫ButterKnife

註解(Annotation)自定義註解(四)--編譯期註解解析講解和手寫ButterKnife

註解(Annotation)自定義註解(四)–編譯期註解解析講解和手寫ButterKnife

前言

前面兩篇講解了執行期註解的使用和xutils原始碼的解析,以及手動打造自己的IOC框架。但是執行期註解由於效能問題被一些人所詬病,所以這裡我們講下編譯器註解的使用和實戰。

介紹

編譯器註解的核心原理依賴APT(Annotation Processing Tolls)實現,例如,我們常用的ButterKnife,Dagger,Retrofit等開源庫都是基於APT的。那麼編譯器註解是如何工作的呢?這就是我們要討論的內容。

編譯時Annotation註解的基本原理是,在某些程式碼元素上(比如型別,函式,欄位)添加註解,然後在編譯時編譯器會檢查AbstractProcessor的子類,並且呼叫該型別的process函式,然後將添加了註解的元素傳遞給process中,使得開發者可以在編譯期可以進行處理,比如,根據註解生成新的ava類,這也是BufferKnife等庫的基本原理。

這裡寫圖片描述

編譯處理時,是分開進行的。如果在某個處理中產生的新的JAVA檔案,那麼就需要另外一個處理來處理新生成的原始檔,反覆進行,知道沒有新的檔案產生。在處理完檔案後,再進行編譯。JDK5提供了APT工具來對註解進行處理。編寫註解處理器和核心是:AnnotationProcessorFactory和AnnotationProcessor兩個介面,前者是為某些註解型別建立註解處理器工廠,後者是表示註解處理器。

對於編譯器,程式碼中元素的結構是不會變的。JDK中為這些元素定義了一個基本型別Element類,它有幾個子類:

PackageElement--包元素,包含包的資訊
TypeElement-------型別元素,描述欄位資訊
ExecutableElement--可執行元素,代表函式型別的元素
VariableElement------變數元素
TypeParameterElement--型別引數元素

使用上面的抽象來對於程式碼中的基本元素。

我們先來分析BuffKnife使用以及原始碼,然後在手動打造我們自己的框架。
BuffKnife使用

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.icon)
    ImageView icon;

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

    @OnClick(R.id.icon)
    public void onClick() {
        Toast.makeText(this, "圖片點選了", Toast.LENGTH_LONG).show();
    }
}

使用非常簡單,主要注意:屬性是不能private就行。

butterknife的原始碼解析

按照邏輯,我們應該先看ButterKnife.bind(this),下面是原始碼:

static void bind(Object target, Object source, Finder finder) {
    Class<?> targetClass = target.getClass();
    try {
      if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
      // 找到viewBinder
      ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
      if (viewBinder != null) {
        // 直接執行方法
        viewBinder.bind(finder, target, source);
      }
    } catch (Exception e) {
      throw new RuntimeException("Unable to bind views for " + 
          targetClass.getName(), e);
    }
}

如果從這裡看我們好像看不到任何的東西,其實工作流程是怎樣的呢?從上面介紹我們知道,編譯器會先呼叫AbstractProcessor的子類的process函式,我們可以看一下Bind這個Annotation註解類和ButterKnifeProcessor這兩個類:

ButterKnife 工作流程

@Retention(CLASS) @Target(FIELD)
public @interface Bind {
  /** View ID to which the field will be bound. */
  int[] value();
}

當你編譯你的Android工程時,ButterKnife工程中ButterKnifeProcessor類的process()方法會執行以下操作:

1.開始它會掃描Java程式碼中所有的ButterKnife註解@Bind、@OnClick、@OnItemClicked等。
2.當它發現一個類中含有任何一個註解時,ButterKnifeProcessor會幫你生成一個Java類,名字類似$$ViewBinder,這個新生成的類實現了ViewBinder介面。
3.這個ViewBinder類中包含了所有對應的程式碼,比如@Bind註解對應findViewById(), @OnClick對應了view.setOnClickListener()等等。
4.最後當Activity啟動ButterKnife.bind(this)執行時,ButterKnife會去載入對應的ViewBinder類呼叫它們的bind()方法。

現在我們總該明白為什麼我們的生成的屬性和方法不能私有了吧?我們最後看一下編譯時生成的class類吧

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131427372, "field 'icon' and method 'onClick'");
    target.icon = finder.castView(view, 2131427372, "field 'icon'");
    view.setOnClickListener(
      new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(View p0) {
           target.onClick();
        }
      });
  }

  @Override public void unbind(T target) {
    target.icon = null;
  }
}

這裡沒有詳細分析ButterKnifeProcessor類,有興趣的同學可以自己看下。

手寫BuffKnife

必備知識點:

- 基本的註解知識
- 反射
- 編譯期註釋處理API
- JavaPoet(用來生成java原始檔,不是必須,可自己手動拼接)
- Auto (自動配置META-INF,不是必須,可以手動配置)

上面後面兩個工具不是必須的,如果感興趣,可以自己手動拼接java原始檔和手動配置Auto。

這裡寫圖片描述

基本的配置:

這邊要注意幾點:

aptlib和annotationlib是java庫,不用android庫是因為androidSDK中沒有javax.annotation.*
android程式呼叫java庫時,需要在java庫中引入com.google.android:android:XXXX和com.android.support:support-annotations:XXXXjar包

想使用APT,專案要應用APT外掛:

dependencies {
    classpath 'com.android.tools.build:gradle:2.3.1'

    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}



apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

然後引入javajar包:

apt  project(':aptlib')
compile project(':annotationlib')

知道上面的知識,就可以開始寫我們的程式碼了。

讓我們先來看看註解類吧

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface ViewInjector {
    int value();
}

//自動配置META-INF

@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor {

//在元素上進行操作的某些實用工具方法
private Elements elementUtils;

//用來建立新源、類或輔助檔案的 Filer
private Filer filer;
//用來在型別上進行操作的某些實用工具
private Types typeUtils;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    elementUtils = processingEnv.getElementUtils();
    filer = processingEnv.getFiler();
    typeUtils = processingEnv.getTypeUtils();
}

//返回支援的註解型別
@Override
public Set<String> getSupportedAnnotationTypes() {
    return Collections.singleton(ViewInjector.class.getCanonicalName());
}
//型別與欄位的關聯表,用於在寫入Java檔案時按型別來寫不同的檔案和欄位
final Map<String, List<VariableElement>> map = new HashMap<String, List<VariableElement>>();

//註解處理的核心方法
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //獲取使用ViewInjector註解的所有元素 
    Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(ViewInjector.class);
    for (Element element:elementSet) {

        checkAnnotationValid(element,ViewInjector.class);
        //註解的欄位 
       VariableElement varElement= (VariableElement)element;
        //型別的完整路徑名,比如某個Activity的完整路徑
       String className = getParentClassName(varElement);
        //獲取這個型別的所有註解,例如某個Activity中的所有View的註解物件 
        List<VariableElement> cacheElements = map.get(className);
        if(cacheElements==null){
            cacheElements = new LinkedList<VariableElement>();
        }
        //將元素新增到該型別對應的欄位列表中
        cacheElements.add(varElement);
        //以類的路徑為key,欄位列表為value,存入map.這裡是將所在欄位按所屬的型別進行分
       map.put(className,cacheElements);
    }

    //生成java原始檔
    generate();

    return false;
}



 //生成java原始檔

private void generate() { 
         Iterator<Map.Entry<String, List<VariableElement>>> iterator = map.entrySet().iterator(); 
         while (iterator.hasNext()){
             Map.Entry<String, List<VariableElement>> entry = iterator.next(); 
              List<VariableElement> cacheElements = entry.getValue(); 
              if (cacheElements == null || cacheElements.size() == 0) {
                      continue; 
                    }
             InjectorInfo info = InjectorInfoUtil.createInjectorInfo(processingEnv,cacheElements.get(0));
          //下面全是JavaPoet的基本使用,不明白的可以點選上面的連結去看 
          final ClassName className = ClassName.get(info.packageName,info.classlName); 
          final ClassName InterfaceName=ClassName.get(InjectAdapter.class); 

            MethodSpec.Builder injectsBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .returns(void.class)
                .addParameter(className, "target");
            for (VariableElement element:cacheElements) {

                InjectorInfo temInfo = InjectorInfoUtil.createInjectorInfo(processingEnv,element);
                ViewInjector annotation = element.getAnnotation(ViewInjector.class);
                int value = annotation.value();
                String fieldName = element.getSimpleName().toString();
                String type = element.asType().toString();

                injectsBuilder.addStatement("target." + fieldName + " = ("+type+")(target).findViewById(" + value + ")");

             }
            MethodSpec injects = injectsBuilder.build();

            TypeSpec typeSpec = TypeSpec.classBuilder(info.newClassName)
                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, className))
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(injects)
                    .build();


           JavaFile javaFile = JavaFile.builder(info.packageName, typeSpec).build();
           try { 
                  javaFile.writeTo(processingEnv.getFiler());
               } catch (IOException e) {
                     e.printStackTrace(); }
                }
        } 


/*列印錯誤的方法 */ 
protected void error(Element element, String message, Object... args) {  
          if (args.length > 0) { message = String.format(message, args);
              } 
          processingEnv.getMessager().printMessage(ERROR, message, element); 
}

//型別的完整路徑名,比如某個Activity的完整路徑 
private String getParentClassName(VariableElement varElement) { 
              TypeElement typeElement = (TypeElement) varElement.getEnclosingElement(); 
              String packageName = AnnotationUtil.getPackageName(processingEnv,typeElement);
             return packageName + "." + typeElement.getSimpleName().toString(); 
  }

}

測試:

public class MainActivity extends AppCompatActivity {

    @ViewInjector(R.id.tv)
    TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjectUtil.injectView(this);

        tv.setText("fddfdfdfdfdfdfdfdfdfdf");

    }
}

重新編譯一下,到當前目錄app\build\generated\source\apt\debug\com\github\essayjoke\可以看到MainActivity$InjectAdapter.java檔案,我們開啟看一下:

package github.com.stoneviewinject;

import com.example.InjectAdapter;
import java.lang.Override;

public class MainActivity$$InjectAdapter implements InjectAdapter<MainActivity> {
  @Override
  public void inject(MainActivity target) {
    target.tv = (android.widget.TextView)(target).findViewById(2131492945);
  }
}

我們再來看下是否真的可以使用:

這裡寫圖片描述

看到這裡我就放心了,哈哈。

下面很簡單了,有興趣,可以自己根據BuffKnife的bind來實現。

註解的知識已經講完了,自己也學到很多東西,希望一直能夠這樣堅持下去。