Java 进阶 _ 注解及其在 Android 中的应用

public void onClick(View view) {
host.OnClick();
}
};
finder.findView(source, 2131230762).setOnClickListener(listener);
}
}

因为我们应用 MyKnife 的类是 MyKnifeActivity,所以这里就生成了名为 MyKnifeActivity?Injector 的类。通过上面的代码,可以看出它实际上调用了 Finder 的方法来为我们的控件 textView 赋值,然后使用控件的 setOnClickListener() 方法为点击事件赋值。这里的 Finder 是我们封装的一个对象,用来从指定的源中获取控件的类,本质上还是调用了指定源的 findViewById() 方法。

然后,与 ButterKnife 类似的是,在使用我们的工具的时候,也需要在 Activity 的 onCreate() 中调用 bind() 方法。这里我们看下这个方法做了什么操作:

public static void bind(Object host, Object source, Finder finder) {
String className = host.getClass().getName();
try {
Injector injector = FINDER_MAPPER.get(className);
if (injector == null) {
Class<?> finderClass = Class.forName(className + “?Injector”);
injector = (Injector) finderClass.newInstance();
FINDER_MAPPER.put(className, injector);
}
injector.inject(host, source, finder);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}

从上面的代码中可以看出,调用 bind() 方法的时候会从 FINDER_MAPPER 尝试获取指定 类名?Injector 的文件。所以,如果说我们应用 bind()的类是 MyKnifeActivity,那么这里获取到的类将会是 MyKnifeActivity?Injector。然后,当我们调用 inject 方法的时候就执行了我们上面的注入操作,来完成对控件和点击事件的赋值。这里的 FINDER_MAPPER 是一个哈希表,用来缓存指定的 Injector 的。所以,从上面也可以看出,这里进行值绑定的时候使用了反射,所以,在应用框架的时候还需要对混淆进行处理。

OK,看完了程序的最终结果,我们来看一下如何生成上面的那个类文件。

API 和注解的定义

首先,我们需要定义注解用来提供给用户进行事件和控件的绑定,

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int id();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
int[] ids();
}

如上面的代码所示,可以看出我们分别用了 ElementType.FIELDElementType.METHOD 指定它们是应用于字段和方法的,然后用了 RetentionPolicy.CLASS 标明它们不会被保留到程序运行时。

然后,我们需要定义 MyKnife,它提供了一个 bind() 方法,其定义如下:

public static void bind(Object host, Object source, Finder finder) {
String className = host.getClass().getName();
try {
Injector injector = FINDER_MAPPER.get(className);
if (injector == null) {
Class<?> finderClass = Class.forName(className + “?Injector”);
injector = (Injector) finderClass.newInstance();
FINDER_MAPPER.put(className, injector);
}
injector.inject(host, source, finder);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}

这里的三个参数的含义分别是:host 是调用绑定方法的类,比如 Activity 等;source是从用来获取绑定的值的数据源,一般理解是从 source 中获取控件赋值给 host 中的字段,通常两者是相同的;最后一个参数 finder 是一个接口,是获取数据的方法的一个封装,有两默认的实现,一个是 ActivityFinder,一个是 ViewFinder,分别用来从 Activity 和 View 中查找控件。

我们之前已经讲过 bind() 方法的作用,即使用反射根据类名来获取一个 Injector,然后调用它的 inject() 方法进行注入。这里的 Injector 是一个接口,我们不会写代码去实现它,而是在编译的时候让编译器直接生成它的实现类。

代码的生成过程

在介绍 Javapoet 和 AbstractProcessor 的时候,我们提到过 Element,它封装了应用注解的对象(方法、字段或者类等)的信息。我们可以从 Element 中获取这些信息并将它们封装成一个对象来方便我们调用。于是就产生了 BindViewFieldOnClickMethod 两个类。它们分别用来描述使用 @BindView 注解和使用 @OnClick 注解的对象的信息。此外,还有一个 AnnotatedClass,它用来描述使用注解的整个类的信息,并且其中定义了List<BindViewField>List<OnClickMethod>,分别用来存储该类中应用注解的字段和方法的信息。

与生成文件和获取注解的对象信息相关的几个字段都是从 AbstractProcessor 中获取的。如下面的代码所示,我们可以从 AbstractProcessor 的 init() 方法的 ProcessingEnvironment 中获取到 ElementsFilerMessager。它们的作用分别是:Elements 类似于一个工具类,用来从 Element 中获取注解对象的信息;Filer 用来支持通过注释处理器创建新文件;Messager 提供注释处理器用来报告错误消息、警告和其他通知的方式。

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elements = processingEnvironment.getElementUtils();
messager = processingEnvironment.getMessager();
filer = processingEnvironment.getFiler();
}

然后在 AbstractProcessor 的 process() 方法中的 RoundEnvironment 参数中,我们又可以获取到指定注解对应的 Element 信息。代码如下所示:

private Map<String, AnnotatedClass> map = new HashMap<>();

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
map.clear();
try {
// 分别用来处理我们定义的两种注解
processBindView(roundEnvironment);
processOnClick(roundEnvironment);
} catch (IllegalArgumentException e) {
return true;
}

