ButterKnife源码分析及简单实现

现在的IOC注解框架层出不穷,选择的余地非常多,我就用过Xutils,Buttknife,以及自己写的IOC框架,这些框架的好处在于:在Android系统的View注入方面,可以减少大量的findViewById以及setOnClickListener的代码,代码一键化生成。今天我们要说的是JakeWharton的ButterKnife,对ButterKnife不熟悉的请先去这里, 点击打开链接
简单使用步骤:

1:首先在gradlew目录下添加依赖,如下:

compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

2:到插件库里面安装插件File->Settings->Plugins->Browse repositories


简单使用,布局文件很简单就一个Button:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="mjoys.com.butterknife.MainActivity">

    <Button
        android:id="@+id/goto_login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

如果我们按照通常的写法,是这样的

 Button gotoLogin = findViewById(R.id.goto_login);

我们在MainActivity中使用ButterKnife是这样的:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.goto_login)
    Button gotoLogin;
    private Unbinder mUnbinder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mUnbinder = ButterKnife.bind(this);

        gotoLogin.setText("ButterKnife");

    }

    @Override
    protected void onDestroy() {
        mUnbinder.unbind();
        super.onDestroy();
    }
}

那么思考这样一个问题,为什么这样写就可以代替我们的findViewBid去找到这个Button控件呢?原来在我们编译项目的时候,动态的生成了这样的一个类MainActivity_ViewBinding,他实现了Unbinder这个类:

public class MainActivity_ViewBinding implements Unbinder {
  //持有MainActivity的引用
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;
    //代替findViewById的操作
    target.gotoLogin = Utils.findRequiredViewAsType(source, R.id.goto_login, "field 'gotoLogin'", Button.class);
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.gotoLogin = null;
  }
}

那么既然是动态的生成的MainActivity_ViewBinding类,如果我们不去动态生成该类,而是自己去写这样的一个类,能不能也达到这样的效果呢?我们新建一个ActivityLogin的类,继承自AppCompatActivity,布局很简单,就是一个TextView的文字显示,我们再根据MainActivity生成的是MainActivity_ViewBinding这个类这样的规则,去新建一个名字叫做ActivityLogin_ViewBinding的类:

public final class ActivityLogin_ViewBinding {

    private ActivityLogin target;


    public ActivityLogin_ViewBinding(ActivityLogin target) {
        this.target = target;
        target.text = target.findViewById(R.id.text);
    }

    public void unbind() {
        ActivityLogin target = this.target;
        if (target == null) throw new IllegalStateException("Bindings already cleared.");
        this.target = null;

        target.text = null;
    }
}

当然我们现在这样写的代码只是用来理解我们的ButterKnife动态生成代码的一个过程,不是实际中真正使用的代码。算作是伪代码吧。我们再看看使用情况:

public class ActivityLogin extends AppCompatActivity {

    TextView text;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        ActivityLogin_ViewBinding viewBinding = new ActivityLogin_ViewBinding(this);
        text.setText("仿照ButterKnife");
    }
}

同样的我们依然能否达到一样的效果。好了,我们的伪代码就写到这里了,那么我们来仿照ButterKnife来一下。首先我们我们需要再AS中新建两个java工程,一个用来编写我们的注解,一个用来在编译的生成我们的代码,类似于xxxx_ViewBinding.java文件。还需要一个Android Module,是我们自己的Butterknife辅助类。

我们用as新建一个java module步骤:


新建一个Android Library就不用再多说了,整体工程的目录如下:


这里我们先写一个绑定控件的注解,其他注解原理都是一差不多的。

在annotations工程下新建一个BindView注解:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)//编译时注解
public @interface BindView {
    int value();
}

如果在工程中使用了汉字注释,有可能工程在编译的工程会报错,解决办法是在相应的module的build.gradle下加上:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

tasks.withType(JavaCompile){
    options.encoding = "UTF-8"//防止注解含有中文的时候,编译报错
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

生成代码的一个类,我们先配置一下他的build.gradle文件,compiler module下的gradle文件配置:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    //该库可以更好地辅助我们完成自定义注解处理器的设计
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    //该库用来动态生成java代码的
    compile 'com.squareup:javapoet:1.8.0'
    implementation project(':butterknife-annotations')
}


