欢迎转载,转载请标明出处:

http://blog.csdn.net/johnny901114/article/details/52672188

本文出自:【余志强的博客】

一、概述

上一篇博客讲了,如何在android studio使用apt 《 Android开发之手把手教你写ButterKnife框架(二)》

然后在Processor里生成自己的代码,把要输出的类,通过StringBuilder拼接字符串,然后输出。

    try { // write the file
        JavaFileObject source = processingEnv.getFiler().createSourceFile("com.chiclaim.processor.generated.GeneratedClass");
        Writer writer = source.openWriter();
        writer.write(builder.toString());
        writer.flush();
        writer.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

输出简单的类这种方法还是挺好的,简单明了,如果要输出复杂点的java文件,这个就不是很方便了,接下来介绍一个square公司开源的框架javapoet来帮助我们构建java文件。

二、 javapoet简单使用

通过MethodSpec类来构建java方法,如:

MethodSpec main = MethodSpec.methodBuilder("main")                       //方法名
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)                      //方法修饰符
    .returns(void.class)                                                 //方法返回类型
    .addParameter(String[].class, "args")                                //方法参数
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//方法体语句
    .build();

通过TypeSpec来构建java类的修饰符

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")  //类名
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)         //类的修饰符
    .addMethod(main)                                       添加类方法(MethodSpec main)
    .build();

通过JavaFile输出类

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();
javaFile.writeTo(System.out);

通过CodeBlock来构建方法体

上面通过MethodSpec.addaddParameter(String[].class, “args”)方法构建方法体,对于比较复杂的可以通过CodeBlock来构建:

codeBlock.addStatement("$L.inject(this, this)", mViewInjectorName);

javapoet就先介绍到这里。更多具体的使用可以查看官方文档或者其他资料。

三、通过javapoet生成Bind类

在ButterKnifeProcessor process方法中获取基本信息

我们要想达到在生成的类中初始化activity Views,那么肯定需要如下类似下面的代码(伪代码):

public class MainActivity_Binding {

    public MainActivity_Binding(MainActivity target,View view) {
        target.text1 = (TextView)view.findViewById(id);
        target.text2 = (TextView)view.findViewById(id);
        target.text3 = (TextView)view.findViewById(id);
        target.text4 = (TextView)view.findViewById(id);
    }
}

据此,我们需要在ButterKnifeProcessor process方法里获取三个基本信息:

1、注解所在的类,用于生成类名,比如MainActivity使用了注解,那么生成的类就是 MainActivity_ViewBinding

2、注解的值,用于findViewById,如:

    @BindView(R.id.title)
    TextView title;

那么我们要获取的值就是R.id.title

3、注解所在字段的类型,用于强转。如:

    @BindView(R.id.title)
    TextView title;

那么我们要获取的类型就是TextView

通过下面的方法可以获取上面的信息

【element.getEnclosingElement()】 //注解所在的类

【element.getAnnotation(BindView.class).value()】 //注解上的值, 用于findViewById

【element.asType()】 //注解字段的类型,用于强转

通过上一篇博客知道,我们是在process方法里生成代码的:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //TODO do something
    return true;
}

对RoundEnvironment里的信息进行分组处理

所有关于类上的注解信息,全部在 RoundEnvironment roundEnv里,而且可能的多个类用到了注解, 所以我们要对RoundEnvironment的信息进行分组处理。

我通过Map来保存分组的信息,

Map

