安卓进阶指南Annotation自定义编译时注解(四)

曾经看过一篇使用运行时注解来实现类似 ButterKnife 功能的文章。直到后来我自己看了ButterKnife 源码后才发现并不是这样。推荐阅读 ButterKnife 原理解析,这是 Butterknife 源码地址,不妨 clone 下来看一看瞧一瞧。

ACBgkflEgD.png

代码地址:android-annotation-tutorial

反射是一个我们在运行时读取一个类及其成员属性,并尝试修改这些属性的过程。 这个过程虽然有助于创建一个通用或独立于实现的程序,但是由于我们不知道运行时的确切条件,因此也容易出现大量异常。 通过反射进行类扫描和修改是一个缓慢的过程,也是一种孤立代码的丑陋方式。

一、示例:

为了更好的理解编译时注解,我们先使用运行时注解来实现绑定控件

  • 定义注释BindView以进行映射
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
  • 将BindView注释放在具有视图ID的View类变量上
public class MainActivity extends AppCompatActivity {
    ...
    @BindView(R.id.txtView)
    TextView txtView;
    ...
}
  • 创建一个类,该类使用id tv_name将XML中定义的TextView对象赋值给变量tvName
public class ViewBinder {
    /*
     * annotations for activity class
     * @param target is the activity with annotations
     */
    public static void bind(final Activity target){
        bindViews(target, target.getClass().getDeclaredFields(),
    }

    /*
     * initiate the view for the annotated public fields
     * @param obj is any class instance with annotations
     * @param fields list of methods in the class with annotation
     * @param rootView is the inflated view from the XML
     */
    private static void bindViews(final Object obj, Field[] fields, View rootView){
        for(final Field field : fields) {
            Annotation annotation = field.getAnnotation(BindView.class);
            if (annotation != null) {
                BindView bindView = (BindView) annotation;
                int id = bindView.value();
                View view = rootView.findViewById(id);
                try {
                    field.setAccessible(true);
                    field.set(obj, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 将Activity实例发送到ViewBinder。
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.txtView)
    private TextView txtView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewBinder.bind(this);
        txtView.setText("Testing");
    }
    ...
}

这种方法能正常运行,但我们刚谈到反射的性能限制,使它变得不可取。
那么,我们怎样才能改进呢?

  • 我们必须消除MainActivity类的运行时扫描并将其替换为方法调用。
  • 我们不希望为每个Activity编写这些方法,并希望它们自动生成。
  • 我们希望消除任何运行时异常,并希望在编译期间移动此类检查。
    编译时注解能满足这些情况。

二、编译时注解如何工作?

编译时注解在编译周期中进行。 在每个循环遍历中,编译器在读取java源文件时找到注册用于处理的注释并调用相应的注释处理器。 如果在该循环中没有生成文件,则该循环继续生成任何文件或终止。

好吧。我们将学习过程分为四个部分:

  1. 为注释处理创建一个Android项目。
  2. 理解用于处理的注释的定义。
  3. 编写一个编译器模块,通过注释处理生成代码。
  4. 使用通过注释处理生成的代码。

三、项目结构

该项目有四个模块:

  1. app:这是Android应用程序项目。
  2. binder:此模块提供一个类,该类将给Activity带注释的视图对象和单击回调方法映射到XML视图。
  3. binder-annotations:此模块定义注释以便于视图和单击回调方法的映射。
  4. binder-compiler:此模块定义生成类以帮助上述映射的处理器。
  • 定义注释

BindView:它将视图引用映射到其XML定义。 示例:带有id tv_content的TextView将映射到变量tvContent。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value();
}

OnClick:它将映射一个方法,当单击具有提供的id的视图时将调用该方法。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface OnClick {
    @IdRes int value();
}

在这里你可以注意到@IdRes注释。 此注释由support-annotations库提供。

  • 创建注释处理器

方法一:

注释处理器循环运行并与应用程序编译并行运行。 在每个周期中,处理器都提供有关正在编译的应用程序源代码的信息。

处理器必须注册到编译器,以便在编译应用程序时可以运行它。 我们将看到如何定义这样的编译器。

现在,我们将创建一个类似于binder-annotations的Java库binder-compiler。 在这个模块中,我们将不得不创建目录结构:

binder-compiler/src/main/resources/META-INF/services

在services目录中,我们将创建一个名为javax.annotation.processing.Processor的文件。
此文件将列出编译器在注释处理时编译应用程序源代码时将调用的类。

还有一种方法是(我采用了这种)

使用 Google 提供的库 auto-service ,gradle 添加依赖

implementation 'com.google.auto.service:auto-service:1.0-rc2'

Processor 添加如下注解

@AutoService(javax.annotation.processing.Processor.class)
public class Processor extends AbstractProcessor {
...
}

所有注释处理器都继承AbstractProcessor,它定义了处理的基本方法。 我们将在此库中创建一个继承AbstractProcessor的类Processor。 我们必须覆盖三种方法来提供处理的实现。

  • init:这里我们将获得Filer,Messager和Elements。
  • process:调用此方法来处理应用程序的源代码。 在这里,我们将定义一个类并编写Java源代码。
  • getSupportedAnnotationTypes:它列出了我们在处理应用程序的Java文件时要查询的注释。

另外还要了解如下类:

  • Filer:它提供API来编写生成的源代码文件。
  • Messager:用于在编译时打印消息。 我们发送可能通过Messager处理的错误消息。 由于注释处理器在其自己的独立环境中运行,因此我们无法通过任何其他方式与应用程序通信。
  • Elements:它提供了utils方法,用于过滤处理器中不同类型的元素。
public class Processor extends AbstractProcessor {

    private Filer filer;
    private Messager messager;
    private Elements elementUtils;

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // all the magic happens in this block  
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return new TreeSet<>(Arrays.asList(
                BindView.class.getCanonicalName(),
                OnClick.class.getCanonicalName()
                ));
    }
}

四、 生成 Java 源代码

Note: 如果有些方法不明白可以参照 Android Studio中调试自定义AbstractProcessor方法 把程序跑起来,打断点看下就一目了然了。

现在我们提供Processor的流程方法的完整实现,并学习使用JavaPoet定义类及其成员。
注释处理在处理Java注释源代码时提供的内容:

  • Set<? extends TypeElement>:它提供注释列表作为正在处理的Java文件中包含的元素。
  • RoundEnvironment:它提供对处理环境的访问,其中包含查询元素的工具。 我们将在这个环境中使用的两个主要功能是:processingOver(它的最后一轮处理)和getRootElements(它提供了一个将被处理的元素列表。这些元素中将包含一些我们感兴趣的注释。)
    因此,我们有一组注释和一系列元素。 我们的库将生成一个包装类,它将帮助映射视图并单击活动的侦听器。

我们的注释将映射视图和按钮以删除样板,就像ButterKnife一样。

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_content)
    TextView tvContent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Binding.bind(this);
    }

    @OnClick(R.id.bt_1)
    void bt1Click(View v) {
        tvContent.setText("Button 1 Clicked");
    }

    @OnClick(R.id.bt_2)
    void bt2Click(View v) {
        tvContent.setText("Button 2 Clicked");
    }
}