tasks.withType(JavaCompile){
    options.encoding = "UTF-8"
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

这是一个java工程,我们在生成代码的时候,需要去扫描我们的注解,因此把上面的java工程annotations作为一个依赖引入,新建类ButterKnifeProcessor继承自AbstractProcessor类,实现其process方法,因为我们是需要自动生成代码的,所以要用APT,简单了解一下:

APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。现在有很多主流库都用上了 APT,比如 Dagger2, ButterKnife, EventBus3 等。AS升级到3.0以后APT配置就很简单了,我们先在我们的Android app下的build.gradle文件下依赖一下我们的自动生成代码的这个工程compiler,配置如下,就可以使用apt了。


下面我们编写我们的ButterKnifeProcessor代码

第一步:需要我们指定需要处理的版本:

    //1.指定处理的版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

第二步:得到我们需要处理的注解,因为我们目前只写了一个BindView注解

    //2.得到要处理的注解
    /**
     * 重写getSupportedAnnotationTypes方法
     * 通过重写该方法,告知Processor哪些注解需要处理
     *
     * @return 返回一个Set集合,集合内容为自定义注解的包名+类名
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }
    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        //在这里将我们写的注解添加进来 目前我们只写了一个BindView注解
        annotations.add(BindView.class);
        return annotations;
    }

重要方法在process方法,这个方法主要用来生成代码的,详细注释都有,就把该类的全部代码贴出来:

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

    private Filer mFiler;
    private Elements mElementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
    }

    //1.指定处理的版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    //2.得到要处理的注解
    /**
     * 重写getSupportedAnnotationTypes方法
     * 通过重写该方法,告知Processor哪些注解需要处理
     *
     * @return 返回一个Set集合,集合内容为自定义注解的包名+类名
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }


    /**
     * 重写process方法
     * process方法表示 有注解的话就会进到这个方法中来
     * 所有的注解处理都是从这个方法开始的,你可以理解为,
     * 当APT找到所有需要处理的注解后,会回调这个方法,你可以通过这个方法的参数,拿到你所需要的信息。
     *
     * @param annotations 待处理的 Annotations
     * @param roundEnv    RoundEnvironment roundEnv :表示当前或是之前的运行环境,可以通过该对象查找找到的注解
     * @return 表示这组 annotations 是否被这个 Processor 接受,如果接受(true)后续子的 pocessor 不会再对这个 Annotations 进行处理
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("----------process---------->");
        //所有被使用的@BindView.class
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        /**
         *  System.out.println("----------process---------->"+element.getSimpleName().toString());
         *  这个打印的情况很乱,就是说会把每个Activity的注解都打印下来
         *  比如在MianActivity中有注解如下
         *  BindView(R.id.tv1)
         *  TextView text1;
         *  BindView(R.id.tv2)
         *  TextView text2;
         *  在SecondActivity中有注解如下
         *  BindView(R.id.tv3)
         *  TextView text3;
         *  BindView(R.id.tv4)
         *  TextView text4;
         *  那么打印结果全部在一起了
         *  Console==================如下
         *  ----------process---------->text1
         *  ----------process---------->text2
         *  ----------process---------->text3
         *  ----------process---------->text4
         *  于是我们就无法区分这里找到的注解到底属于哪一个类所有
         *  我们要解决的就是一个activity对应一个List<Element>
         *  element.getEnclosingElement().getSimpleName().toString();
         *  上面这句话解析出来对应的就是注解相对应的那个activity
         */
        /*for (Element element : elements) {
            Element enclosingElement = element.getEnclosingElement();
            System.out.println("-------------------->"+element.getSimpleName().toString());
            System.out.println("-------------------->"+enclosingElement.getSimpleName().toString());

        }*/
        Map<Element, List<Element>> elementsMap = new LinkedHashMap<>();
        for (Element element : elements) {//这里理解为拿到所有的被@BindView修饰的元素
            //拿到element所属是哪一个类 比如text1属于MainActivity,text3属于SecondActivity
            //enclosingElement就表示MainActivity或者SecondActivity……
            Element enclosingElement = element.getEnclosingElement();
            //拿到enclosingElement下面的被@BindView修饰的元素
            List<Element> viewBindElements = elementsMap.get(enclosingElement);
            if (viewBindElements == null) {
                viewBindElements = new ArrayList<>();
                elementsMap.put(enclosingElement, viewBindElements);
            }
            viewBindElements.add(element);
        }
        //生成代码
        for (Map.Entry<Element, List<Element>> entry : elementsMap.entrySet()) {
            Element enclosingElement = entry.getKey();
            List<Element> viewBindElements = entry.getValue();
            //现在我们要生成这样一个类xxxxx_ViewBinding文件
            String activityClassNameStr = enclosingElement.getSimpleName().toString();
            ClassName unbindClassName = ClassName.get("com.butterknife", "Unbinder");
            ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
            //下面这行代码主要是为了拼接这样的一句话public final class xxxx_ViewBinding implements Unbinder
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC)//指定类的属性为public final
                    .addField(activityClassName, "target", Modifier.PRIVATE)//添加属性
                    .addSuperinterface(unbindClassName);
            //实现Unbinder方法
            ClassName callSuperClassName = ClassName.get("android.support.annotation", "CallSuper");
            MethodSpec.Builder unbindMethodBuild = MethodSpec.methodBuilder("unbind")
                    .addAnnotation(Override.class)
                    .addAnnotation(callSuperClassName)
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC);

            unbindMethodBuild.addStatement("$L target = this.target",activityClassName);
            unbindMethodBuild.addStatement("if (target == null) throw new IllegalStateException(\"Bindings already cleared.\")");

            unbindMethodBuild.addStatement("this.target = null");
            //构造函数
            //public ActivityLogin_ViewBinding(ActivityLogin target) {
            //this.target = target;
            //target.text = Utils.findViewById(source,R.id.text);
            //}
