面試官: ButterKnife爲何執行效率爲何比其餘注入框架高?它的原理是什麼java
心理分析: ButterKnife框架一直都是使用,不多又開發者對butterknife深刻研究的,既然你是面試Android高級崗位,天然須要有相應被問到原理的準備,面試官想問你對註解處理器瞭解多少,Android編譯流程有多少認識android
**求職者:**應該從 註解處理器原理 與優點提及,確定註解處理器對解放生產力的做用。而後能夠引伸常見的 Butterknife,Dagger2,DBFlow。這纔是加分項git
優點github
接下來咱們一塊兒來看註解處理的原理面試
在android開發中,比較經常使用到的第三方庫中,有很多用到了 註解處理器(Annotation Processor)。 比較常見的就有 Butterknife,Dagger2,DBFlow 等。api
Java中存在很多關於註解的Api, 好比@Override
用於覆蓋父類方法,@Deprecated
表示已捨棄的類或方法屬性等,android中又多了一些註解的擴展,如@NonNull
, @StringRes
, @IntRes
等。bash
使用代碼自動生成,一是爲了提升編碼的效率,二是避免在運行期大量使用反射,經過在編譯期利用反射生成輔助類和方法以供運行時使用。app
註解處理器的處理步驟主要有如下:框架
Butterknife的註解處理器的工做方式以下:dom
Butterknife.bind(..)
方法。當你點擊Android Studio的Build
按鈕時,Butterknife先是按照上述步驟生成了對應的輔助類和方法。在代碼執行到bind(..)
方法時,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;
}
複製代碼
public enum ElementType {
TYPE, //類
FIELD, //屬性
METHOD, //方法
PARAMETER, //參數
CONSTRUCTOR, //構造函數
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE;
private ElementType() {
}
}
複製代碼
public enum RetentionPolicy {
SOURCE, //被編譯器所忽略
CLASS, //被編譯器保留至類文件,但不會保留至運行時
RUNTIME //保留至類文件,且保留至運行時,能在運行時反射該註解修飾的對象
}
複製代碼
真正處理註解並生成代碼的操做都在這裏。 在寫代碼以前咱們須要先導入兩個重要的庫,以及咱們的註解模塊:
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;
}
}
複製代碼
META-INF/services
下生成javax.annotation.processing.Processor
文件,文件的內容爲com.rhythm7.lib_compiler.RandomProcessor
複製代碼
也就是說,你所聲明的註解處理器都會在被寫入這個配置文件中。 這樣子,當外部程序裝載這個模塊的時候,就能經過該模塊的jar包下的META-INF/services下找到具體的註解處理器的實現類名,並加載實例化,完成模塊的注入。 註解處理器須要實現AbstractProcessor
接口,並實現對應的方法
processingEnvironment
對象,藉由該對象能夠獲取到生成代碼的文件對象, debug輸出對象,以及一些相關工具類process()
方法所接收初始化
較詳細代碼以下:
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()
方法中執行如下操做:
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)
完成文件寫入。
在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」命名的生成類,並調用它的構造方法。而在咱們以前的註解處理器中,咱們已在生成類的構造方法中實現了屬性的賦值操做。
在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
複製代碼
被註解的元素成功被自動賦值,說明注入成功。
註解處理器的debug跟普通的代碼debug有點不一樣:
在當前工程路徑下輸入命令
gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac
複製代碼
並在Edit Configurations
中新添加一個遠程配置(remote),名字隨意,端口爲5005。 而後點擊debug按鈕,就能夠鏈接上遠程調試器進行Annotation的調試了。