搭建Android客户端APP架构——《编译时APT技术》

背景

本人从事开发工作也有多年,目前坐标湖南长沙,以前在各种平台也发过一些文章但是都没有坚持下来;
我初步规划是写一个完整的项目系列文章期望能坚持下来。
为什么会想到要写呢?
其一、眨眼就到了而立之年,觉得自己记忆力也是下降久做过的东西总是记不起,果然是好记性不如烂笔头。
其二、这么多年白嫖了网上很多的文章,视频,一直觉得应该分享一些东西但总是沉不下心去做。
其三、可能写的不好至少也留下一些东西,也是希望能帮助到一些朋友。

说明

第一篇文章其实写好了很多天,然后一直怕自己不会去写就没发,最近在研究鸿蒙系统,也写了一个入门文章,在写鸿蒙系统的UI的时候上天保佑我又想起上篇文章结尾时写的编译时技术。
于是我花了一点时间自己写了一个简易的鸿蒙系统的系统的类似的ButterKnife插件,只实现了@BindView 和@OnClick,这两个比较常用的Annotation注解,写这篇文章主要想跟朋友们讲清楚什么是APT编译时技术,如何去做,期望其他的朋友可以做到举一反三在工作中用到。

什么是APT编译是技术?

回答这个问题前让我们来了解一下我们从开发到程序运行,编译器做了那些事情这点非常重要!
可能在刚刚一些Android程序员的世界里Android开发到运行就时时下面这个图

编译
安装
源代码
APK
运行

当然这没有错,因为编译的整个过程由编译器做了。
那么我们今天想要说的APT是什么呢?
这个问题实际上我们可以从下面这个简单的文件类型流转过程图来入手解答。

生成
生成
A.java
B.class
C.dex

编译时技术是指的在开发后,在运行之前,通过某种技术手段改变现有的代码,用以达到某种程序业务逻辑实现,这个就是编译时技术!
采用这种编译时形式编程的有一个名字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的大概使用方式,如有不懂之处或者写得不甚明了的地方可以留言。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值