前言:
Annotation注解在Android的開發中的使用越來越普遍,例如EventBus、ButterKnife、Dagger2等,之前使用注解的時候需要利用反射機制勢必影響到運行效率及性能,直到后來android-apt的出現通過注解根據反射機制動態編譯生成代碼的方式來解決在運行時不再使用發射機制,不過隨着android-apt的退出不再維護,我們今天利用Android studio的官方插件annotationProcessor來實現一下自己的ButterKnife UI注解框架。
需要了解的知識:
自動成代碼:
1.)先看下整個項目結構
整個項目分一個app、app.api、app.annotation、app.complier
app:整個項目的入口 用於測試注解框架
app.annotation:主要用於申明app所有使用的UI注解
app.api:用於申明UI注解框架的api
app.complier:用於在編譯期間通過反射機制自動生成代碼
2.)app.annotation 聲明注解框架中要使用的注解
這里我聲明了一個BindView注解,聲明周期為Class,作用域為成員變量
@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView { int value(); }
因為這里僅僅想要實現綁定View控件,這里就聲明了一個BindView注解
注意:
app.annotation module為java library,build.gradle配置如下
apply plugin: 'java'
sourceCompatibility =JavaVersion.VERSION_1_7
targetCompatibility =JavaVersion.VERSION_1_7
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
3.)app.api 聲明UI注解框架中使用的api,比如綁定解綁,查找View控件等
面向接口編程,定義一個綁定注解的接口
/** * UI綁定解綁接口 * * @param <T> */ public interface ViewBinder<T> { void bindView(T host, Object object, ViewFinder finder); void unBindView(T host); }
定義一個被綁定者查找view的接口
/** * ui提供者接口 */ public interface ViewFinder { View findView(Object object, int id); }
這里聲明一個Activity 默認的View查找者
/** * Activity UI查找提供者 */ public class ActivityViewFinder implements ViewFinder { @Override public View findView(Object object, int id) { return ((Activity) object).findViewById(id); } }
注解框架向外提供綁定方法,這里使用靜態類來管理
public class LCJViewBinder { private static final ActivityViewFinder activityFinder = new ActivityViewFinder();//默認聲明一個Activity View查找器 private static final Map<String, ViewBinder> binderMap = new LinkedHashMap<>();//管理保持管理者Map集合 /** * Activity注解綁定 ActivityViewFinder * * @param activity */ public static void bind(Activity activity) { bind(activity, activity, activityFinder); } /** * '注解綁定 * * @param host 表示注解 View 變量所在的類,也就是注解類 * @param object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找 * @param finder ui綁定提供者接口 */ private static void bind(Object host, Object object, ViewFinder finder) { String className = host.getClass().getName(); try { ViewBinder binder = binderMap.get(className); if (binder == null) { Class<?> aClass = Class.forName(className + "$$ViewBinder"); binder = (ViewBinder) aClass.newInstance(); binderMap.put(className, binder); } if (binder != null) { binder.bindView(host, object, finder); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } /** * 解除注解綁定 ActivityViewFinder * * @param host */ public static void unBind(Object host) { String className = host.getClass().getName(); ViewBinder binder = binderMap.get(className); if (binder != null) { binder.unBindView(host); } binderMap.remove(className); } }
4.)app.complier根據注解在編譯期間自動生成java代碼
優先需要自定義一個AbstractProcessor,然后Annotation生成代碼,完整的AbstractProcessor
public class LCJViewBinderProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment env){ } @Override public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { } @Override public Set<String> getSupportedAnnotationTypes() { } @Override public SourceVersion getSupportedSourceVersion() { } }
重要函數解說
- init(ProcessingEnvironment env): 每一個注解處理器類都必須有一個空的構造函數。然而,這里有一個特殊的init()方法,它會被注解處理工具調用,並輸入ProcessingEnviroment參數。ProcessingEnviroment提供很多有用的工具類Elements,Types和Filer。
-
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env)這相當於每個處理器的主函數main()。 在這里寫掃描、評估和處理注解的代碼,以及生成Java文件。輸入參數RoundEnviroment,可以讓查詢出包含特定注解的被注解元素。
- getSupportedAnnotationTypes();這里必須指定,這個注解處理器是注冊給哪個注解的。注意,它的返回值是一個字符串的集合,包含本處理器想要處理的注解類型的合法全稱。換句話說,在這里定義你的注解處理器注冊到哪些注解上。
- getSupportedSourceVersion();用來指定你使用的Java版本。
該module同樣是Java Library,build.gradle配置如下
apply plugin: 'java'
//apply plugin: 'com.github.dcendents.android-maven'
sourceCompatibility =JavaVersion.VERSION_1_7
targetCompatibility =JavaVersion.VERSION_1_7
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':app.annotation')
}
-
com.google.auto.service:auto-service:1.0-rc2 谷歌提供的Java 生成源代碼庫
-
com.squareup:javapoet:1.7.0 提供了各種 API 讓你用各種姿勢去生成 Java 代碼文件
定義一個被注解類對象AnnotedClass,用於保存哪些被注解的對象
class AnnotatedClass { private static class TypeUtil { static final ClassName BINDER = ClassName.get("com.whoislcj.appapi", "ViewBinder"); static final ClassName PROVIDER = ClassName.get("com.whoislcj.appapi", "ViewFinder"); } private TypeElement mTypeElement; private ArrayList<BindViewField> mFields; private Elements mElements; AnnotatedClass(TypeElement typeElement, Elements elements) { mTypeElement = typeElement; mElements = elements; mFields = new ArrayList<>(); } void addField(BindViewField field) { mFields.add(field); } JavaFile generateFile() { //generateMethod MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder("bindView") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(TypeName.get(mTypeElement.asType()), "host") .addParameter(TypeName.OBJECT, "source") .addParameter(TypeUtil.PROVIDER, "finder"); for (BindViewField field : mFields) { // find views bindViewMethod.addStatement("host.$N = ($T)(finder.findView(source, $L))", field.getFieldName(), ClassName.get(field.getFieldType()), field.getResId()); } MethodSpec.Builder unBindViewMethod = MethodSpec.methodBuilder("unBindView") .addModifiers(Modifier.PUBLIC) .addParameter(TypeName.get(mTypeElement.asType()), "host") .addAnnotation(Override.class); for (BindViewField field : mFields) { unBindViewMethod.addStatement("host.$N = null", field.getFieldName()); } //generaClass TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewBinder") .addModifiers(Modifier.PUBLIC) .addSuperinterface(ParameterizedTypeName.get(TypeUtil.BINDER, TypeName.get(mTypeElement.asType()))) .addMethod(bindViewMethod.build()) .addMethod(unBindViewMethod.build()) .build(); String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString(); return JavaFile.builder(packageName, injectClass).build(); } }
然后再定義一個BindViewField對象用於被注解的成員變量
class BindViewField { private VariableElement mVariableElement; private int mResId; BindViewField(Element element) throws IllegalArgumentException { if (element.getKind() != ElementKind.FIELD) { throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s", BindView.class.getSimpleName())); } mVariableElement = (VariableElement) element; BindView bindView = mVariableElement.getAnnotation(BindView.class); mResId = bindView.value(); if (mResId < 0) { throw new IllegalArgumentException( String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(), mVariableElement.getSimpleName())); } } /** * 獲取變量名稱 * * @return */ Name getFieldName() { return mVariableElement.getSimpleName(); } /** * 獲取變量id * * @return */ int getResId() { return mResId; } /** * 獲取變量類型 * * @return */ TypeMirror getFieldType() { return mVariableElement.asType(); } }
上面兩個對象定義好了之后,就下來實現一下根據注解生成代碼過程
@AutoService(Processor.class) public class LCJViewBinderProcessor extends AbstractProcessor { private Filer mFiler; //文件相關的輔助類 private Elements mElementUtils; //元素相關的輔助類 private Messager mMessager; //日志相關的輔助類 private Map<String, AnnotatedClass> mAnnotatedClassMap; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); mFiler = processingEnv.getFiler(); mElementUtils = processingEnv.getElementUtils(); mMessager = processingEnv.getMessager(); mAnnotatedClassMap = new TreeMap<>(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { mAnnotatedClassMap.clear(); try { processBindView(roundEnv); } catch (IllegalArgumentException e) { e.printStackTrace(); error(e.getMessage()); } for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) { try { annotatedClass.generateFile().writeTo(mFiler); } catch (IOException e) { error("Generate file failed, reason: %s", e.getMessage()); } } return true; } private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException { for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField bindViewField = new BindViewField(element); annotatedClass.addField(bindViewField); } } private AnnotatedClass getAnnotatedClass(Element element) { TypeElement typeElement = (TypeElement) element.getEnclosingElement(); String fullName = typeElement.getQualifiedName().toString(); AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullName); if (annotatedClass == null) { annotatedClass = new AnnotatedClass(typeElement, mElementUtils); mAnnotatedClassMap.put(fullName, annotatedClass); } return annotatedClass; } private void error(String msg, Object... args) { mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args)); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new LinkedHashSet<>(); types.add(BindView.class.getCanonicalName()); return types; } }
原理是現解析保存被注解的類,然后再根據注解解析被注解的成員變量,進行保存,最后根據生成java類進行寫文件
5.)app使用注解框架的應用
在build.gradle中添加如下配置
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:24.2.1' testCompile 'junit:junit:4.12' compile project(':app.api') compile project(':app.annotation') annotationProcessor project(':app.compiler') }
Activity中使用
public class MainActivity extends AppCompatActivity { @BindView(R.id.test) Button mButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); LCJViewBinder.bind(this); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "success", Toast.LENGTH_SHORT).show(); } }); } @Override protected void onDestroy() { super.onDestroy(); LCJViewBinder.unBind(this); } }
然后把項目重新build一下就會自動生成MainActivity$$ViewBinder類
public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> { @Override public void bindView(MainActivity host, Object source, ViewFinder finder) { host.mButton = (Button)(finder.findView(source, 2131427413)); } @Override public void unBindView(MainActivity host) { host.mButton = null; } }
總結:
通過注解生成代碼在平時的開發過程中可能很少接觸,因為目前很多開源框架幫我們處理了這部分,如果我們需要自己做一個使用注解的框架就需要這方面知識了,這個例子僅僅是我自己查找資源然后模仿做出來的,其實我們項目中業務組件化之間可以通過注解來聲明路由scheme地址,后期有時間實現一下。