我们将使用MainActivity定义使用注释处理自动生成名为MainActivity$Binding的包装器类。处理后,将创建以下类。编译的时候生成在该目录下:
[图片上传失败...(image-9b18a6-1536888483860)]
打开后看到如下内容:

public class MainActivity$Binding {
  public MainActivity$Binding(MainActivity activity) {
    bindViews(activity);
    bindOnClicks(activity);
  }

  private void bindViews(MainActivity activity) {
    activity.tvContent = (TextView)activity.findViewById(2131165322);
  }

  private void bindOnClicks(final MainActivity activity) {
    activity.findViewById(2131165218).setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        activity.bt1Click(view);
      }
    });
    activity.findViewById(2131165219).setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        activity.bt2Click(view);
      }
    });
  }
}

现在我们知道了我们必须生成什么,让我们分析如何使用我们在处理时可以使用的信息来创建它。

我们将首先过滤掉那些在getRootElements方法提供的元素列表中使用@BindView或@OnClick的类(Type)元素。
然后,我们将迭代这些过滤的元素,然后扫描其成员和方法,以使用JavaPoet开发包装类的类模式。 最后,我们将该类写入Java文件。希望使搜索更有效, 因此,我们将创建一个具有过滤方法的ProcessingUtils类。

public class ProcessingUtils {