//            ClassName uiThreadClassName = ClassName.get("android.support.annotation", "UiThread");
            MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(activityClassName, "target");
            constructorMethodBuilder.addStatement("this.target = target");
            for (Element viewBindElement : viewBindElements) {
                String filedName = viewBindElement.getSimpleName().toString();
                ClassName classUtilsName = ClassName.get("com.butterknife", "Utils");
                int resId = viewBindElement.getAnnotation(BindView.class).value();
                constructorMethodBuilder.addStatement("target.$L = $T.findViewById(target,$L)",filedName,classUtilsName,resId);
                unbindMethodBuild.addStatement("target.$L = null",filedName);
            }
            classBuilder.addMethod(constructorMethodBuilder.build());
            classBuilder.addMethod(unbindMethodBuild.build());
            //生成类
            try {
                //获取包名
                String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
                JavaFile.builder(packageName, classBuilder.build())
                        .addFileComment("buttknife自动生成的类")
                        .build().writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("生成类过程异常");
            }
        }
        return false;
    }

    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        //在这里将我们写的注解添加进来 目前我们只写了一个BindView注解
        annotations.add(BindView.class);
        return annotations;
    }
}

下面来看一下butterknife工程,我们看ButterKnife源码知道自动生成的类xxx_ViewBinding是实现了Unbinder这个类,我们照搬这个类:

public interface Unbinder {
    @UiThread
    void unbind();

    Unbinder EMPTY = new Unbinder() {
        @Override
        public void unbind() {
        }
    };
}

我们的ButterKnife代码如下,我们在最开始的例子中写的伪代码中可以看到我们在使用的时候是这样的:

xxxx_ViewBinding viewBinding = new xxxx_ViewBinding(this);

新建了这样一个类,然后再构造方法中就实现了findViewById就找到控件,那么我们在使用的时候不至于每次都new xxx_ViewBinding(this)吧;那么在这里就会用到一点反射的方法:ButterKnife代码如下。

public class ButterKnife {

    public static Unbinder bind(Activity activity){
     // xxxx_ViewBinding viewBinding = new xxxx_ViewBinding(this);
        try {
            //在这这里我们先拿到ButterKnife自动生成的那个类
            Class<? extends Unbinder> bindClassName = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName()+"_ViewBinding");
            //找到他的构造函数
            Constructor<? extends Unbinder> bindConstructor = bindClassName.getDeclaredConstructor(activity.getClass());
            Unbinder unbinder = bindConstructor.newInstance(activity);
            return unbinder;

        } catch (Exception e) {
            e.printStackTrace();
        }

        return Unbinder.EMPTY;
    }
}

butterknife里面还有个Util工具类,其实代码很简单就是用来找控件。

public class Utils {


    public static final <T extends View> T findViewById(Activity activity, int viewId) {
        Log.i("test",activity.findViewById(viewId).getClass().getName());
        return activity.findViewById(viewId);
    }
}

再看看我们的简单使用:

public class MyButterKnifeActivity extends AppCompatActivity {


    @BindView(R.id.text222)
    TextView text22;

    private Unbinder mUnbinder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_butterknife);
        mUnbinder = ButterKnife.bind(this);
        text22.setText("我的butterknife");
    }


    @Override
    protected void onDestroy() {
        mUnbinder.unbind();
        super.onDestroy();
    }
}
好了,ButterKnife原理及简单实现就写好了,你也可以试试的。源码地址: 点击打开链接


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值