//roundEnv里的信息进行分组
private void parseRoundEnvironment(RoundEnvironment roundEnv) {
        // 保存分组信息
        Map<TypeElement, BindClass> map = new LinkedHashMap<>();
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            //注解的值
            int annotationValue = element.getAnnotation(BindView.class).value();
            //如果不存在创建BindClass,要创建的代码都存在BindClass里
            BindClass bindClass = map.get(enclosingElement);
            if (bindClass == null) {
                bindClass = BindClass.createBindClass(enclosingElement);
                map.put(enclosingElement, bindClass);
            }
            String name = element.getSimpleName().toString();
            TypeName type = TypeName.get(element.asType());
            //ViewBinding用于保存每个注解的相关信息(比如注解所在字段的名称、注解所在字段的类型、注解上的值,)
            ViewBinding viewBinding = ViewBinding.createViewBind(name, type, annotationValue);
            //因为一个类上可能多处用了注解,所以用一个集合保存
            bindClass.addAnnotationField(viewBinding);
        }

        //迭代分组后的信息,主义生成对应的类
        for (Map.Entry<TypeElement, BindClass> entry : map.entrySet()) {
            printValue("==========" + entry.getValue().getBindingClassName());
            try {
                entry.getValue().preJavaFile().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

ViewBinding用于保存每个注解的相关信息,代码也很简单:

class ViewBinding {

    private final String name;
    private final TypeName type;
    private final int value;

    private ViewBinding(String name, TypeName type, int value) {
        this.name = name;
        this.type = type;
        this.value = value;
    }

    static ViewBinding createViewBind(String name, TypeName type, int value) {
        return new ViewBinding(name, type, value);
    }
}

BindClass用于保存需要生成的代码,里面封装了javapoet相关处理,所有具有生成代码的功能.

先来看看创建BindClass构造方法:

    private BindClass(TypeElement enclosingElement) {
        //asType 表示注解所在字段是什么类型(eg. Button TextView)
        TypeName targetType = TypeName.get(enclosingElement.asType());
        if (targetType instanceof ParameterizedTypeName) {
            targetType = ((ParameterizedTypeName) targetType).rawType;
        }
        //注解所在类名(包括包名)
        String packageName = enclosingElement.getQualifiedName().toString();
        packageName = packageName.substring(0, packageName.lastIndexOf("."));
        String className = enclosingElement.getSimpleName().toString();
        //我们要生成的类的类名
        ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
        boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
        //注解所在类,在生成的类中,用于调用findViewById
        this.targetTypeName = targetType;
        this.bindingClassName = bindingClassName;
        //生成的类是否是final
        this.isFinal = isFinal;
        //用于保存多个注解的信息
        fields = new ArrayList<>();
    }

添加注解信息实体

    void addAnnotationField(ViewBinding viewBinding) {
        fields.add(viewBinding);
    }

生成类的修饰符,方法:

    private TypeSpec createTypeSpec() {
        TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(PUBLIC);
        if (isFinal) {
            result.addModifiers(FINAL);
        }

        result.addMethod(createConstructor(targetTypeName));

        return result.build();
    }

创建构造方法,在构造方法里生成初始化View的代码:

    private MethodSpec createConstructor(TypeName targetType) {
        MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                .addModifiers(PUBLIC);
        //构造方法有两个参数,target和source,在本例子中,Target就是activity,source就是activity的DecorView
        constructor.addParameter(targetType, "target", FINAL);
        constructor.addParameter(VIEW, "source");
        //可能有多个View需要初始化,也就是说activity中多个字段用到了注解
        for (ViewBinding bindings : fields) {
            //生成方法里的语句,也就是方法体
            addViewBinding(constructor, bindings);
        }

        return constructor.build();
    }

下面看看如何为activity中每个用到注解的View在构造方法中生成初始化代码:

    private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
        //通过CodeBlock生成语句,因为生成的语句比较复杂。
        CodeBlock.Builder builder = CodeBlock.builder()
                .add("target.$L = ", binding.getName());
        //判断是否需要强制类型转换,如果目标View本来就是View,那就不需要强转了
        boolean requiresCast = requiresCast(binding.getType());
        if (!requiresCast) {
            builder.add("source.findViewById($L)", binding.getValue());
        } else {
            //我们使用ProcessorUtils重点工具方法findViewByCast进行强转 $T就是一个占位符,UTILS就是ClassName包含了UTILS的包名和类名
            //用ProcessorUtils替换成$T CodeBlock还支持很多占位符,需要了解更多可以去看看文档.
            builder.add("$T.findViewByCast", UTILS);
            //ProcessorUtils.findViewByCast需要的参数source就是DecorView
            builder.add("(source, $L", binding.getValue());
            //ProcessorUtils.findViewByCast需要的参数$T.class,就是目标View需要强转的类型
            builder.add(", $T.class", binding.getRawType());
            builder.add(")");
        }
        result.addStatement("$L", builder.build());

    }

下面就是强转用到的工具类:

public class ProcessorUtils {

    public static <T> T findViewByCast(View source, @IdRes int id, Class<T> cls) {
        View view = source.findViewById(id);
        return castView(view, id, cls);
    }

    private static <T> T castView(View view, @IdRes int id, Class<T> cls) {
        try {
            return cls.cast(view);
        } catch (ClassCastException e) {
            //提示使用者类型转换异常
            throw new IllegalStateException(view.getClass().getName() + "不能强转成" + cls.getName());
        }
    }

}

注意, 如果你需要调试public boolean process(Set

// Generated code from My Butter Knife. Do not modify!!!
package com.chiclaim.sample;

import android.view.View;
import android.widget.TextView;
import com.chiclaim.butterknife.ProcessorUtils;

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    target.textView = ProcessorUtils.findViewByCast(source, 2131427414, TextView.class);
    target.view = ProcessorUtils.findViewByCast(source, 2131427415, TextView.class);
  }
}

