搭建Android客户端APP架构——《编译时APT技术》
背景
本人从事开发工作也有多年,目前坐标湖南长沙,以前在各种平台也发过一些文章但是都没有坚持下来;
我初步规划是写一个完整的项目系列文章期望能坚持下来。
为什么会想到要写呢?
其一、眨眼就到了而立之年,觉得自己记忆力也是下降久做过的东西总是记不起,果然是好记性不如烂笔头。
其二、这么多年白嫖了网上很多的文章,视频,一直觉得应该分享一些东西但总是沉不下心去做。
其三、可能写的不好至少也留下一些东西,也是希望能帮助到一些朋友。
说明
第一篇文章其实写好了很多天,然后一直怕自己不会去写就没发,最近在研究鸿蒙系统,也写了一个入门文章,在写鸿蒙系统的UI的时候上天保佑我又想起上篇文章结尾时写的编译时技术。
于是我花了一点时间自己写了一个简易的鸿蒙系统的系统的类似的ButterKnife插件,只实现了@BindView 和@OnClick,这两个比较常用的Annotation注解,写这篇文章主要想跟朋友们讲清楚什么是APT编译时技术,如何去做,期望其他的朋友可以做到举一反三在工作中用到。
什么是APT编译是技术?
回答这个问题前让我们来了解一下我们从开发到程序运行,编译器做了那些事情这点非常重要!
可能在刚刚一些Android程序员的世界里Android开发到运行就时时下面这个图
当然这没有错,因为编译的整个过程由编译器做了。
那么我们今天想要说的APT是什么呢?
这个问题实际上我们可以从下面这个简单的文件类型流转过程图来入手解答。
编译时技术是指的在开发后,在运行之前,通过某种技术手段改变现有的代码,用以达到某种程序业务逻辑实现,这个就是编译时技术!
采用这种编译时形式编程的有一个名字AOP:面向切面编程(Aspect-Oriented Programming)。
目前比较流行的编译时技术有APT,AspectJ,Javassist,asm
APT技术就是在编译时通过读取注解生成一些正常开发中可使用的文件,APT技术一般会在开发阶段需要书写较为重复繁琐的代码且开发时需要使用的时候使用。
ButterKnife的由来
实际上我们从开发Android的时候就会写大量的findViewById
或者setOnClickListener getString之类的代码。这些往往很多代码繁琐。为了减少开发者写这类重复没有太多意义的代码。于是就有了很多的库的实现。有些库用注解+反射实现但是对于性能有所影响,有些用编译器插件开发生成简单的findViewById生成但是功能单一,ButterKnife的作者这位为了考虑得这些问题写了功能超全,性能几乎无影响的ButterKnife。
理清思路
理清需要实现的功能并模拟写出代码
需要实现两个功能
1.通过@BindView注解让我们声明的控件变量不需要调用findViewById就可以直接调用。
模拟代码:
Button mBtnId=(android.widget.Button) findViewById(R.id.btn_id);
2.通过添加@OnClick注解点击事件声明一个方法可以不需要设置监听事件就可以点击访问到方法。
findViewById(R.id.btn_id).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
调用方法(v);
}
});
理清如何将注解和模拟代码结合起来实现只需要添加注解就可以使用或者调用
public class MainActivity extends AppCompatActivity {
@BindView(value = R.id.btn_id)
Button mBtnId;
@OnClick(value = {R.id.btn_id})
public void click(View view) {
Toast.makeText(this, "点击了", Toast.LENGTH_SHORT).show();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
mBtnId.setText("11111");
}
}
实际上我们最后需要实现的就是上述代码的效果。
在ButterKnife.bind(this);之后
声明@BindView(value = R.id.btn_id)注解的mBtnId的控件变量可以直接使用而不需要再findViewById。
声明@OnClick(value = {R.id.btn_id})的方法
不再需要setOnClickListener点击就可以直接访问。
那么ButterKnife.bind(this);究竟要实现什么才能实现这个效果呢?
当然如果我们使用反射,我们可以通过遍历获取到包含注解的列Field findViewById set给列和获取到方法Method ,findViewById 后set监听,在点击监听里invoke执行该Method。
我们这里使用APT技术请忽略上面这种方式!
ButterKnife.bind(this)我们需要通过传进去的参数找到APT生成的相应包含注解实现的类。
String className = object.getClass().getName() + "$ViewBinding";
try {
Class clazz = Class.forName(className);
Constructor constructor = clazz.getDeclaredConstructor(a.getClass());
constructor.newInstance(a);
} catch (Exception e) {
e.printStackTrace();
}
那么生成的类应该是怎么样实现呢?
public final class MainActivity$ViewBinding {
MainActivity$ViewBinding(final MainActivity target) {
target.mBtnId =(android.widget.Button) target.findViewById(注解的控件id);
target.findViewById(注解的控件id)
.setOnClickListener(new android.view.View.OnClickListener() {
@Override
public void onClick(android.view.View v) {
target.添加的注解方法(v);
}});
}
}
是的我们直接把类传进去直接赋值变量,设置监听就可以了!这个生成的类代码就是我们需要通过APT生成的
这样我们思路就理清楚了,只需要使用APT生成代码就可以了!
开始动手从0-1实现简单ButterKnife
创建注解模块
创建两个注解类BindView 和OnClick
BindView
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@Retention(SOURCE)
@Target(FIELD)
public @interface BindView {
int value();
}
OnClick
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Target(METHOD)
public @interface OnClick {
int[] value();
}
Retention 和 Target分别指注解有效期,和注解可添加的位置
创建butterknife调用模块
创建模块选择com.android.library
添加ButterKnife类只添加支持Activity和View
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Constructor;
public class ButterKnife {
public static void bind(Activity a) {
bindInstance(a);
}
public static void bind(View a) {
bindInstance(a);
}
public static void bindInstance(Object a) {
String className = a.getClass().getName() + "$ViewBinding";
try {
Class clazz = Class.forName(className);
Constructor constructor = clazz.getDeclaredConstructor(a.getClass());
Object o = constructor.newInstance(a);
o = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
创建APT生成模块butterknife-compiler
在build.gradle的dependencies下添加下面的包
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
implementation 'com.google.auto.service:auto-service:1.0-rc6'
implementation 'com.squareup:javapoet:1.10.0'
implementation project(path: ':butterknife-annotations')
(annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
implementation 'com.google.auto.service:auto-service:1.0-rc6')
这两个包用于自动生成服务
implementation 'com.squareup:javapoet:1.10.0'
用于构建生成Java文件当然也可以不引用直接用拼接的方式生成文件。
implementation project(path: ':butterknife-annotations')
引入要用到的注解
创建ButterKnifeProcessor类继承 AbstractProcessor
重写以下几个方法
主要是输入ProcessingEnvironment参数
@Override
public synchronized void init(ProcessingEnvironment processingEnv)
主要用于处理生成文件的方法
@Override
public boolean process(Set<? extends TypeElement> typeElements, RoundEnvironment env)
用于返回支持的源码版本
@Override
public SourceVersion getSupportedSourceVersion()
用于返回支持的注解类别
@Override
public Set<String> getSupportedAnnotationTypes()
public synchronized void init(ProcessingEnvironment processingEnv)
这个方法可以获取ElementUtils ,Filer用于生成文件
mElementUtils = processingEnv.getElementUtils();
mFiler = processingEnv.getFiler();
下面实现process和getSupportedAnnotationTypes两个方法
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindView.class);
annotations.add(OnClick.class);
return annotations;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
process方法这里不直接Copy讲解代码,直接用伪代码讲讲如何分类文件注解,如何生成文件
如何分类文件的注解Element
一般用如下步骤:
1.通过RoundEnvironment.getElementsAnnotatedWith(注解类)
获取到该注解类的Element列表
2.通过Element.getEnclosingElement()方法获取所属的类名做区分
3.定义一个Map 以类名的Element作为Key 以同文件的Element添加列表作为Value
如何生成文件
上一步获取到了Map,通过遍历Map 同一Key就是可以认为就是同一个类中的注解。
然后遍历Map就可以根据自己的需要生成相应的文件了。
我们这里使用javapoet生成相应的JAVA代码具体代码就不贴了。
有兴趣的话可以到我的github里面去找。
在项目里使用
在apply plugin: 'com.android.application’模块里dependencies
添加如下引入
implementation project(path: ':butterknife')
implementation project(path: ':butterknife-annotations')
annotationProcessor project(path: ':butterknife-compiler')
点击运行或者执行assembleDebug就可以调用butterknife-compiler里的 public boolean process(Set<? extends TypeElement> typeElements, RoundEnvironment env) 方法进行生成你想要的文件
如果有些不懂得地方联合《鸿蒙系统APT尝试》进行查看
链接: 鸿蒙系统APT尝试
链接: Javapoet说明
源码
链接: Android 项目源码地址
链接: JAVA后端项目源码地址
吐槽
//TODO 给我感觉写代码远比写文章要轻松....致敬所有写文分享的人
这篇文章主要写了一些简单的 APT的大概使用方式,如有不懂之处或者写得不甚明了的地方可以留言。