Android注解越来越引领潮流,比如 Dagger2, ButterKnife, EventBus3 等,他们都是注解类型,而且他们都有个共同点就是编译时生成代码,而不是运行时利用反射,这样大大优化了性能;而这些框架都用到了同一个工具就是:APT(Annotation Processing Tool ),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。
今天我们要自己实现的就是类似ButterKnife的简单的view初始化和点击事件;
先看下整个项目的目录结构:
先从最简单入手,注解moudle:
1.创建名字为viewinject-annotation
的java类型module
2.该module只有两个类:
1.BindView
用来对成员变量进行注解,并且接收一个 int 类型的参数
* Created by JokAr on 16/8/6.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
2.OnClick
对方法进行注解,接收一个或一组 int 类型参数,相当于给一组 View 指定点击响应事件。
/** * Created by JokAr on 16/8/6. */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] value();
}
注解module就完成了,下面看看API module
1.首先创建一个Android moudle 的inject
,然后创建interface
/** * Created by JokAr on 16/8/6. */
public interface Inject<T> {
void inject(T host, Object object, Provider provider);
}
/** * Created by JokAr on 16/8/6. */
public interface Provider {
Context getContext(Object object);
View findView(Object object, int id);
}
因为我们需要生成的文件是这么写的:
public class MainActivity$$ViewInject implements Inject<MainActivity> {
@Override
public void inject(final MainActivity host, Object source, Provider provider) {
host.textView = (TextView)(provider.findView(source, 2131427412));
host.button1 = (Button)(provider.findView(source, 2131427413));
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
host.click();
}
} ;
provider.findView(source, 2131427412).setOnClickListener(listener);
}
}
当然这个生成文件是根据自己需求生成,然后需要一个类来关联自己的activity类与生成的类:
/** * Created by JokAr on 16/8/6. */
public class ViewInject {
private static final ActivityProvider activityProvider = new ActivityProvider();
private static final ViewProvider viewProvider = new ViewProvider();
private static final ArrayMap<String, Inject> injectMap = new ArrayMap<>();
public static void inject(Activity activity) {
inject(activity, activity, activityProvider);
}
public static void inject(View view) {
inject(view, view);
}
private static void inject(Object host, View view) {
inject(host, view, viewProvider);
}
private static void inject(Object host, Object object, Provider provider) {
String className = host.getClass().getName();
try {
Inject inject = injectMap.get(className);
if (inject == null) {
Class<?> aClass = Class.forName(className + "$$ViewInject");
inject = (Inject) aClass.newInstance();
injectMap.put(className, inject);
}
inject.inject(host, object, provider);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
使用方法就是:
ViewInject.inject(this);
Inject
对象,用一个 Map 将第一次找到的对象缓存起来,后面用的时候直接从 Map 里面取。API module类就完成了
再看viewinject-compiler
module:
首先创建名为iewinject-compiler
的Java module ,然后在该module的buile.gradle加上一些依赖:
compile project(':viewinject-annotation')
compile 'com.squareup:javapoet:1.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'
auto-service
主要用于注解 Processor,对其生成 META-INF 配置信息。首先创建ViewInjectProcesser
类:
/** * Created by JokAr on 16/8/8. */
@AutoService(Processor.class)
public class ViewInjectProcesser extends AbstractProcessor {
private Filer mFiler; //文件相关的辅助类
private Elements mElementUtils; //元素相关的辅助类
private Messager mMessager; //日志相关的辅助类
@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) {
return false;
}
/** * 指定使用的 Java 版本。通常返回SourceVersion.latestSupported()。 * @return */
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/** * 指定哪些注解应该被注解处理器注册 * @return */
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
}
}
@AutoService
来注解这个处理器,可以自动生成配置信息。在 init() 可以初始化拿到一些实用的工具类。
这里涉及到了Element
元素,借用一下别人的分析:
这个类的的基本内容就完成了,
现在创建BindViewField
类,来解析BindView
注解类来获取用该注解的相关信息
/** * Created by JokAr on 16/8/8. */
public class BindViewField {
private VariableElement mVariableElement;
private int mresId;
public 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 */
public Name getFieldName() {
return mVariableElement.getSimpleName();
}
/** * 获取变量id * @return */
public int getResId() {
return mresId;
}
/** * 获取变量类型 * @return */
public TypeMirror getFieldType() {
return mVariableElement.asType();
}
}
创建OnClickMethod
类来解析使用OnClick
注解的方法,获取相关信息
public class OnClickMethod {
private ExecutableElement mExecutableElement;
private int[] resIds;
private Name mMethodName;
public OnClickMethod(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.METHOD) {
throw new IllegalArgumentException(
String.format("Only methods can be annotated with @%s",
OnClick.class.getSimpleName()));
}
mExecutableElement = (ExecutableElement) element;
resIds = mExecutableElement.getAnnotation(OnClick.class).value();
if (resIds == null) {
throw new IllegalArgumentException(String.format("Must set valid ids for @%s",
OnClick.class.getSimpleName()));
} else {
for (int id : resIds) {
if (id < 0) {
throw new IllegalArgumentException(String.format("Must set valid id for @%s",
OnClick.class.getSimpleName()));
}
}
}
mMethodName = mExecutableElement.getSimpleName();
List<? extends VariableElement> parameters = mExecutableElement.getParameters();
if (parameters.size() > 0) {
throw new IllegalArgumentException(
String.format("The method annotated with @%s must have no parameters",
OnClick.class.getSimpleName()));
}
}
/** * 获取方法名称 * @return */
public Name getMethodName() {
return mMethodName;
}
/** * 获取id数组 * @return */
public int[] getResIds() {
return resIds;
}
}
然后重点就是生成Java代码文件的类:
/** * Created by JokAr on 16/8/8. */
public class AnnotatedClass {
private TypeElement mTypeElement;
private ArrayList<BindViewField> mFields;
private ArrayList<OnClickMethod> mMethods;
private Elements mElements;
public AnnotatedClass(TypeElement typeElement, Elements elements) {
mTypeElement = typeElement;
mElements = elements;
mFields = new ArrayList<>();
mMethods = new ArrayList<>();
}
public String getFullClassName() {
return mTypeElement.getQualifiedName().toString();
}
public void addField(BindViewField field) {
mFields.add(field);
}
public void addMethod(OnClickMethod method) {
mMethods.add(method);
}
public JavaFile generateFile() {
//generateMethod
MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mTypeElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.PROVIDER,"provider");
for(BindViewField field : mFields){
// find views
injectMethod.addStatement("host.$N = ($T)(provider.findView(source, $L))",
field.getFieldName(),
ClassName.get(field.getFieldType()), field.getResId());
}
for(OnClickMethod method :mMethods){
TypeSpec listener = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeUtil.ANDROID_VIEW, "view")
.addStatement("host.$N()", method.getMethodName())
.build())
.build();
injectMethod.addStatement("View.OnClickListener listener = $L ", listener);
for (int id : method.getResIds()) {
// set listeners
injectMethod.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
}
}
//generaClass
TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewInject")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJET, TypeName.get(mTypeElement.asType())))
.addMethod(injectMethod.build())
.build();
String packgeName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
return JavaFile.builder(packgeName, injectClass).build();
}
}
具体的可以看javapoet的API,然后我们需要完善ViewInjectProcesser
类,增加:
private Map<String, AnnotatedClass> mAnnotatedClassMap;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mAnnotatedClassMap.clear();
try {
processBindView(roundEnv);
processOnClick(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 void processOnClick(RoundEnvironment roundEnv) throws IllegalArgumentException {
for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
OnClickMethod onClickMethod = new OnClickMethod(element);
annotatedClass.addMethod(onClickMethod);
}
}
private void error(String msg, Object... args) {
mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
}
实际使用
在项目的根目录的build.gradle添加:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
在项目的主module的build.gradle添加:
apply plugin: 'com.neenbedankt.android-apt'
compile project(':viewinject-annotation')
compile project(':inject')
apt project(':viewinject-compiler')
compile project(':viewinject-annotation')
compile project(':inject')
annotationProcessor project(':viewinject-compiler')
在自己的activity类使用:
/** * Created by JokAr on 16/8/8. */
public class MainActivity extends AppCompatActivity {
@BindView(R.id.textView)
TextView textView;
@BindView(R.id.button1)
Button button1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewInject.inject(this);
}
@OnClick(R.id.textView)
public void click() {
Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show();
}
}
点击makeProject 就编译完成后就可以在主项目module的/build/generated/source/apt/debug 目录下看到生成的java类文件了
一个学习级的apt项目就完成了。
项目源码
实战项目:Android6.0权限管理 工具,我用java重写别人的kotlin项目;地址:
https://github.com/a1018875550/PermissionDispatcher