Android编译时注解框架6-APT的优缺点与应用

转载请注明出处:https://lizhaoxuan.github.io

概述

如果你已经读完了前面的5章博客,相信你已经对APT整体已经比较熟悉了,所以,APT真的很简单对嘛?

但就像我前面提到过的,APT是一套非常强大的机制,它唯一的限制在于你天马行空的设计!

APT有着非常简单的技术实现,但其应用场景却着实有点尴尬。我一直期望可以探索出ButterKnife和EventBus以外的应用场景,却始终未能如愿。姑且把我目前总结的成果列举,共勉~

本系列所讲APT均泛指编译时注解+代码生成,虽然运行时注解也属APT)

APT优点

  • 对代码进行标记,在编译时收集信息,并做处理。

  • 生成一套独立代码,辅助代码运行

  • 生成代码位置的可控性(可以在任意包位置生成代码),与原有代码的关联性更为紧密方便

  • 更为可靠的自动代码生成

  • 自动生成的代码可以最大程度的简单粗暴,在不必考虑编写效率的情况下提高运行效率

APT缺点

  • APT往往容易被误解可以实现代码插入,然而事实是并不可以

  • APT可以自动生成代码,但在运行时却需要主动调用

  • 与GreenDao不同,GreenDao代码生成于app目录下,可以在编写时调用并修改。APT代码生成于Build目录,只能在运行时通过接口等方式进行操作。这意味着生成的代码必须要有一套固定的模板

APT容易被你忽视的点

一个非常容易被你误解的点:只有被注解标记了的类或方法等,才可以被处理或收集信息。或者这样说,想要收集一些信息,只能先用注解修饰它。

产生这样误解容易引起一个问题:你可能会觉得一个需要大量注解的框架体验不好而决定放弃。

事实是怎么样呢?想一下同源的运行时注解+反射。反射可以通过一个类名便获取一个类的所有信息(方法、属性、方法参数等等等)。编译时注解也是可以的。当你修饰一个类时,可以通过类的Element获得类的属性和方法的Element,通过属性的Element可以获得属性所属类的信息,通过方法的Element可以获得所属类和其参数的信息。

说白了,编译时注解你也完全可以当反射来理解。

APT的优缺点都非常明显,优点足够了,缺点也不致命,只是让你在设计你的框架,选择技术方案时注意就好了。那么基于上面列出的几点,几个通用的应用场景就可以被设想了~一定要放大你的脑洞!!!

应用场景-信息收集与统计

注解的主要作用就是用于标记,所以最基础的应用就是信息收集与统计。可能你还是有点懵懵懂懂,没关系,举例子嘛~

编译时代码检查或统计

统计可能会一点奇怪:看看我这次写了多少个方法呀,多少个类呀。回头可以给BOSS说一下,以后KPI用方法数来计算?抱歉我的脑洞也就这样了,你再扩展一下~

//示例代码  类统计
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    Messager messager = processingEnv.getMessager();
    int size = env.getElementsAnnotatedWith(GetMsg.class).size();
    messager.printMessage(Diagnostic.Kind.NOTE,
            "Annotation class size = " + size);
}

代码检查就比较靠谱一点了:类名是不是首字母大写的驼峰式啊?方法名有没有问题呀?常量是不是全大写啊?

这里你可能比较好奇,我怎么检查啊,难道要给每个类都加一个注解嘛?不不不,你看刚刚我们才讲了:【APT容易被你忽视的点】,只需要一个就够了~

(此处有点瞎扯淡了,一般代码检查都不会这么干)

运行时数据收集与统计

通常来说,最容易想到的一个应用方向就是生成一个类似于字符串到类的对应Map结构。

手写代码容易出错,交给APT来实现便可以将错误率降到最低。

另外还有一个灵感来源于一个你不陌生的类:BuildConfig.

在BuildConfig中存放着一些静态属性,而这些静态属性值是Grandle编译时赋予的。可能这里你最常用的就是 BuildConfig.DEBUG了。

同理,APT也可以实现这样的功能。

应用场景-事件代理

此类应用场景的标志框架是ButterKnife。通过生成的代码代理实现View绑定。

//示例代码
@Override
public void bind(final Finder finder, final T target, Object source) {
    //定义了一个View对象引用,这个对象引用被重复使用了(这可是一个偷懒的写法哦~)
    View view;

    //暂时不管Finder是个什么东西,反正就是一种类似于findViewById的操作。
    view = finder.findRequiredView(source, 2131558541, "field 'accountEdit'");

    //target就是我们的ForgetActivity,为ForgetActivity中的accountEdit赋值
    target.accountEdit = finder.castView(view, 2131558541, "field 'accountEdit'");

    view = finder.findRequiredView(source, 2131558543, "field 'forgetBtn' and method 'forgetOnClick'");
    target.forgetBtn = finder.castView(view, 2131558543, "field 'forgetBtn'");

    //给view设置一个点击事件
    view.setOnClickListener(
        new butterknife.internal.DebouncingOnClickListener() {
            @Override
            public void doClick(android.view.View p0) {

                //forgetOnClick()就是我们在ForgetActivity中写得事件方法。
               target.forgetOnClick();

            }
        });
}

ButterKnife扩展

ButterKnife绑定View的同时,我们也可以附加一些操作。

