面試官:ButterKnife爲何執行效率爲何比其餘注入框架高?它的原理是什麼

面試官: ButterKnife爲何執行效率爲何比其餘注入框架高?它的原理是什麼java

心理分析: ButterKnife框架一直都是使用,不多又開發者對butterknife深刻研究的,既然你是面試Android高級崗位,天然須要有相應被問到原理的準備,面試官想問你對註解處理器瞭解多少,Android編譯流程有多少認識android

**求職者:**應該從 註解處理器原理 與優點提及,確定註解處理器對解放生產力的做用。而後能夠引伸常見的 Butterknife,Dagger2,DBFlow。這纔是加分項git

優點github

  1. 咱們日常在使用Java進行開發Android時,常常會須要寫不少重複冗餘的樣板代碼,開發中最多見的一種,就是findViewById了,若是一個界面有不少View,寫起來那叫一個要死要死。因而咱們註解處理器能夠幫助解決冗餘的代碼的,
  2. 因爲是在編譯器進行生成的代碼,並非經過反射實現,因此性能優點是很是高的
  3. 加快開發速度,因爲減小了寫繁瑣的代碼,會對項目進度起有利的做用

接下來咱們一塊兒來看註解處理的原理面試

在android開發中,比較經常使用到的第三方庫中,有很多用到了 註解處理器(Annotation Processor)。 比較常見的就有 ButterknifeDagger2DBFlow 等。api

註解

Java中存在很多關於註解的Api, 好比@Override用於覆蓋父類方法,@Deprecated表示已捨棄的類或方法屬性等,android中又多了一些註解的擴展,如@NonNull, @StringRes, @IntRes等。bash

代碼自動生成

使用代碼自動生成,一是爲了提升編碼的效率,二是避免在運行期大量使用反射,經過在編譯期利用反射生成輔助類和方法以供運行時使用。app

註解處理器的處理步驟主要有如下:框架

  1. 在java編譯器中構建
  2. 編譯器開始執行未執行過的註解處理器
  3. 循環處理註解元素(Element),找到被該註解所修飾的類,方法,或者屬性
  4. 生成對應的類,並寫入文件
  5. 判斷是否全部的註解處理器都已執行完畢,若是沒有,繼續下一個註解處理器的執行(回到步驟1)

Butterknife註解處理器的例子

Butterknife的註解處理器的工做方式以下:dom

  1. 定義一個非私有的屬性變量
  2. 添加該屬性變量的註解和傳入id
  3. 調用Butterknife.bind(..)方法。

當你點擊Android Studio的Build按鈕時,Butterknife先是按照上述步驟生成了對應的輔助類和方法。在代碼執行到bind(..)方法時,Butterknife就去調用以前生成的輔助類方法,完成對被註解元素的賦值操做。

自定義註解處理器

瞭解了基本的知識點後,咱們應該嘗試去使用這些技巧。 接下來是實踐時間,咱們來開發一個簡單的例子,利用註解處理器來自動產生隨機數字和隨機字符串。

  1. 首先建立一個project。
  2. 建立lib_annotations, 這是一個純java的module,不包含任何android代碼,只用於存放註解。
  3. 建立lib_compiler, 這一樣是一個純java的module。該module依賴於步驟2建立的module_annotation,處理註解的代碼都在這裏,該moduule最終不會被打包進apk,因此你能夠在這裏導入任何你想要的任意大小依賴庫。
  4. 建立lib_api, 對該module不作要求,能夠是android library或者java library或者其餘的。該module用於調用步驟3生成的輔助類方法。

1. 添加註解

在lib_annotations中添加兩個註解:RandomString, RandomInt,分別用於生成隨機數字和隨機字符串:

@Retention(CLASS)
@Target(value = FIELD)
public @interface RandomString {
}


@Retention(CLASS)
@Target(value = FIELD)
public @interface RandomInt {
    int minValue() default 0;
    int maxValue() default 65535;
}

複製代碼
  • @interface 自定義註解,使用 @interface 做爲類名修飾符
  • @Target 該註解所能修飾的元素類型,可選類型以下:
