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

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. swiper 应用

    swiper之PC端的广告页面[当前示例对应网站:http://shang.shuaishou.com/] plugins:[红线部分] html: <div class="banne ...

  2. ios开发-将false和true,当做字典的值,并将字典转成字符串,上传到服务器

    今天遇到一个需求,将false和true,当做字典的值,并将字典转成字符串,上传到服务器. 可能这个需求大家遇到过,大部分原因是安卓的同事已经按这样的需求开发完了.我们只能跟随安卓的脚步了. (一)处 ...

  3. 最快的3x3中值模糊

    10.1国庆后,知名博主:laviewpbt  http://www.cnblogs.com/Imageshop/ 发起了一个优化3x3中值模糊的小活动. 俺也参加其中,今天博主laviewpbt   ...

  4. HDU3311Dig The Wells

    给定N个寺庙,和M个另外的地方. 然后给定点权,表示在这个点挖水井需要的代价. 再给定边权,为建造无向边i,j的代价. 然后求怎样弄最小的代价使得前N个点,就是寺庙都能从挖的井里得到水. 输入输出格式 ...

  5. [Noi2016]区间

    题目描述 在数轴上有 n个闭区间 [l1,r1],[l2,r2],...,[ln,rn].现在要从中选出 m 个区间,使得这 m个区间共同包含至少一个位置.换句话说,就是使得存在一个 x,使得对于每一 ...

  6. [JSOI2007]祖码Zuma

    题目描述 这是一个流行在Jsoi的游戏,名称为祖玛. 精致细腻的背景,外加神秘的印加音乐衬托,彷佛置身在古老的国度里面,进行一个神秘的游戏——这就是著名的祖玛游戏.祖玛游戏的主角是一只石青蛙,石青蛙会 ...

  7. 51nod 1376 最长递增子序列的数量(线段树)

    51nod 1376 最长递增子序列的数量 数组A包含N个整数(可能包含相同的值).设S为A的子序列且S中的元素是递增的,则S为A的递增子序列.如果S的长度是所有递增子序列中最长的,则称S为A的最长递 ...

  8. hdu 5480(前缀和)

    题意:如果一个点,则这点的横竖皆被占领,询问矩阵是否全被占领. 思路:将被占领的x,y标记为1,用x表示1 - i的和 如果x轴的差为 x2 - x1 + 1则表示全被占领,y轴同理 #include ...

  9. 【CODEVS 6384 大米兔学全排列】

    ·大米兔学习全排列,还有一些逆序对,还有一棵二叉索引树.· ·分析:       首先肯定不是像题目上说的那样,使用next_permutation去完成这道题,因为就算是线性的它也不能承受庞大的排列 ...

  10. SpringCloud学习之SpringCloudStream&集成kafka

    一.关于Spring-Cloud-Stream Spring Cloud Stream本质上就是整合了Spring Boot和Spring Integration,实现了一套轻量级的消息驱动的微服务框 ...