前言
对于IOC的概念以及运行时处理不了解的可以看下前面我写的这篇文章:Android IOC——运行时(Runtime)处理
Android开发大多是基于Java开发的,而Java后端的技术又沉淀了那么多年。于是乎,我们可以发现最近这几年,Android开发借鉴了非常多的Java后端思想,比如IOC、AOP、组件化……
目前比较优秀并且使用范围较广的Android IOC框架有ButterKnife、Dagger2、Dagger等等。我们以ButterKnife为例,贴一段使用方式的代码片:
class ExampleActivity extends Activity {
@BindView(R.id.user) EditText username;
@BindView(R.id.pass) EditText password;
@BindString(R.string.login_error) String loginErrorMessage;
@OnClick(R.id.submit) void submit() {
// TODO call server...
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
为什么ButterKnife使用@BindView
、@BindString
和@OnClick
注解修饰成员变量后,再调用ButterKnife.bind(this)后,就完成了对成员变量的赋值呢?其实很简单就是在编译时(Compile time),ButterKnife通过注解处理器(Annotation Processor)生成了这些编译时注解对应的依赖注入辅助类;然后在运行时(*** Runtime***),ButterKnife.bind(this)
通过反射调用这些依赖注入辅助类完成对象的注入。
概念
为了跟上时代,本文的关注重点是如何写一个Android IOC基于注解实现——编译时(Compile time)处理,而其中的核心就是注解处理器(Annotation Processor)。
注解处理器是javac的一个工具,它用来在编译时扫描和处理注解。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些生成的.java文件,会同其他普通的手动编写的.java源文件一样被javac编译。
简单的说,通过注解处理器的这一特性,某些文件(通常是.java,当然也可以是.txt、.xml……)或者说代码,我们可以不用手动编写了,注册的注解处理器会在编译时自动帮我们生成。
说了那么多的注解处理器,还是来看下处理器API吧。在Java中,所有的注解处理器都实现自接口Processor
,通常都是使用抽象类AbstractProcessor。只关注AbstractProcessor
的重要代码,如下所示:
public abstract class AbstractProcessor implements Processor {
/**
* 会被注解处理工具调用
*
* @param processingEnvironment 提供一些有用的工具类Elements、Messager、Filer……
*/
public synchronized void init(ProcessingEnvironment processingEnvironment){ }
/**
* 当前处理器支持的注解
*/
public Set<String> getSupportedAnnotationTypes() { }
/**
* 当前使用的Java版本号,通常这里返回值使用{#link SourceVersion#latestSupported()}
*/
public SourceVersion getSupportedSourceVersion() { }
/**
* 负责注解的扫描、解析、处理,以及生成相应的文件
*
* @param set
* @param roundEnvironment 可以查询出包含特定注解的被注解元素
* @return
*/
public abstract boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment);
}
对于注解处理器的getSupportedAnnotationTypes()
和getSupportedSourceVersion()
来说,还可以用注解实现,比如@SupportedAnnotationTypes
和@SupportedSourceVersion
。
IOC——编译时(Compile time)处理
说明下本文IOC目标——编译时生成依赖注入的辅助类,实现成员变量view和string的依赖注入。
先上个IOC工程结构图,后续以工程module结构来逐步解释:
自定义注解module——lee-annotations
自定义两个编译时(Compile time)注解——InjectString
和InjectView
:
/**
* 用于字符串的编译时注入
* <ul>
* <li>编译完成后,注解失效</li>
* </ul>
*
* @author xpleemoon
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface InjectString {
String value() default "IOC——编译时注解处理\n既要会用洋枪洋炮\n又要会造土枪土炮";
}
/**
* 用于view的编译时注入
* <ul>
* <li>编译完成后,注解失效</li>
* </ul>
*
* @author xpleemoon
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface InjectView {
int id() default 0;
}
@Target(ElementType.FIELD)
限定了它们的使用范围——成员变量,同时注解的属性都有默认值——InjectString
的默认值为“IOC——编译时注解处理\n既要会用洋枪洋炮\n又要会造土枪土炮”,InjectView
的默认id为0。
@Retention(RetentionPolicy.CLASS)
表明注解的保留策略为编译时,即注解的生命周期一直会被保持到编译时,在运行时就无法获取该注解的任何信息了。鉴于此,我们对编译时的注解只能在编译时进行处理。
注解的编译时处理module——lee-compiler
先上一段LeeProcessor
的代码片:
@AutoService(Processor.class)
public class LeeProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
this.mMessager = processingEnvironment.getMessager();
this.mElementUtils = processingEnvironment.getElementUtils();
this.mFiler = processingEnvironment.getFiler();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(InjectString.class.getCanonicalName());
annotations.add(InjectView.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
parseInjector(InjectString.class, roundEnvironment.getElementsAnnotatedWith(InjectString.class));
parseInjector(InjectView.class, roundEnvironment.getElementsAnnotatedWith(InjectView.class));
generateCode();
return false;
}
}
-
@AutoService(Processor.class)
——auto-service是google提供的注解处理器注册工具,通过使用注解@AutoService(Processor.class)
来快速的完成注册过程。当然若不使用auto-service,可以按照以下步骤,纯手工注册LeeProcessor
:- 在lee-compiler module中创建一个文件夹services,该文件夹的全路径为“lee-compiler/src/main/resources/META_INF/services”
- 在第1步创建的services文件夹下创建一个文本文件,名字为“javax.annotation.processing.Processor”
- 在“javax.annotation.processing.Processor”文件中,写入注解处理器
LeeProcessor
的类全限定名com.xpleemoon.compiler.LeeProcessor
,完成对注解处理器LeeProcessor
的注册。若有多个注解处理器,则每个注解处理器都必须要在该文件中注册,并且每个处理器单独一行
-
init(processingEnvironment)
——通过processingEnvironment
获取一系列工具类对象-
mMessager
,用于编译时的信息打印 -
mElementUtils
,Element
的处理工具 -
mFiler
,用于文件创建
-
-
getSupportedAnnotationTypes()
——支持注解InjectString
和InjectView
-
getSupportedSourceVersion()
——当前支持的最新Java版本号 -
process(set, roundEnvironment)
——parseInjector(injectorClz, injectorElements)
:解析处理注解InjectString
和InjectView
,generateCode()
:生成.java辅助类
为了方便理解process(set, roundEnvironment)
的执行过程,接下来将重点分析parseInjector(injectorClz, injectorElements)
和generateCode()
。
分析parseInjector(injectorClz, injectorElements)
/**
* 解析注解
*
* @param injectorClz 注解的class对象
* @param injectorElements 注解的{@link Element}集合
*/
private void parseInjector(Class<?> injectorClz, Set<? extends Element> injectorElements) {
if (injectorClz == null
|| injectorElements == null
|| injectorElements.size() <= 0) {
return;
}
mMessager.printMessage(Diagnostic.Kind.NOTE, "解析注解:" + injectorClz.getSimpleName());
Map<String, List<Element>> map = new HashMap<>(); // key为注解宿主类的全限定名,value为注解element列表
for (Element element : injectorElements) {
if (element.getKind() != ElementKind.FIELD) {
String exceptionMsg = "Only fields can be annotated with " + injectorClz.getSimpleName();
mMessager.printMessage(Diagnostic.Kind.ERROR, exceptionMsg, element);
throw new IllegalStateException(exceptionMsg);
}
String fullClassName = TypeInfoUtils.getFullClzName(mElementUtils, element);
List<Element> elementList = map.get(fullClassName);
if (elementList == null) {
elementList = new ArrayList<>();
map.put(fullClassName, elementList);
}
elementList.add(element);
}
for (Map.Entry<String, List<Element>> entry : map.entrySet()) {
String fullClassName = entry.getKey();
Set<AbstractBinding> bindings = mBindingMap.get(fullClassName);
if (bindings == null) {
bindings = new HashSet<>();
mBindingMap.put(fullClassName, bindings);
}
if (injectorClz == InjectString.class) {
bindings.add(new StringBinding(InjectString.class, mElementUtils, mMessager, entry.getValue()));
} else if (injectorClz == InjectView.class) {
bindings.add(new ViewBinding(InjectView.class, mElementUtils, mMessager, entry.getValue()));
}
}
}
Map<String, List<Element>> map = new HashMap<>()
,构造一个map
(key为注的宿主类的全限定名,value为注解element列表)用于缓存。
第一个for循环遍历注解的Element
集合injectorElements
:
-
String fullClassName = TypeInfoUtils.getFullClzName(mElementUtils, element)
,获取类的全限定名 -
List<Element> elementList = map.get(fullClassName)
,通过类的全限定名fullClassName
作为key,获取对应的value——elementList
-
elementList.add(element)
,把注解的element
添加到上面第2步获取到的列表,也就是间接的把element
存到了map
中
简单的说,第一个for循环干的事情就是把注解的宿主类找出来,然后把宿主类的所有注解缓存到一个列表中,从而形成注解的宿主类(Key)对应注解列表(Value)的结构
第二个for循环遍历map
(由上面第一个for循环生成):
-
String fullClassName = entry.getKey()
,获取注解的宿主类全限定名 -
Set<AbstractBinding> bindings = mBindingMap.get(fullClassName)
,通过第1步拿到的注解的宿主类全限定名获取AbstractBinding
的集合binds
-
mBindingMap
,它是一个成员变量Map<String, Set<AbstractBinding>> mBindingMap = new HashMap<>()
。key:注解的宿主类的全限定名;value:对应宿主类中所有注解的集合 -
AbstractBinding
的主要作用就是生成注解对应的注入方法
- if条件判断
injectorClz
的注解类型,然后new出injectorClz
对应的AbstractBinding
对象,最后添加到第2步的集合binds
。这样也就间接的缓存到了成员变量mBindingMap
中
简单的说,第二个循环就是把注解的宿主类找出来,然后把宿主类的所有注解缓存到一个列表中,从而形成注解的宿主类(Key)对应
AbstractBinding
(生成注解的注入方法)列表(Value)的结构
分析generateCode()
/**
* 生成代码
*/
private void generateCode() {
for (Map.Entry<String, Set<AbstractBinding>> entry : mBindingMap.entrySet()) {
Set<AbstractBinding> bindings = entry.getValue();
if (bindings == null || bindings.size() <= 0) {
continue;
}
List<Element> elementList = bindings.iterator().next().mElementList;
if (elementList == null || elementList.size() <= 0) {
continue;
}
Element element = elementList.get(0);
String simpleClzName = TypeInfoUtils.getSimpleClzName(element) + SUFFIX; // 生成java类的simple名
TypeName typeName = TypeInfoUtils.getEnclosingTypeName(element); // 注解的宿主类
// 1. 创建一系列方法
mMessager.printMessage(Diagnostic.Kind.NOTE, "创建入口方法:" + METHOD);
MethodSpec.Builder bindsMethodBuilder = MethodSpec.methodBuilder(METHOD)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(typeName, PARAMETER)
.addJavadoc("数据绑定的入口\n为{@code $N}中{@link $T}和{@link $T}注解的变量绑定数据", PARAMETER, InjectString.class, InjectView.class);
List<MethodSpec> methodSpecs = new ArrayList<>(); // 方法列表,用于后续创建类时使用
for (AbstractBinding binding : bindings) {
methodSpecs.add(binding.generateMethod());
bindsMethodBuilder.addStatement(binding.getMethodName() + "($N)", PARAMETER);
}
methodSpecs.add(bindsMethodBuilder.build());
// 2. 创建java类
mMessager.printMessage(Diagnostic.Kind.NOTE, "创建类:" + simpleClzName);
TypeSpec.Builder injectorClzBuilder = TypeSpec.classBuilder(simpleClzName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addJavadoc("{@link $T}的leeknife注解字段注入器\n" +
"<ul><li>通过编译器挂载的注解处理器自动生成java源文件后,编译器会再将该java源文文件同其它java源文件一起编译成class文件</li></ul>\n\n" +
"@author $N", typeName, LeeProcessor.class.getSimpleName());
for (MethodSpec methodSpec : methodSpecs) {
injectorClzBuilder.addMethod(methodSpec);
}
TypeSpec injectorClz = injectorClzBuilder.build();
// 3. 生成java源文件
mMessager.printMessage(Diagnostic.Kind.NOTE, "创建java源文件:" + simpleClzName);
JavaFile javaFile = JavaFile.builder(TypeInfoUtils.getPkgName(mElementUtils, element), injectorClz)
.addFileComment("This codes are generated automatically. Do not modify!")
.build();
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
}
循环遍历mBindingMap
,依次执行如下步骤(注:这些步骤已经通过mMessager
手动调用进行打印):
- 创建一系列私有的静态绑定方法,暴露
binds
方法作为那些似有方法的访问入口 - 创建java辅助类
XXX$$Injector
- 生成java源文件
生成代码使用到的
MethodSpec
以及TypeSpec
都是square开源的javapoet,提供了一系列非常方便实用的API,用于生成.java源码。javapoet把这些代码的中间生成过程都当作对象来处理,更符合面向对象编程。当然,你如果一定不想用javapoet,就想找麻烦,那也行就用idk提供的Writer
吧
为了便于理解,现在Terminal通过命令./gradlew clean assembleDebug
编译LeeKnife-IOC这个工程(注:注解已经在CompileIOCActivity中使用,若注解没有使用,那么注解处理器就不会做任何处理),编译过程中,我们将会看到如下信息:
以ViewBinding
为例,我们看下它到底生成了什么样的绑定方法:
final class ViewBinding extends AbstractBinding<InjectView> {
ViewBinding(Class<InjectView> clz, Elements elementUtils, Messager messager, List<Element> elementList) {
super(clz, elementUtils, messager, elementList);
}
@Override
String getMethodName() {
return "bindView";
}
@Override
MethodSpec generateMethod() {
super.generateMethod();
TypeName enclosingTypeName = TypeInfoUtils.getEnclosingTypeName(mElementList.get(0)); // 获取注解宿主类
MethodSpec.Builder builder = MethodSpec.methodBuilder(getMethodName()) // 方法名 bindView
.addModifiers(Modifier.PRIVATE, Modifier.STATIC) // 方法修饰 private static
.returns(void.class) // 方法返回值 无
.addParameter(enclosingTypeName, "target") // 参数名 target
.addJavadoc("为{@code $N}中{@link $T}注解的view变量绑定数据", "target", mClz); // 方法注释
for (Element viewInjectorElement : mElementList) { // 循环添加语句
TypeName typeName = TypeInfoUtils.getTypeName(viewInjectorElement); // 获取注解修饰的字段类型
int viewId = viewInjectorElement.getAnnotation(mClz).id(); // 获取注解指定的id值
builder.addStatement("target.$N = ($T) target.findViewById($L)", viewInjectorElement.getSimpleName(), typeName, viewId); // 构建赋值语句
}
return builder.build();
}
}
ViewBinding
的generateMethod()
方法注视很详细,就不细讲了。调用ViewBinding.generateMethod()
,它最终生成的代码将会是如下形式:
/**
* 为{@code target}中{@link com.xpleemoon.annotations.InjectView}注解的view变量绑定数据 */
private static void bindView(XXX target) {
target.mText = (TextView) target.findViewById(2131427415);
target.mBtn = (Button) target.findViewById(2131427416);
}
IOC使用module——app
效果图镇楼:
public class CompileIOCActivity extends AppCompatActivity {
/**
* 使用默认值注入
*/
@InjectString
String mTextStr;
@InjectString("编译时注解处理,IOC就这么简单")
String mToastStr;
@InjectView(id = R.id.compile_inject_text)
TextView mText;
@InjectView(id = R.id.compile_inject_button)
Button mBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_compile_ioc);
LeeKnife.inject(this);
mText.setText(mTextStr);
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), mToastStr, Toast.LENGTH_LONG).show();
}
});
}
}
可以看到对CompileIOCActivity
类中类型为View
和String
的成员变量使用注解@InjectView
和@InjectString
,同时还发现这里注解修饰的成员变量的访问权限使用包可访问的,那这是为什么呢?先留下这个疑问,本小节最后来解答。
前面说过编译时注解处理器会生成.java源文件,这个文件在下面这个目录:
通过观察发现他的类名就是"CompileIOCActivity"+"$$Injector",并且所在的包名和CompileIOCActivity
一致。那这里包名为什么要一致呢?其实很简单,就是为了调用LeeKnife.inject(target)
时,方便LeeKnife
获取辅助类XXX$$Injector
:
// SUFFIX的值为$$Injector
Class injectorClz = Class.forName(target.getClass().getName() + SUFFIX);
再来看下生成的IOC辅助类CompileIOCActivity$$Injector
源代码:
// This codes are generated automatically. Do not modify!
package com.xpleemoon.annotations.demo.annotationprocess.compile;
import android.widget.Button;
import android.widget.TextView;
/**
* {@link CompileIOCActivity}的leeknife注解字段注入器
* <ul><li>通过编译器挂载的注解处理器自动生成java源文件后,编译器会再将该java源文文件同其它java源文件一起编译成class文件</li></ul>
*
* @author LeeProcessor */
public final class CompileIOCActivity$$Injector {
/**
* 为{@code target}中{@link com.xpleemoon.annotations.InjectView}注解的view变量绑定数据 */
private static void bindView(CompileIOCActivity target) {
target.mText = (TextView) target.findViewById(2131427415);
target.mBtn = (Button) target.findViewById(2131427416);
}
/**
* 为{@code target}中{@link com.xpleemoon.annotations.InjectString}注解的字符串变量绑定数据 */
private static void bindString(CompileIOCActivity target) {
target.mTextStr = "IOC——编译时注解处理\n"
+ "既要会用洋枪洋炮\n"
+ "又要会造土枪土炮";
target.mToastStr = "编译时注解处理,IOC就这么简单";
}
/**
* 数据绑定的入口
* 为{@code target}中{@link com.xpleemoon.annotations.InjectString}和{@link com.xpleemoon.annotations.InjectView}注解的变量绑定数据 */
public static void binds(CompileIOCActivity target) {
bindView(target);
bindString(target);
}
}
CompileIOCActivity
调用的LeeKnife.inject(this)
进行IOC数据注入,其实就是反射调用辅助类CompileIOCActivity$$Injector.binds(target)
。CompileIOCActivity$$Injector
的具体生成过程,可见下一节。
分析到了这里,现在可以回答之前的问题——为什么注解修饰的成员变量的访问权限使用包可访问的?既然注解处理器LeeProcessor
生成的辅助类CompileIOCActivity$$Injector
与注入目标CompileIOCActivity
是在同一个包名下的,那么为了不影响代码执行效率,我们在CompileIOCActivity$$Injector
的bind方法中不使用反射进行数据的注入,而是直接通过target.xxx = yyy
进行赋值。
IOC注入module——lee-knife
public final class LeeKnife {
/**
* 编译时处理生成的java文件后缀
*/
private static final String SUFFIX = "$$Injector";
/**
* 入口方法名
*/
private static final String METHOD = "binds";
private static void check(Activity activity) {
if (activity == null) {
throw new IllegalStateException("依赖注入的activity不能为null");
}
Window window = activity.getWindow();
if (window == null || window.getDecorView() == null) {
throw new IllegalStateException("依赖注入的activity未建立视图");
}
}
/**
* IOC注入
*
* @param target
*/
public static void inject(@NonNull Activity target) {
check(target);
try {
Class injectorClz = Class.forName(target.getClass().getName() + SUFFIX);
Method bindsMethod = injectorClz.getMethod(METHOD, target.getClass());
bindsMethod.invoke(null, target);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
inject(target)
:
- 获取参数
target
对应的辅助类XXX$$Injector
- 反射调用辅助类
XXX$$Injector
中的binds
方法,完成IOC的数据注入target过程
结束
经过上面的分析,应该也了解了如何写一个Android IOC框架(Demo级别)。既然会写了的话,那么对于阅读ButterKnife这一类的Android IOC开源库来说也不是什么难事了。
另外,目前Android App开发上还有一个比较热的东西——AOP,其实也不是什么新东西或者新概念(做过后端或者了解spring的一定知道)。其实要想实现一个AOP也不是什么难事:在IOC的基础上,加上动态代理也是可以实现AOP的。
奉上LeeKnife-IOC源码