前几天在学习Android模块化的时候,对注解有了新的理解,对于模块间通信的问题,可以利用动态代理,反射来获取运行时注解的参数,进而拼接成完整的Uri请求,从而实现不同模块间界面的跳转。感兴趣的朋友可以点击这里。大多数情况下,跳转界面时都会携带一些简单的参数,为了优化这个过程,打算利用编译时注解的技术,类似butterknife,免去获取参数的一系列繁琐的代码。强烈建议先看鸿洋大神的Android如何编写基于编译时注解的项目文章,写的十分详细,我这里主要记录自己整个项目搭建的过程,包括遇到的一些问题,和自己的一些心得体会。
简单来说,编译时注解框架就是为了节省我们的工作量,避免一些重复且繁琐的代码编写。且不说他的实现方式,其实就目的来说和动态代理很类似,都是替我们多“做事”,解放我们的时间。
一、搭建项目基本框架
这里搭建一个类似butterknife的注解框架,在编写编译时注解框架的时候,一般需要建立多个module:
(1)ioc-annotation:用于存放注解等,Java模块(也可以是Android模块)
(2)ioc-compiler:用于编写注解处理器,Java模块(这个必须是Java模块,而且算是最重要的一个环节)
(3)ioc-api:用于调用的API,主要是调用生成后的Java文件,Android模块(也可以是Java模块)
这里会新建一个android项目,用于测试这个框架,就直接称之为app
模块新建完后需要设置依赖关系:
ioc-compiler依赖于ioc-annotation:注解处理器需要获取注解信息
app依赖于ioc-annotation,ioc-api:app需要调用注解和接口进行使用,其实也需要导入ioc-compiler,只不过方式不一样,下面会提到。
二、注解模块的实现
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView
{
int value();
}
注解比较简单:
(1)声明注解存活时间
(2)声明注解运用的地方
(3)声明注解的属性,资源文件的ID值,所以是一个int类型
三、注解处理器的实现
注解处理器的实现其实是比较复杂的,但因为有模板,所以整个过程还是比较直观,我看第一遍的时候由于没有抓住要点,所以整个人看的比较混乱。这里必须要先弄清楚注解处理器是怎么工作的,它的目的是什么,抓住主要的内容即可。
在我看来注解处理器主要做了两件事情:
(1)获取注解信息
(2)生成对应的Java文件,也就是我们想要省掉的那些代码
一开始看到butterknife的时候还在想,这么厉害,一个注解就能代替那么多代码,请恕我年少无知,其实代码并没有少,只不过用了另外一种方式生成了。
添加依赖:
compile 'com.google.auto.service:auto-service:1.0-rc2'
基本代码:
注解处理器一般继承于AbstractProcessor
,这部分的代码基本都是固定的。
@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor
{
private Messager messager;
private Elements elementUtils;
private Map<String, ProxyInfo> mProxyMap = new HashMap<String, ProxyInfo>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv)
{
super.init(processingEnv);
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes()
{
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes;
}
@Override
public SourceVersion getSupportedSourceVersion()
{
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
return true;
}
private boolean checkAnnotationValid(Element annotatedElement, Class clazz)
{
if (annotatedElement.getKind() != ElementKind.FIELD)
{
error(annotatedElement, "%s must be declared on field.", clazz.getSimpleName());
return false;
}
if (ClassValidator.isPrivate(annotatedElement))
{
error(annotatedElement, "%s() must can not be private.", annotatedElement.getSimpleName());
return false;
}
return true;
}
private void error(Element element, String message, Object... args)
{
if (args.length > 0)
{
message = String.format(message, args);
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message, element);
}
}
在实现AbstractProcessor
后,process()
方法是必须实现的,也是我们编写代码的核心部分,后面会介绍。
我们一般会实现getSupportedAnnotationTypes()
和getSupportedSourceVersion()
两个方法,这两个方法一个返回支持的注解类型,一个返回支持的源码版本,参考上面的代码,写法基本是固定的。
除此以外,我们还会选择复写init()
方法,该方法传入一个参数processingEnv
,可以帮助我们去初始化一些父类类:
- Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
- Elements mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
- Messager mMessager;跟日志相关的辅助类。
process的实现
process中的实现,相比较会比较复杂一点,一般可以认为两个大步骤:
- 收集信息
- 生成代理类(本文把编译时生成的类叫代理类)
就是前面说的注解处理器主要做的事情
例如本例,我们会针对每一个类生成一个代理类,例如MainActivity
我们会生成一个MainActivity$$ViewInjector
。那么如果多个类中声明了注解,就对应了多个类,这里就需要:
- 一个类对象,代表具体某个类的代理类生成的全部信息,本例中为
ProxyInfo
- 一个集合,存放上述类对象(到时候遍历生成代理类),本例中为
Map<String, ProxyInfo>
,key为类的全路径。
这里获取信息的代码其实也都是类似的,主要是生成代理类的方法,是最重要的。
收集信息
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
messager.printMessage(Diagnostic.Kind.NOTE , "process...");
mProxyMap.clear();
Set<? extends Element> elesWithBind = roundEnv.getElementsAnnotatedWith(BindView.class);
for (Element element : elesWithBind)
{
checkAnnotationValid(element, BindView.class);
VariableElement variableElement = (VariableElement) element;
//class type
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
//full class name
String fqClassName = classElement.getQualifiedName().toString();
ProxyInfo proxyInfo = mProxyMap.get(fqClassName);
if (proxyInfo == null)
{
proxyInfo = new ProxyInfo(elementUtils, classElement);
mProxyMap.put(fqClassName, proxyInfo);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int id = bindAnnotation.value();
proxyInfo.injectVariables.put(id , variableElement);
}
for (String key : mProxyMap.keySet())
{
ProxyInfo proxyInfo = mProxyMap.get(key);
try
{
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
proxyInfo.getProxyClassFullName(),
proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e)
{
error(proxyInfo.getTypeElement(),
"Unable to write injector for type %s: %s",
proxyInfo.getTypeElement(), e.getMessage());
}
}
return true;
}
第一个for循环是为了遍历注解信息,获取对应的一些信息
第二个for循环是为了遍历整理好的信息,生成对应的代理类,即proxyInfo.generateJavaCode()方法,这个是代理类的字符串格式。
生成代理类
public String generateJavaCode()
{
StringBuilder builder = new StringBuilder();
builder.append("// Generated code. Do not modify!\n");
builder.append("package ").append(packageName).append(";\n\n");
builder.append("import com.example.francis.commonmodule.*;\n");
builder.append("import com.example.ioc_api.ViewInject;\n");
builder.append('\n');
builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfo.PROXY + "<" + typeElement.getQualifiedName() + ">");
builder.append(" {\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
private void generateMethods(StringBuilder builder)
{
builder.append("@Override\n ");
builder.append("public void inject(" + typeElement.getQualifiedName() + " host, Object source ) {\n");
for (int id : injectVariables.keySet())
{
VariableElement element = injectVariables.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
builder.append(" if(source instanceof android.app.Activity){\n");
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")(((android.app.Activity)source).findViewById( " + id + "));\n");
builder.append("\n}else{\n");
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")(((android.view.View)source).findViewById( " + id + "));\n");
builder.append("\n};");
}
builder.append(" }\n");
}
这里import的包名需要换上对应项目的包名,否则会报错,看这个也还是不太直观,可以看下下面生成后的java文件就会比较清楚了。
// Generated code. Do not modify!
package com.example.francis.commonmodule;
import com.example.francis.commonmodule.*;
import com.example.ioc_api.ViewInject;
public class MainActivity$$ViewInject implements ViewInject<com.example.francis.commonmodule.MainActivity> {
@Override
public void inject(com.example.francis.commonmodule.MainActivity host, Object source ) {
if(source instanceof android.app.Activity){
host.tvCommon = (android.widget.TextView)(((android.app.Activity)source).findViewById( 2131165307));
}else{
host.tvCommon = (android.widget.TextView)(((android.view.View)source).findViewById( 2131165307));
}; }
}
我认为最重要的就是生成代理类的方法,其他的都可以说是模板,改动的不多,主要是要弄清楚,你想要实现的代码也就是你不想重复写的代码到底是怎样的,我认为整个框架也是围绕这个去编写
这里采用了完全拼接的方式编写Java代码,也可以使用一些开源库,来通过Java api的方式来生成代码,例如:javapoet,我还没有使用过,
Api模块的实现
这个模块的作用在于提供使用者找到并且调用刚才生成的代理类,类似使用butterknife时,都会在onCreate方法中设置
ButterKnife.bind(this);
都是一样的作用
定义一个接口:
public interface ViewInject<T>
{
void inject(T t, Object source);
}
模仿了butterknife,第一个参数为宿主对象,第二个参数为实际调用findViewById
的对象;当然在Actiivty中,两个参数就一样了。
这个接口其实是为了调用之前的代理类,细心的朋友可以发现前面的代理类实现了这个接口,当然这都是我们自己定义的,只要保证前后的名称一致就是了。
找到代理类:
public class ViewInjector
{
private static final String SUFFIX = "$$ViewInject";
public static void injectView(Activity activity)
{
ViewInject proxyActivity = findProxyActivity(activity);
proxyActivity.inject(activity, activity);
}
public static void injectView(Object object, View view)
{
ViewInject proxyActivity = findProxyActivity(object);
proxyActivity.inject(object, view);
}
private static ViewInject findProxyActivity(Object activity)
{
try
{
Class clazz = activity.getClass();
Class injectorClazz = Class.forName(clazz.getName() + SUFFIX);
return (ViewInject) injectorClazz.newInstance();
} catch (ClassNotFoundException e)
{
e.printStackTrace();
} catch (InstantiationException e)
{
e.printStackTrace();
} catch (IllegalAccessException e)
{
e.printStackTrace();
}
throw new RuntimeException(String.format("can not find %s , something when compiler.", activity.getClass().getSimpleName() + SUFFIX));
}
}
injectView静态方法:提供给使用者调用,同时获得Activity的名字(这里不是只能传Activity,这里只是举例)
findProxyActivity方法:通过名字找到对应的代理类
最后由于代理类实现了我们自定义的接口,我们只要再调用下接口的方法就行了
集成编译模块
Android Stduio 2.2以下使用方法
在项目的根目录的build.gradle添加:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
在项目的build.gradle添加:
apt project(':ioc-compiler')
Android Stduio 2.2以上使用方法
直接在项目的build.gradle添加:
annotationProcessor project(':ioc-compiler')
源码地址:点这里