public enum ElementType {
    TYPE, //類
    FIELD, //屬性
    METHOD, //方法
    PARAMETER, //參數
    CONSTRUCTOR, //構造函數
    LOCAL_VARIABLE, 
    ANNOTATION_TYPE,
    PACKAGE,
    TYPE_PARAMETER,
    TYPE_USE;

    private ElementType() {
    }
}

複製代碼
  • @Retention 該註解的保留策略,有三種選項:
public enum RetentionPolicy {
    SOURCE, //被編譯器所忽略

    CLASS, //被編譯器保留至類文件,但不會保留至運行時

    RUNTIME //保留至類文件,且保留至運行時,能在運行時反射該註解修飾的對象
}
複製代碼

2. 註解處理器

真正處理註解並生成代碼的操做都在這裏。 在寫代碼以前咱們須要先導入兩個重要的庫,以及咱們的註解模塊:

compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.squareup:javapoet:1.9.0'
implementation project(':lib_annotations')

複製代碼

新建類RandomProcessor.java:

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

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

複製代碼
  • @AutoService @AutoService(Processor.class)會告訴編譯器該註解處理器的存在,並在編譯時自動在META-INF/services下生成javax.annotation.processing.Processor文件,文件的內容爲
com.rhythm7.lib_compiler.RandomProcessor
複製代碼

也就是說,你所聲明的註解處理器都會在被寫入這個配置文件中。 這樣子,當外部程序裝載這個模塊的時候,就能經過該模塊的jar包下的META-INF/services下找到具體的註解處理器的實現類名,並加載實例化,完成模塊的注入。 註解處理器須要實現AbstractProcessor接口,並實現對應的方法

  • init() 可選 在該方法中能夠獲取到processingEnvironment對象,藉由該對象能夠獲取到生成代碼的文件對象, debug輸出對象,以及一些相關工具類
  • getSupportedSourceVersion() 返回所支持的java版本,通常返回當前所支持的最新java版本便可
  • getSupportedAnnotationTypes() 你所須要處理的全部註解,該方法的返回值會被process()方法所接收
  • process() 必須實現 掃描全部被註解的元素,並做處理,最後生成文件。該方法的返回值爲boolean類型,若返回true,則表明本次處理的註解已經都被處理,不但願下一個註解處理器繼續處理,不然下一個註解處理器會繼續處理。

初始化

較詳細代碼以下:

private static final List<Class<? extends Annotation>> RANDOM_TYPES
        = Arrays.asList(RandomInt.class, RandomString.class);

private Messager messager;
private Types typesUtil;
private Elements elementsUtil;
private Filer filer;

private TypeonProcess()per.init(processingEnv);
    messager = processingEnv.getMessager(); 
    typesUtil = processingEnv.getTypeUtils();
    elementsUtil = processingEnv.getElementUtils();
    filer = processingEnv.getFiler();
}

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

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotations = new LinkedHashSet<>();

    for (Class<? extends Annotation> annotation : RANDOM_TYPES) {
        annotations.add(annotation.getCanonicalName());
    }
    return annotations;
}

複製代碼

處理註解

process()方法中執行如下操做:

  1. 掃描全部註解元素,並對註解元素的類型作判斷
for (Element element : roundEnv.getElementsAnnotatedWith(RandomInt.class)) {
    //AnnotatedRandomInt是對被RandomInt註解的Elment的簡單封裝
    AnnotatedRandomInt randomElement = new AnnotatedRandomInt(element);
    messager.printMessage(Diagnostic.Kind.NOTE, randomElement.toString());
    //判斷被註解的類型是否符合要求
    if (!element.asType().getKind().equals(TypeKind.INT)) { 
        messager.printMessage(Diagnostic.Kind.ERROR, randomElement.getSimpleClassName().toString() + "#"
          + randomElement.getElementName().toString() + " is not in valid type int");
    }
  
    //按被註解元素所在類的完整類名爲key將被註解元素存儲進Map中,後面會根據key生成類文件
    String qualifier = randomElement.getQualifiedClassName().toString();
    if (annotatedElementMap.get(qualifier) == null) {
        annotatedElementMap.put(qualifier, new ArrayList<AnnotatedRandomElement>());
    }
    annotatedElementMap.get(qualifier).add(randomElement);
}