try {
// 为缓存的各个使用注解的类生成类文件
for (AnnotatedClass annotatedClass : map.values()) {
annotatedClass.generateFinder().writeTo(filer);
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}

// 从RoundEnvironment中获取@BindView注解的信息
private void processBindView(RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
}
}

// 从RoundEnvironment中获取@OnClick注解的信息
private void processOnClick(RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
OnClickMethod method = new OnClickMethod(element);
annotatedClass.addMethod(method);
}
}

// 获取使用注解的类的信息,先尝试从缓存中获取,缓存中没有的话就实例化一个并放进缓存中
private AnnotatedClass getAnnotatedClass(Element element) {
TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
String fullClassName = encloseElement.getQualifiedName().toString();
AnnotatedClass annotatedClass = map.get(fullClassName);
if (annotatedClass == null) {
annotatedClass = new AnnotatedClass(encloseElement, elements);
map.put(fullClassName, annotatedClass);
}
return annotatedClass;
}

上面的代码的逻辑是,在调用 process() 方法的时候,会根据传入的 RoundEnvironment 分别处理两种注解。两个注解的相关信息都会被解析成 List<BindViewField>List<OnClickMethod>,然后把使用注解的整个类的信息统一放置在 AnnotatedClass 中。为了提升程序的效率,这里用了缓存来存储类信息。最后,我们调用了 annotatedClass.generateFinder() 获取一个JavaFile,并调用它的 writeTo(filer) 方法生成类文件。

上面的代码重点在于解析使用注解的类的信息,至于如何根据类信息生成类文件,我们还需要看下 AnnotatedClassgenerateFinder() 方法,其代码如下所示。这里我们用了之前提到的 Javapoet 来帮助我们生成类文件:

public JavaFile generateFinder() {
// 这里用来定义inject方法的签名
MethodSpec.Builder builder = MethodSpec.methodBuilder(“inject”)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(typeElement.asType()), “host”, Modifier.FINAL)
.addParameter(TypeName.OBJECT, “source”)
.addParameter(TypeUtils.FINDER, “finder”);
// 这里用来定义inject方法中@BindView注解的绑定过程
for (BindViewField field : bindViewFields) {
builder.addStatement(“host. N = ( N=( N=(T)finder.findView(source, KaTeX parse error: Expected 'EOF', got '}' at position 85: ….getViewId()); }̲ // 这里用来定义injec…T listener”, TypeUtils.ONCLICK_LISTENER);
}
for (OnClickMethod method : onClickMethods) {
TypeSpec listener = TypeSpec.anonymousClassBuilder(“”)
.addSuperinterface(TypeUtils.ONCLICK_LISTENER)
.addMethod(MethodSpec.methodBuilder(“onClick”)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeUtils.ANDROID_VIEW, “view”)
.addStatement(“host.$N()”, method.getMethodName())
.build())
.build();
builder.addStatement(“listener = $L”, listener);
for (int id : method.getIds()) {
builder.addStatement(“finder.findView(source, $L).setOnClickListener(listener)”, id);
}
}
// 这里用来获取要生成的类所在的包的信息
String packageName = getPackageName(typeElement);
String className = getClassName(typeElement, packageName);
ClassName bindClassName = ClassName.get(packageName, className);

// 用来最终组装成我们要输出的类
TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + “?Injector”)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtils.INJECTOR, TypeName.get(typeElement.asType())))
.addMethod(builder.build())
.build();
return JavaFile.builder(packageName, finderClass).build();
}

上面就是我们用来最终生成类文件的方法,这里用了 Javapoet ,如果对它不是很了解可以到 Github 上面了解一下它的用法。

这样我们就完成了整个方法的定义。

使用 MyKnife

使用我们定义的 MyKnife ,我们只需要在 Gradle 里面引入我们的包即可:

implementation project(‘:knife-api’)
implementation project(‘:knife-annotation’)
annotationProcessor project(‘:knife-compiler’)

也许你在有的地方看到过要使用 android-apt 引入注解处理器,其实这里的annotationProcessor 与之作用是一样的。这里推荐使用 annotationProcessor,因为它更加简洁,不需要额外的配置,也是官方推荐的使用方式。

然后,我们只需要在代码中使用它们就可以了:

public class MyKnifeActivity extends CommonActivity {

@BindView(id = R.id.tv)
public TextView textView;

@OnClick(ids = {R.id.btn})
public void OnClick() {
ToastUtils.makeToast(“OnClick”);
}

@Override
protected int getLayoutResId() {
return R.layout.activity_my_knife;
}

@Override
protected void doCreateView(Bundle savedInstanceState) {
MyKnife.bind(this);
textView.setText(“This is MyKnife demo!”);
}
}

这里有几个地方需要注意:

  1. 使用注解的方法和字段需要至少是 protected,因为我们使用了直接引用的方式,而生成的文件和上面的类包相同,所以至少应该保证包级别访问权限;
  2. 上面使用注解的方式只能在当前 Module 作为 application 的时候使用,作为 library 的时候无法使用,这是因为只有当 Module 作为 application 的时候,R文件中的 id 是 final 的,作为 library 的时候是非 final 的。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

详细整理在GitHub可以见;

Android架构视频+BAT面试专题PDF+学习笔记​

合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-pnA7bL1G-1710841685823)]

详细整理在GitHub可以见;

Android架构视频+BAT面试专题PDF+学习笔记​

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值