接下来就简单了,在MainActivity中调用MainActivity_ViewBinding的构造方法就可以了。因为我们生成的类是有规律的,包名就是使用者的包名,类名是使用者类名加ViewBinding。然后通过反射调用下就可以了:

public class MyButterKnife {
    public static void bind(Activity activity) {
        //获取activity的decorView
        View view = activity.getWindow().getDecorView();
        String qualifiedName = activity.getClass().getName();

        //找到该activity对应的Bind类
        String generateClass = qualifiedName + "_ViewBinding";
        try {
            //然后调用Bind类的构造方法,从而完成activity里view的初始化
            Class.forName(generateClass)
                    .getConstructor(activity.getClass(), View.class).newInstance(activity, view);
        } 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();
        }
    }
}

所以只需要在跟butterknife一样在activity的onCreate声明周期方法里调用bind方法即可,如下所示:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text_view)
    TextView textView;

    @BindView(R.id.view)
    TextView view;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //完成初始化操作
        MyButterKnife.bind(this);

        Toast.makeText(this, textView + "--textView", Toast.LENGTH_LONG).show();
        Log.d("MainActivity", textView + "," + view);

        textView.setText("initialed by my butter knife");
    }
}

四、总结

1> butterknife 是一个运行时依赖祝框架,简化android的大量模板代码,使用apt来生成代码

2> 像很多框架都是跟butterKnife的机制太不多的,比如下面几款流行的框架:

greendao 流行的sqlite框架

dagger2 依赖注入框架

PermissionsDispatcher 处理Android6.0权限的框架

所以利用这个技术,也可以整个自己的框架。

更多实现信息,可以查看github上的源码 https://github.com/chiclaim/study-butterknife

