手写ButterKnife
开发中使用注解框架可以极大地提高编码效率,注解框架用到的技术可以分为两种,运行时注解跟编译时注解。运行时注解一般配合反射机制使用,编译时注解则是用来生成模板代码。这里我们分别使用这两种方法实现ButterKnife的控件绑定功能。
1、运行时注解
运行时注解实现比较简单,但是由于完全依靠反射技术,所以运行效率较低。首先我们需要新建一个注解类,指定其保留时间为运行时,修饰对象为类成员变量,值为控件ID。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
@IdRes int value();
}
然后我们需要使用反射实现 bindView 方法。先是使用 class.getDeclaredFields 方法获取类中所有的成员变量,如果该成员变量被 @BindView 注解则根据注解中的控件ID调用 view.findViewById ,并将返回值赋给该成员变量。这种方法的一个好处是被 @BindView 注解的成员变量可以是私有的。
public class ButterKnife {
public static void bindView(Activity activity) {
bindView(activity, activity.getWindow().getDecorView());
}
public static void bindView(Object object, View view) {
Class clazz = object.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field f : fields) {
BindView binder = f.getAnnotation(BindView.class);
if (binder != null) {
f.setAccessible(true);
f.set(object, view.findViewById(binder.value()));
}
}
}
}
2、编译时注解
我们知道ButterKnife是基于编译时注解的,依赖注解生成模板代码从而实现控件绑定。使用编译时注解前需要对注解处理器(Annotation Processor)有一个基本了解。
Annotation processing is a tool build in javac for scanning and processing annotations at compile time. You can register your own annotation processor for certain annotations.
在这里我们通过注解处理器自动生成模板代码,并且与项目代码一起编译。
2.1、创建库项目
在Android Studio中创建完项目后,我们需要在 File -> New -> New Module... 创建3个模块。
| 名称 | 类别 | 作用 |
| test-annotations | Java Liabrary | 存放我们自己定义的注解类 |
| test-api | Android Liabrary | 存放在主模块中使用的工具类 |
| test-compiler | Java Liabrary | 存放注解处理器 |
然后需要在每个模块的build.gradle文件中配置它们之间的依赖关系以及需要用到的类库。
// test-annotations:
dependencies {
...
implementation 'com.android.support:support-annotations:27.1.0@jar'
} // test-api:
dependencies {
...
compile project(path: ':test-annotations')
} // test-compiler:
dependencies {
...
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile project(path: ':test-annotations')
}
2.2、自定义注解
接下来我们需要在“test-annotations”模块中创建一个用于控件绑定的注解类,与第一节中类似,不过保留时间更改为了编译时。
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
@IdRes int value();
}
2.3、创建注解处理器
处理器的实现在“test-compiler”模块中,代码比较繁琐,大体上分为两步:收集信息然后生成代码。
@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor { private Filer mFileUtils;
private Elements mElementUtils; @Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFileUtils = processingEnvironment.getFiler();
mElementUtils = processingEnvironment.getElementUtils();
} @Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationType = new HashSet<>();
annotationType.add(BindView.class.getCanonicalName());
return annotationType;
} @Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 处理逻辑...
}
}
这里使用了谷歌提供的 @AutoService 对处理器进行注册,然后复写AbstractProcessor类的方法。在 getSupportedAnnotationTypes 函数中返回处理器支持的注解类型,在 init 函数中获取我们需要用到的工具类,处理器的主要逻辑则在 process 函数中。
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Map<String, ClassInfo> classMap = new HashMap<>();
// 收集信息:
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(AnnotationValue.class);
for (Element element : elements) {
if (element.getKind() == ElementKind.FIELD) {
VariableElement variableElement = (VariableElement) element;
// 获取成员变量所在的类。
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
// 获取类的全限定名。
String qualifeiedName = typeElement.getQualifiedName().toString();
// 将同一个类的所有成员变量放到一个ClassInfo类中
ClassInfo classInfo = classMap.get(qualifeiedName);
if (classInfo == null) {
PackageElement packageElement = mElementUtils.getPackageOf(typeElement);
classInfo = new ClassInfo(packageElement, typeElement);
classMap.put(qualifeiedName, classInfo);
}
classInfo.addVariable(variableElement);
}
}
// 生成代码:
...
}
首先我们通过 roundEnvironment.getElementsAnnotatedWith 获取到被 @BindView 注解的所有元素,显然这里的 @BindView 希望注解在类的成员变量上,所以先对元素的类型进行判断,然后将所有的元素按照所在类的全限定名进行分类(通过ClassInfo类表示)。这里需要了解Element类的含义:
- VariableElement:一般代表成员变量。
- ExecutableElement:一般代表类中的方法。
- TypeElement:一般代表类。
- PackageElement:一般代表包。
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 收集信息:
...
// 生成代码:
for (String key : classMap.keySet()) {
ClassInfo classInfo = classMap.get(key);
try {
JavaFileObject sourceFile = mFileUtils.createSourceFile(classInfo.getProxyClassFullName());
Writer writer = sourceFile.openWriter();
writer.write(classInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
代码生成阶段比较简单,先使用 mFileUtils.createSourceFile 函数创建文件对象,文件名通过 classInfo.getProxyClassFullName函数获得。然后向该文件对象中写入通过 classInfo.generateJavaCode 函数获取的源码。接下来看一下ClassInfo类的实现:
public class ClassInfo {
private String mPackageName;
private String mProxyClassName;
private ArrayList<VariableElement> mInjectVariables = new ArrayList<>();
public static final String PROXY = "ViewInject";
public ClassInfo(PackageElement packageElement, TypeElement classElement) {
// 获取包名
String packageName = packageElement.getQualifiedName().toString();
this.mPackageName = packageName;
// 获取全限定名后去除包名,将‘.’替换为‘$’是因为如果类A有个内部
// 类B,那么B在编译后名字会变为A.B
int packageLen = packageName.length() + 1;
String className = classElement.getQualifiedName().toString().substring(packageLen).replace('.', '$');
// 设置代理类的名字,之后我们需要根据这个名字获取到代理类。
this.mProxyClassName = className + "$$" + PROXY;
}
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append(String.format("package %s;\n", mPackageName));
builder.append(String.format("public final class %s {\n", mProxyClassName));
builder.append(String.format("public %s(%s obj) {\n", mProxyClassName, mClassName));
for (VariableElement element : mInjectVariables) {
AnnotationValue binder = element.getAnnotation(AnnotationValue.class);
String name = element.getSimpleName().toString();
builder.append(String.format("obj.%s=obj.findViewById(%d);\n", name, binder.value()));
}
builder.append("}\n }\n");
return builder.toString();
}
public String getProxyClassFullName() {
// 返回代理类全限定名
return mPackageName + "." + mProxyClassName;
}
public void addVariable(VariableElement element) {
mInjectVariables.add(element);
}
}
它的功能是为我们需要的代理类的生成源码,这里使用了字符串拼接的方式,也有许多第三方库可供使用,如Square公司的JavaPoet。
2.4、使用处理器
处理器编写完成后需要在主模块的build.gradle文件中进行配置才能生效,一是引入存放注解的“test-annotations”模块,二是指定“test-compiler”模块中的注解处理器。
dependencies {
...
annotationProcessor project(':test-complier')
compile project(path: ':test-api')
}
接着在MainActivity中用 @BindView 注解一个控件然后编译项目,编译完成后处理器自动生成的类就可以在主模块的 build\generated\source\apt\debug\包名\ 下找到。
package com.mmmmar.androidannotation;
public final class MainActivity$$ViewInject {
public MainActivity$$ViewInject(MainActivity obj) {
obj.mTextView = obj.findViewById(2131165307);
}
}
2.5、完成控件绑定
生成代理类后我们可以在源码中进行手动调用,但是比较优雅的做法是像ButterKnife一样提供一个统一的调用工具,接下来我们在“test-api”模块中进行实现。
public class ButterKnife {
public static void bindView(Activity activity) {
bindView(activity, activity.getWindow().getDecorView());
}
public static void bindView(Object object, View view) {
Class clazz = object.getClass();
String clazzName = clazz.getCanonicalName() + "$$ViewInject";
try {
Class proxy = clazz.getClassLoader().loadClass(clazzName);
Constructor constructor = proxy.getConstructor(clazz);
constructor.newInstance(object);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
因为处理器生成的代理类的逻辑都在构造函数中,所以我们只需要通过类加载器根据类名获取到代理类的Class对象,然后调用其构造函数即可完成功能。虽然这里也用到了反射,但只是用来调用一次代理类的构造函数,所以不会对性能照成影响。
手写ButterKnife的更多相关文章
- 手写butterknife来剖析其原理
基本使用: 对于butterknife库我想基本上都非常熟了,如今在项目中用它也用得非常之频繁了,不过为了学习的完整性,先来简单的回顾一下基本用法,先新建一个工程: 然后给textview增加一个点击 ...
- 移动架构-手写ButterKnife框架
ButterKnife在实际开发中有着大量运用,其强大的view绑定和click事件处理,使得开发效率大大提高,同时增加了代码的阅读性又不影响其执行效率 注解的分类 注解主要有两种分类,一个是运行时, ...
- 【Win 10 应用开发】手写识别
记得前面(忘了是哪天写的,反正是前些天,请用力点击这里观看)老周讲了一个14393新增的控件,可以很轻松地结合InkCanvas来完成涂鸦.其实,InkCanvas除了涂鸦外,另一个大用途是墨迹识别, ...
- JS / Egret 单笔手写识别、手势识别
UnistrokeRecognizer 单笔手写识别.手势识别 UnistrokeRecognizer : https://github.com/RichLiu1023/UnistrokeRecogn ...
- 如何用卷积神经网络CNN识别手写数字集?
前几天用CNN识别手写数字集,后来看到kaggle上有一个比赛是识别手写数字集的,已经进行了一年多了,目前有1179个有效提交,最高的是100%,我做了一下,用keras做的,一开始用最简单的MLP, ...
- 【转】机器学习教程 十四-利用tensorflow做手写数字识别
模式识别领域应用机器学习的场景非常多,手写识别就是其中一种,最简单的数字识别是一个多类分类问题,我们借这个多类分类问题来介绍一下google最新开源的tensorflow框架,后面深度学习的内容都会基 ...
- caffe_手写数字识别Lenet模型理解
这两天看了Lenet的模型理解,很简单的手写数字CNN网络,90年代美国用它来识别钞票,准确率还是很高的,所以它也是一个很经典的模型.而且学习这个模型也有助于我们理解更大的网络比如Imagenet等等 ...
- 使用神经网络来识别手写数字【译】(三)- 用Python代码实现
实现我们分类数字的网络 好,让我们使用随机梯度下降和 MNIST训练数据来写一个程序来学习怎样识别手写数字. 我们用Python (2.7) 来实现.只有 74 行代码!我们需要的第一个东西是 MNI ...
- 手写原生ajax
关于手写原生ajax重要不重要,各位道友自己揣摩吧, 本着学习才能进步,分享大家共同受益,自己也在自己博客里写一下 function createXMLHTTPRequest() { //1.创建XM ...
随机推荐
- VxWorks 操作系统内存布局
在VxWorks操作系统过程中可能使用到的BootRom和VxWorks内核映像本身都可以存在两种方式:压缩的和非压缩的. 1.非压缩形式 如果没有进行压缩,则只有一次重定位,即从ROM到RAM只存在 ...
- JavaScript获取当前值
JavaScript获取当前值 1.说明 获取select下拉框中的选中的值以及文本值 2.实现源码 <!DOCTYPE html PUBLIC "-//W3C//DTD ...
- 检测dll是32/64位 ?
检测dll是32/64位 ? void CCheck32Or64Dlg::OnButton2() { CString fileName = ""; CFileDialog *fil ...
- hdu5556 Land of Farms
我对于题目的一种理解 改造农场 1.建新农场 在空的点选 2.重建旧农场 选一个点属于这个农场的地方都要选 最后的农场都不能相连 所以枚举旧农场的个数并进行二分图匹配 #include<bits ...
- java用Kruskal实现最小生成树
今天更新这篇文章超级激动,因为我会最小生成树的算法了(其实昨天就开始研究了,只是昨天参加牛客网的算法比赛,结果又被虐了,好难过~) 最小生成树的算法,其实学了数据结构就会有一定的基础,Kruskal算 ...
- window.load 和$(document).ready() 区别
1.执行时间 window.onload必须等到页面内包括图片的所有元素加载完毕后才能执行. $(document).ready()是DOM结构绘制完毕后就执行,不必等到加载完毕.2.编写个数不同 w ...
- ASP.NET CSS 小结
1.ASP.NET 引用CSS 1.Site.master里面设置webopt <webopt:bundlereferencerunat="server"path=" ...
- 求字符串空格、数字、字母个数--JAVA基础
相关内容:charAt()函数 package com.nxl123.www;public class NumString { public static void main(String[] arg ...
- 结合实例分析Android MVP的实现
最近阅读项目的源码,发现项目中有MVP的痕迹,但是自己却不能很好地理解相关的代码实现逻辑.主要原因是自己对于MVP的理解过于概念话,还没有真正操作过.本文打算分析一个MVP的简单实例,帮助自己更好的理 ...
- 【BZOJ2337】Xor和路径(高斯消元)
[BZOJ2337]Xor和路径(高斯消元) 题面 BZOJ 题解 我应该多学点套路: 对于xor之类的位运算,要想到每一位拆开算贡献 所以,对于每一位拆开来看 好了,既然是按位来算 我们就只需要计算 ...