ButterKnife執行效率為什么比其他注入框架高?它的原理是什么 [復制鏈接]

2019-8-6 11:00
Newpaper 閱讀:1400 評論:0 贊:0
Tag:  ButterKnife

面試官: ButterKnife為什么執行效率為什么比其他注入框架高?它的原理是什么

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

**求職者:**應該從 注解處理器原理 與優勢說起,肯定注解處理器對解放生產力的作用。然后可以引申常見的 Butterknife,Dagger2,DBFlow。這才是加分項

優勢

  1. 我們平常在使用Java進行開發Android時,經常會需要寫很多重復冗余的樣板代碼,開發中最常見的一種,就是findViewById了,如果一個界面有很多View,寫起來那叫一個要死要死。于是我們注解處理器可以幫助解決冗余的代碼的,
  2. 由于是在編譯器進行生成的代碼,并不是通過反射實現,所以性能優勢是非常高的
  3. 加快開發速度,由于減少了寫繁瑣的代碼,會對項目進度起有利的作用

接下來我們一起來看注解處理的原理

在android開發中,比較常用到的第三方庫中,有不少用到了 注解處理器(Annotation Processor)。 比較常見的就有 Butterknife,Dagger2,DBFlow 等。

注解

Java中存在不少關于注解的Api, 比如@Override用于覆蓋父類方法,@Deprecated表示已舍棄的類或方法屬性等,android中又多了一些注解的擴展,如@NonNull, @StringRes, @IntRes等。

代碼自動生成

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

注解處理器的處理步驟主要有以下:

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

Butterknife注解處理器的例子

Butterknife的注解處理器的工作方式如下:

  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生成的輔助類方法。
ButterKnife執行效率為什么比其他注入框架高?它的原理是什么

ButterKnife執行效率為什么比其他注入框架高?它的原理是什么

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的調試了。

ButterKnife執行效率為什么比其他注入框架高?它的原理是什么

附:Android高級進階學習資料圖

ButterKnife執行效率為什么比其他注入框架高?它的原理是什么

我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

阿拉斯加垂钓APP下载
去非洲卖什么东西赚钱 青海11选五最大遗漏号 用一万炒期货能赚钱吗 辽宁快乐12手机版走势图 小说 竟彩 时时彩后三直选规律 山东11选5开奖公告 答题赚钱精力点 江西快3走势图一定牛 天津时时彩玩法说明 999捕鱼下 在超市里门口卖什么赚钱 成都麻将实战一百例46 中体育比分 pt平台是哪个公司的 云南快乐十分app