    private ProcessingUtils() {
        // not to be instantiated in public
    }

    public static Set<TypeElement> getTypeElementsToProcess(Set<? extends Element> elements,
                                                            Set<? extends Element> supportedAnnotations) {
        Set<TypeElement> typeElements = new HashSet<>();
        for (Element element : elements) {
            if (element instanceof TypeElement) {
                boolean found = false;
                for (Element subElement : element.getEnclosedElements()) {
                    for (AnnotationMirror mirror : subElement.getAnnotationMirrors()) {
                        for (Element annotation : supportedAnnotations) {
                            if (mirror.getAnnotationType().asElement().equals(annotation)) {
                                typeElements.add((TypeElement) element);
                                found = true;
                                break;
                            }
                        }
                        if (found) break;
                    }
                    if (found) break;
                }
            }
        }
        return typeElements;
    }
}

这里有两件事需要我们理解:

  • element.getEnclosedElements():封闭元素是给定元素中包含的元素。 在我们的例子中,元素将是MainActivity(TypeElement),Enclosed元素将是tvContent,onCreate,bt1Click,bt2Click和其他继承的成员。
  • subElement.getAnnotationMirrors():它将提供subElement上使用的所有注释。
JavaPoet速成课程:

JavaPoet使得定义类结构并在处理时编写它非常简单。 它创建了非常接近手写代码的类。 它提供了自动推断导入以及美化代码的工具。当然也可以使用 JavaFileObject ,但是这个远古的笨重且不切实际的东西我们就不多说了,有兴趣的还是建议放弃这个兴趣,就使用 JavaPoet 吧。

要使用JavaPoet,我们需要将以下依赖项添加到binder-compiler模块中。

dependencies {
    implementation project(':binder-annotations')
    implementation 'com.squareup:javapoet:1.11.1'
}

本教程所需的JavaPoet的基本用法(可以从其 javapoet 获得任何提前了解。)

  • TypeSpec.Builder:定义类模式。
  • addModifiers(Modifier):添加private,public或protected关键字。
  • addAnnotation:向元素添加注释。 示例:@Override on methods或@Keep on class in case。
  • TypeSpec.Builder - > addMethod:向类添加方法。 示例:构造函数或其他方法。
  • MethodSpec - > addParameter:为方法添加参数类型及其名称。 示例:在我们的示例中,我们希望将具有变量名称活动的MainActivity类型传递给方法。
  • MethodSpec - > addStatement:它将在方法中添加代码块。 在这个方法中,我们首先定义语句的占位符,然后传递参数来映射那些占位符。 示例
addStatement("$N($N)", "bindViews", "activity") //这将生成代码bindViews(activity)

占位符:N -> names,T -> type(ClassName), $L -> literals(long etc.)
参考JavaPoet的基本介绍,可以很容易地理解其他内容。剩下的就是你自己去完成了。

「百闻不如一见」,百看不如一试。最后再说下源码地址吧:
android-annotation-tutorial

参考链接:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,709评论 2 59
  • 现在市面上很多框架都有使用到注解,比如butterknife库、EventBus库、Retrofit库等等。...
    tuacy阅读 5,557评论 1 15
  • 主持状态很给力,控制台值守不专业,主持训练不足,主持人数可以,致辞时大伙明显兴趣不足,致辞本身非常精彩,致辞提升了...
    李杰_1d6d阅读 72评论 0 0
  • 2018.7.27 星期五 天气:阴 读经进度:第8周第5天 读经人员:仁杰、妈妈 读经...
    翟露露阅读 149评论 0 0
  • 楼下有两家快餐店,一家卖米饭快餐,一家卖面。卖米饭快餐的那家是一年前开张的,当时因为离得近,又是老乡,所以常去他们...
    Zerof阅读 250评论 4 3