複製代碼

生成類文件

將以前以註解所在類爲key的map遍歷,並以key值爲分組生成類文件。

for (Map.Entry<String, List<AnnotatedRandomElement>> entry : annotatedElementMap.entrySet()) {
    MethodSpec constructor = createConstructor(entry.getValue());
    TypeSpec binder = createClass(getClassName(entry.getKey()), constructor);
    JavaFile javaFile = JavaFile.builder(getPackage(entry.getKey()), binder).build();
    javaFile.writeTo(filer);
}

複製代碼

生成類、構造函數、代碼段以及文件都是利用到了javapoet依賴庫。固然你也能夠選擇拼接字符串和本身用文件IO寫入,可是用javapoet要更方便得多。

private MethodSpec createConstructor(List<AnnotatedRandomElement> randomElements) {
    AnnotatedRandomElement firstElement = randomElements.get(0);
    MethodSpec.Builder builder = MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addParameter(TypeName.get(firstElement.getElement().getEnclosingElement().asType()), "target");
    for (int i = 0; i < randomElements.size(); i++) {
        addStatement(builder, randomElements.get(i));
    }
    return builder.build();
}

private void addStatement(MethodSpec.Builder builder, AnnotatedRandomElement randomElement) {
    builder.addStatement(String.format(
            "target.%1$s = %2$s",
            randomElement.getElementName().toString(),
            randomElement.getRandomValue())
    );
}

private TypeSpec createClass(String className, MethodSpec constructor) {
    return TypeSpec.classBuilder(className + "_Random")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addMethod(constructor)
            .build();
}

private String getPackage(String qualifier) {
    return qualifier.substring(0, qualifier.lastIndexOf("."));
}

private String getClassName(String qualifier) {
    return qualifier.substring(qualifier.lastIndexOf(".") + 1);
}

複製代碼

經過以上幾行代碼,建立了類文件。在類的構造函數中添加參數(target), 併爲每個被註解元素添加語句"target.%1$s = %2$s",最後經過javaFile.writeTo(filer)完成文件寫入。

3. 調用生成類的方法

在lib_api中新建一個類:RandomUtil.java,添加註入方法:

public static void inject(Object object) {
    Class bindingClass = Class.forName(object.getClass().getCanonicalName() + "_Random"); 
    Constructor constructor = bindingClass.getConstructor(object.getClass());
    constructor.newInstance(object);
}

複製代碼

這裏利用反射找到了以「Object類名_Random」命名的生成類,並調用它的構造方法。而在咱們以前的註解處理器中,咱們已在生成類的構造方法中實現了屬性的賦值操做。

4. 使用生成類

在app module中依賴剛纔建立的庫:

implementation project(':lib_annotations')
implementation project(':lib_api')
annotationProcessor project(':lib_compiler')

複製代碼

在Activity中的使用

public class MainActivity extends AppCompatActivity {
    @RandomInt(minValue = 10, maxValue = 1000)
    int mRandomInt;

    @RandomString
    String mRandomString;

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

        RandomUtil.inject(this);

        Log.i("RandomInt ==> ", mRandomInt + "");
        Log.i("RandomString ==> ", mRandomString);
    }
}

複製代碼

編譯,運行,查看結果:

RandomInt ==>: 700
RandomString ==>: HhRayFyTtt

複製代碼

被註解的元素成功被自動賦值,說明注入成功。

註解處理的使用可參考完整的demo地址

調試

註解處理器的debug跟普通的代碼debug有點不一樣:

在當前工程路徑下輸入命令

gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac

複製代碼

並在Edit Configurations中新添加一個遠程配置(remote),名字隨意,端口爲5005。 而後點擊debug按鈕,就能夠鏈接上遠程調試器進行Annotation的調試了。

相關文章
相關標籤/搜索