Android开发之手把手教你写ButterKnife框架(三)的更多相关文章

  1. Android开发之手把手教你写ButterKnife框架(二)

    欢迎转载,转载请标明出处: http://blog.csdn.net/johnny901114/article/details/52664112 本文出自:[余志强的博客] 上一篇博客Android开 ...

  2. Android开发之手把手教你写ButterKnife框架(一)

    欢迎转载,转载请标明出处: http://blog.csdn.net/johnny901114/article/details/52662376 本文出自:[余志强的博客] 一.概述 JakeWhar ...

  3. 手把手教你写DI_1_DI框架有什么?

    DI框架有什么? 在上一节:手把手教你写DI_0_DI是什么? 我们已经理解DI是什么 接下来我们就徒手撸一撸,玩个支持构造函数注入的DI出来 首先我们回顾一下 构造函数注入 的代码形式, 大概长这模 ...

  4. 手把手教你写DI_2_小白徒手撸构造函数注入

    小白徒手撸构造函数注入 在上一节:手把手教你写DI_1_DI框架有什么? 我们已经知道我们要撸哪些东西了 那么我们开始动工吧,这里呢,我们找小白同学来表演下 小白同学 :我们先定义一下我们的广告招聘纸 ...

  5. 手把手教你写DI_0_DI是什么?

    DI是什么? Dependency Injection 常常简称为:DI. 它是实现控制反转(Inversion of Control – IoC)的一个模式. fowler 大大大神 "几 ...

  6. 网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接

    本文原作者:“水晶虾饺”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.引言 好多小白初次接触即时通讯(比如:IM或者消息推送应用)时,总是不 ...

  7. 只有20行Javascript代码!手把手教你写一个页面模板引擎

    http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...

  8. 手把手教你写Sublime中的Snippet

    手把手教你写Sublime中的Snippet Sublime Text号称最性感的编辑器, 并且越来越多人使用, 美观, 高效 关于如何使用Sublime text可以参考我的另一篇文章, 相信你会喜 ...

  9. 手把手教你写LKM rookit! 之 第一个lkm程序及模块隐藏(一)

    唉,一开始在纠结起个什么名字,感觉名字常常的很装逼,于是起了个这<手把手教你写LKM rookit> 我觉得: 你们觉得:...... 开始之前,我们先来理解一句话:一切的操作都是系统调用 ...

随机推荐

  1. Glide v4版本用法探究.md

    一基本介绍 本博客是基于Glide4.0+进行探究和学习 使用配置 用法比对 二使用配置 1. Android studio 使用项目gradle配置 dependencies { //glide c ...

  2. Python中的上下文管理器和with语句

    Python2.5之后引入了上下文管理器(context manager),算是Python的黑魔法之一,它用于规定某个对象的使用范围.本文是针对于该功能的思考总结. 为什么需要上下文管理器? 首先, ...

  3. Xshell5下利用sftp上传下载传输文件

    sftp是Secure File Transfer Protocol的缩写,安全文件传送协议.可以为传输文件提供一种安全的加密方法.sftp 与 ftp 有着几乎一样的语法和功能.SFTP 为 SSH ...

  4. [python]使用django快速生成自己的博客小站,含详细部署方法

    前言 人生苦短,我用python 这是之前经常听到的一句笑谈.因为新公司很多业务是用的python语言,所以这几天也一直在学习python的一些东西. 作为一个之前一直java后端的开发人员,对比ja ...

  5. 谈一谈泛型(Generic)

    谈一谈泛型 首先,泛型是C#2出现的.这也是C#2一个重要的新特性.泛型的好处之一就是在编译时执行更多的检查. 泛型类型和类型参数 ​ 泛型的两种形式:泛型类型( 包括类.接口.委托和结构 没有泛型枚 ...

  6. POJ2454 Jersey Politics

    Description In the newest census of Jersey Cows and Holstein Cows, Wisconsin cows have earned three ...

  7. codeforces 815C Karen and Supermarket

    On the way home, Karen decided to stop by the supermarket to buy some groceries. She needs to buy a ...

  8. ●BZOJ 3561 DZY Loves Math VI

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=3561 题解: 莫比乌斯反演 $$\begin{aligned}ANS&=\sum_{ ...

  9. ●HDU 6021 MG loves string

    题链: http://acm.hdu.edu.cn/showproblem.php?pid=6021 题解: 题意:对于一个长度为 N的由小写英文字母构成的随机字符串,当它进行一次变换,所有字符 i ...

  10. [bzoj5016][Snoi2017]一个简单的询问

    来自FallDream的博客,未经允许,请勿转载,谢谢. 给你一个长度为N的序列ai,1≤i≤N和q组询问,每组询问读入l1,r1,l2,r2,需输出   get(l,r,x)表示计算区间[l,r]中 ...