一个典型案例就是 前面博客提到的OnceClick

在给View设置监听事件时,添加一些自定操作。

view = finder.findViewById(source, 2131492945);
    if (view != null) {
        view.setOnClickListener(new View.OnClickListener() {
            long time = 0L;
            @Override
            public void onClick(View v) {
                long temp = System.currentTimeMillis();
                if (temp - time >= intervalTime) {
                    time = temp;
                    target.once();
                }
            }
        });
    }

其他属性(跨域)初始化\赋值

ButterKnife的核心便是View的初始化操作,View可以初始化,其他对象的初始化当然也不在话下。

举一个列子:

Intent不能传输过大的数据量,那么在跳转Activity时有这大数据量传输的需求怎么办? APT遍可以解决。其核心原理通ButterKnife相同。

ActivityA向ActivityB的代理类ProxyB赋值,ProxyB初始化ActivityB的属性。

应用场景-代理执行 or “代码插入”

虽然前面有说过APT并不能像Aspectj一样实现代码插入,但是可以使用某种变种方式实现,就是使用上怪怪的。

####代理执行

用注解修饰一系列方法,由APT来代理执行。

此部分参考CakeRun

public class CrashApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //初始化APT框架,由Apt代理类来调用下列init方法,并在其中做些处理
        //某种程度实现了代码插入
        CakeRun.getInstance().applicationInit();
    }

    @AppInit(tag = 1, canSkip = true)
    protected void init1() {
        Log.d("TAG", "init1()  将引起crash。非关键路径可以跳过");
        String s = null;
        Log.d("TAG", s);
    }

    @AsyncInit(tag = 2, packageName = {"com.lizhaoxuan.cakerundemo.Lib1", "other packageName"})
    protected void init2() {
        Log.d("TAG", "AsyncInit2() 引起Crash ,关键路径不可跳过");
        Lib1.AsyncInit();
    }

    @AppInit(tag = 3)
    protected void init3() {
        Log.d("TAG", "init3() 未引起crash");
    }

    @AppInit(tag = 4)
    protected void init4() {
        Log.d("TAG", "init4() 未引起crash");
    }
}

APT生成的代理类按照一定次序依次执行修饰了注解的初始化方法,并且在其中增加了一些逻辑判断,来决定是否要执行这个方法。从而绕过发生Crash的类。

代码插入AOP

使用APT实现AOP

因为APT限制,通过click事件做切面,是最简单的,就是我们上面讲的 OnceClick

@CrashClick(id = R.id.btn, target = HomeActivity)
public void startActivity() {
      ...
    this.startActivity(intent);
}

但对于普通方法,可能就需要这样调用

protected void onCreate(Bundle savedInstanceState) {

    //...

    //原本是这样调用方法的
    startHomeActivity();

    //使用了APT,需要在调用方法时插入一些逻辑,比如做AOP切面
    //就需要这样调用
    AptClient.doMethod(this,"startHomeActivity");

}

@DemoTest(method = "startHomeActivity")
protected void startHomeActivity() {

}

APT生成代码样式:

public void doMethod(Object target){
    int temp;
    if(//一些逻辑条件){
        //执行前做一些操作,比如记录
        temp = 2; 
        //执行真正的方法
        target.startHomeActivity();
    }else{
        //这个方法有些问题不能执行
    }
}

应用场景-反射优化

编译时注解与反射异曲同工,只不过反射是在运行时获取类信息,编译时注解是在编译时获取类信息。所以反射可以做到事情,APT也是可以做到的。

EventBus优化

EventBus效率的桎梏点在于需要通过反射遍历类中的Event接收方法,虽然做了缓存优化,但对效率的影响还是比较严重的。如果使用APT进行优化,EventBus最大的缺点就被解决了。

APT优化:

  • 使用编译时注解标记Event接收方法。

  • 通过APT+代码生成,生成对应代理类,并提取所有Event接收方法

  • 每次注册不在需要在原本的类里寻找Event接收方法,而是直接注册代理类。

应用场景-让代码返璞归真

实际项目开发中,往往为了提高开发效率,会牺牲一点性能。最简单的例子就是运行时注解的大量使用。

运行时注解的大量使用减少了很多代码的编写,但谁都知道这是有性能损耗的。不过权衡利弊下,我们选择了妥协。

以ORM数据库框架为例。

细数目前Android主流的数据库框架:GreenDao、OrmLite、Active Android 。

OrmLite、Active Android均使用了运行时注解作为辅助从而实现了ORM。极大地简化了数据库操作,在使用上是非常轻松便捷的。但也因为使用运行时注解,用到了反射,导致了数据库操作性能的下降。

而作为数据库操作速度最快的GreenDao,它的原理是通过java工程替我们在项目中写了一套代码,一套返璞归真的数据库操作代码。没有反射的影响,采用最普通的方式操作数据库,它的速度是最快的!

但缺点是GreenDao的使用太奇葩了……导致初学GreenDao很痛苦。这是一个很致命的缺点。

那么通过APT,则是一个很好的技术方案:CakeDao

https://github.com/lizhaoxuan/CakeDao

与GreenDao同理,自动生成最普通的数据库操作代码,从而提高数据库操作效率。但因为APT是在编译时自动进行的,所以他的学习成本是非常小的。