前言
其实一直想写这么个系列, 虽然 Android 开发大部分是基于 Java 语言的, 但是日常开发中基本涉及的都比较简单, 当遇到一些疑难杂症的时候, 很难去找到根因, 本系列就针对一些平常开发比较少涉及的 JAVA 点, 比如, 注解, 代理, 并发等等, 希望能帮到一些朋友重新巩固下基础知识.
今天我们主要来说道说道注解中另一种实现方式, 编译时注解 (CLASS), 不同于运行时注解 (RUNTIME), 大家比较熟悉的需要在代码运行时, 反射拿到注解的参数值, 然后再把值绑定回去, 这样反射毕竟消耗性能. 著名的 ButterKnife 就是用的编译时注解, 利用 APT 在编译时生成文件, 再去赋值, 就不会有性能消耗问题啦~
1. 编译时注解
因为编译时注解需要用到 AbstractProcessor 这个类, 而这个是在 JDK 里面的, 所以我们需要 Android Stuio 中新建一个 Java lib
建好之后, 我们新建一个注解类
- @Retention(RetentionPolicy.CLASS)
- @Target(ElementType.FIELD)
- public @interface InjectView {
- int value();
- }
然后我们在新建一个 java 类, 集成 AbstractProcessor
- import java.util.Set;
- import javax.annotation.processing.AbstractProcessor;
- import javax.annotation.processing.ProcessingEnvironment;
- import javax.annotation.processing.RoundEnvironment;
- import javax.lang.model.SourceVersion;
- import javax.lang.model.element.TypeElement;
- @AutoService(Processor.class)
- public class BuildProcess extends AbstractProcessor {
- private Filer mFiler;
- private Messager mMessager;
- @Override
- public synchronized void init(ProcessingEnvironment mProcessingEnvironment) {
- super.init(mProcessingEnvironment);
- // 初始化我们需要的基础工具
- mFiler = processingEnv.getFiler();
- mMessager = processingEnv.getMessager();
- }
- @Override
- public Set<String> getSupportedAnnotationTypes() {
- Set<String> annotations = new LinkedHashSet<>();
- annotations.add(InjectView.class.getCanonicalName());
- return annotations;
- }
- @Override
- public SourceVersion getSupportedSourceVersio() {
- return SourceVersion.latestSupported();
- }
- @Override
- public boolean process(Set<? extends TypeElement> mSet, RoundEnvironment mRoundEnvironment) {
- return false;
- }
- }
我们主要关注这 4 个方法
init 方法作为一个初始化的入口, 做一些初始化工具类的操作
getSupportedAnnotationTypes(), 这个方法要求我们返回一个 Set 集合, 从方法名我们应该也能看出来是一组支持注解类型的集合元素, 这里我们把注解类加进去, 后续如果要添加注解, 这里需要手动再添加修改
getSupportedSourceVersio(), 返回支持的版本号, 一般返回上次支持的版本号即可
process(), 大头来了, 这个方法是整个类的核心, 也是实现编译时注解最关键的地方, 我们这里从获取到注解参数的值供我们调用, 借鉴一波他人图来展示一下
@AutoService(Processor.class) 也是 google 出品的一个开源库, 省了我们需要写配置文件 META-INF 的工作. 添加依赖
implementation 'com.google.auto.service:auto-service:1.0-rc3'
首先看看最终需要生成的类, 是不是似曾相识的样子!:
接下来就是实现了, 主要的代码当然就在 process 方法里面了
- @Override
- public boolean process(Set<? extends TypeElement> mSet, RoundEnvironment mRoundEnvironment) {
- Map<TypeElement, List<InjectViewField>> parseHashMap = getParseHashMap(mRoundEnvironment);
- for (Map.Entry<TypeElement, List<InjectViewField>> entry : parseHashMap.entrySet()) {
- TypeElement typeElement = entry.getKey();
- List<InjectViewField> list = entry.getValue();
- if (list == null || list.size() == 0) {
- continue;
- }
- // 获取包名
- String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
- // 根据旧 Java 类名创建新的 Java 文件
- String className = typeElement.getQualifiedName().toString().substring(packageName.length() + 1);
- String newClassName = className + "_ViewBinding";
- // 生成方法
- MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
- .addModifiers(Modifier.PUBLIC)
- .addParameter(ClassName.bestGuess(className), "target");
- for (InjectViewField injectViewField : list) {
- String packageNameString = injectViewField.getFieldType().toString();
- ClassName viewClass = ClassName.bestGuess(packageNameString);
- methodBuilder.addStatement
- ("target.$L=($T)target.findViewById($L)", injectViewField.getFieldName()
- , viewClass, injectViewField.getId());
- }
- TypeSpec typeBuilder = TypeSpec.classBuilder(newClassName)
- .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
- .addMethod(methodBuilder.build())
- .build();
- JavaFile javaFile = JavaFile.builder(packageName, typeBuilder)
- .addFileComment("Generated code from Butter Knife. Do not modify!")
- .build();
- try {
- javaFile.writeTo(mFiler);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return true;
- }
- private Map<TypeElement,List<InjectViewField>> getParseHashMap(RoundEnvironment mRoundEnvironment) {
- Map<TypeElement,List<InjectViewField>> map = new HashMap<>();
- List<InjectViewField> injectViewFields = new ArrayList<>();
- for (Element element :mRoundEnvironment.getElementsAnnotatedWith(InjectView.class)) {
- InjectViewField injectViewField = new InjectViewField(element);
- injectViewFields.add(injectViewField);
- map.put((TypeElement) element.getEnclosingElement(),injectViewFields);
- }
- return map;
- }
InjectViewField 作为一个简单的实体类封装一些属性
- public class InjectViewField {
- private int id;
- private TypeElement mTypeElement;
- private String fieldName;
- private TypeMirror fieldType;
- public InjectViewField(Element element) {
- // 获取成员变量名称 mTextView
- fieldName = element.getSimpleName().toString();
- // 获取成员变量类型 TextView
- fieldType = element.asType();
- // 获取成员变量具体 value R.id.textView
- id = element.getAnnotation(InjectView.class).value();
- // 获取完整类名
- mTypeElement = (TypeElement) element.getEnclosingElement();
- }
- public int getId() {
- return id;
- }
- public TypeElement getTypeElement() {
- return mTypeElement;
- }
- public String getFieldName() {
- return fieldName;
- }
- public TypeMirror getFieldType() {
- return fieldType;
- }
- }
这里生成文件用到了 https://github.com/square/javapoet , 这样就不用自己手动去拼接字符串, 具体 API 使用方法, 可以进去瞅瞅, 也是出自 square 之手, 非常简便 然后再动动我们的小手, build 一下, 就能得到我们想要的文件了~, 然后我们看看怎么使用
- public class MainActivity extends AppCompatActivity {
- private static final String TAG = "MainActivity";
- @InjectView(R.id.textView)
- TextView mTextView;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- InjectManager.init(this);
- mTextView.setText("i have changed");
- }
- }
很简单吧!, 看着和 butterknife 很类似, 实际原理差不多, 当然 butterknife 里面涉及的还是蛮复杂的, 但基本是通的, 有兴趣的可以去看看源码, http://jakewharton.github.io/butterknife/ , 然后我们的 InjectManager
- public class InjectManager {
- private static final String TAG = "InjectManager";
- public static void init(Activity mActivity){
- //1, 获取全限定类名
- String name = mActivity.getClass().getName();
- try {
- //2, 根据全限定类名获取通过注解解释器生成的 Java 类
- Class<?> clazz = mActivity.getClass().getClassLoader().loadClass(name + "_ViewBinding");
- //3, 通过反射获取构造方法并创建实例完成依赖注入
- clazz.getConstructor(mActivity.getClass()).newInstance(mActivity);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
有朋友要说了, 你这个不也是反射嘛? emmm... 确实是通过反射生成对应的文件对象, 但是起码每个文件只涉及到了一次反射, 而不是像运行时注解, 每个注解都需要反射拿到, 所以 不要太苛刻了~~ 忽略忽略..
大概基本就是这么多, 再来总结一波!
新建 java lib 库, 自定义类, 继承 AbstractProcessor, 最少复写 3 个函数 (init 不需要的可以不复写)
在 process 方法里面获取对应注解的变量, 方法等等, 存到 Map 里, 后续可以做一定的缓存, 减少消耗, 使用 javapoet, 生成 java 文件
通过反射拿到对应类的对象, 执行赋值代码
好了, 基本大概如此了, 当然也有很多需要优化的地方, 缓存, 设计结构等等, 有兴趣的朋友可以去尝试尝试, 也是一个好的锻炼机会
有疑问的可以留言讨论, 感谢观看 谢谢~~
来源: https://juejin.im/post/5c91faede51d455c354bfe13