天天看点

利用注解实现简单的ButterKnife

#利用注解实现简单的ButterKnife

✍ Author by NamCooper

##一、注解的基础知识

####简介

  在开发中,我们经常可以看到@Override,@Deprecated,@SuppressWarnings这些常见的注解。这些注解相当于一种标记,有了这些标记,编译器、开发工具或者其他程序就可以根据标记去进行相应的操作。

  上述的三种常见注解,都是标记在方法上的,而在Java中这样的标记可以作用在包、类、字段、方法、方法的参数以及局部变量上,使用范围很广,也就有很多值得钻研和思考的地方。当然,本文仅仅是介绍注解的简单使用,班门弄斧而已。

####注解类型,以及如何定义一个注解

  • 注解的类型

      java中注解支持:8种基本数据类型,String,Class,enum,annotation以及以上类型的数组类型。

  • 定义一个注解类

      下面的代码是自定义注解的第一步,Android开发中使用注解时这一步也是必须的。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
	int value();//当使用注解时,如果只给名为value的属性赋值时,可以省略“value=”  
	String name() default "zhangsan";//默认值
}
           

以上代码声明了一个注解类ViewInject,那么之后再使用该注解的地方,就会使用格式为@ViewInject()这样的自定义注解。@Target、@Retention被称为元注解 ,元注解还有与@Documented、@Inherited,在本文中不涉及,感兴趣的可以自行研究。

  ||1、@Target: 声明了这个注解类所修饰对象的范围,上文的例子中@Target(ElementType.FIELD),代表着这个注解只能用在修饰成员变量,如下

class Test{
@ViewInject(value = 1)
	int a;
	public void xxx(){
		...
	};
}
           

声明了作用于成员变量的注解,不能使用在其他位置。以下是作用域枚举:

public enum ElementType {
	TYPE,
	FIELD,
	METHOD,
	PARAMETER,
	CONSTRUCTOR,
	LOCAL_VARIABLE,
	ANNOTATION_TYPE,
	PACKAGE,
	TYPE_PARAMETER,
	TYPE_USE;
	private ElementType() {
}
           

常用的如TYPE(作用于class)、FIELD(作用于成员变量)、METHOD(作用于方法)、CONSTRUCTOR(作用于构造方法)。在后面的使用中都会有所涉及。

  ||2、@Retention: 标示注解的保存策略,也是一个枚举值。

  RetentionPolicy.SOURCE:注解只保存在源代码中,即.java文件

  RetentionPolicy.CLASS:注解保存在字节码中,即.class文件

  RetentionPolicy.RUNTIME:注解保存在内存中的字节码,可用于反射

  ||3、int value(): 定义一个int类型的属性,这里可以把注解类看成一个普通的bean类,int value()就可以看成int value,后文会对使用加以说明。

  ||4、String name() default “zhangsan”: 同上,default后定义的是这个属性的默认值。

####注解的基础运用

#####1、定义一个注解类

package demo;import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.lang.model.element.Element;

@Target(ElementType.METHOD)//作用于方法上@Retention(RetentionPolicy.RUNTIME)//运行时注解
public @interface UseCase {
	int id();
	String description() default "no description";
}
           

#####2、使用自定义注解

package demo;
public class PasswordUtil {
@UseCase(id = 1,description = "hahaha")//使用自定义注解,并且给定两个属性public void outPut(UseCase useCase){
	System.out.println("再次执行id = " + useCase.id() + ":description = "+ useCase.description());
}}
           

####3、解析注解

package demo;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
Method m = null;
try {//通过反射获取被注解的方法
	Class c = Class.forName("demo.PasswordUtil");
	m = c.getDeclaredMethod("outPut", UseCase.class);
}catch (NoSuchMethodException | SecurityException e){
	e.printStackTrace();
} catch (ClassNotFoundException e) {
	e.printStackTrace();
}
if (m != null) {//获取注解类对象,打印信息
	UseCase useCase = m.getAnnotation(UseCase.class);
PasswordUtil util = new PasswordUtil();util.outPut(useCase);}}}
           

运行结果如下:

id = 1:description = hahaha
再次执行id = 1:description = hahaha
           

由运行结果不难发现,通过反射获取被注解的方法Method对象,并通过getAnnotation方法指定注解类,即可获取注解类对象,通过注解类对象调用其中被定义的属性id()、description()就可以获取在注解里输入的值。而由一个中间类Test很容易就实现了注解与被注解的方法之间的交互。

以上是对java中注解基本使用的介绍,了解了基本用法,下面就到了重头戏——如何利用注解机制,在Android中实现类似于ButterKnife的功能。

##二、自定义ButterKnife(如粘贴下文代码,请代码中的删除中文注释!)

####简述:

  早期的ButterKnife采用了运行时注解,也就是上文所使用的@Retention(RetentionPolicy.RUNTIME),由注解实现findViewById的操作也与上文的原理类似,都是在程序运行时采用反射获取注解并且执行相应的操作,这样做的弊端是很明显的。

  在一个Android程序中,甚至说在一个activity中,view的个数都可以很多,findViewById的次数也就非常频繁。而频繁的反射操作势必会影响性能,所以在ButterKnife的更新中舍弃了这样的实现方式,改为编译时注解@Retention(RetentionPolicy.CLASS)。两者最大的不同在于,原来需要反射来做的事情,现在在编译期已经基本完成了并且生成了源码,在使用者调用的时候简单地调用了一个bind方法,就可以通过调用已生成的源码完成findViewById的操作。

  下面,我们就自己动手来进行实践!

####+  自定义注解Moudle

  • 创建一个java library “annotation” ,注意一定要是java library!

      

    利用注解实现简单的ButterKnife
  • 创建一个注解类
package com.namcooper;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindField {
    int value();//这里只需要一个int类型的值,需要传入控件id
}
           

####+  自定义处理器Moudle

  • 创建一个java library “compiler”,注意一定要是java library!
    利用注解实现简单的ButterKnife
  • 添加依赖
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    //这个引用是可选的,用于在main文件夹下创建resources/META-INF/javax.annotation.processing.Processor,并在其中注册我们自定义的处理器。当然也可以不依赖,由自己通过手动创建并写入的方式也可以进行注册。
    compile 'com.google.auto.service:auto-service:1.0-rc2'Processor
    //这个引用也是可选的,帮助我们进行源码生成的,可以使生成的源码格式良好,当然也可以全部手动编写。
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':annotation')//这里要依赖刚刚定义的注解类java Moudle

}
           
  • 创建一个处理器
package com.compiler;

import com.google.auto.service.AutoService;
import com.namcooper.BindField;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.Collections;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

@AutoService(Processor.class)//通过注解注册处理器,不可省略
public class BindProcessor extends AbstractProcessor {

    /**
     * 每一个注解处理器类都必须有一个无参构造方法。
     * init方法是在Processor创建时被apt调用并执行初始化操作。例如后文中使用的processingEnv.getFiler()可以在这个方法中一次获取。
     * @param processingEnv 提供一系列的注解处理工具。
     **/
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override//返回支持的Annotation类型,这里返回的是一个集合,所以可以返回多种类型
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(BindField.class.getCanonicalName());
    }

    @Override//扫描到注解后该方法会被调用
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {
	//获取被BindField注解的节点(关于节点的知识,请自行补充),此处获取的就是被注解的成员变量的element对象的集合
        Set<? extends Element> elements = environment.getElementsAnnotatedWith(BindField.class);
	//调用javaopet的Api创建方法
        MethodSpec.Builder findViewBuilder = MethodSpec.methodBuilder("findView")
                .addModifiers(Modifier.PUBLIC)//添加声明
                .returns(void.class)//添加返回值类型
                .addParameter(Object.class, "activity");//添加方法参数,分别是引用类型和形参名
        String packageName = "";
        String className = "";

        for (Element el : elements) {//遍历节点集合,获取相关信息

            PackageElement packageElement = getPackage(el);
            packageName = packageElement.getQualifiedName().toString();//获取被注解的元素所在包的包名

            TypeElement typeElement = getClass(el);
            className = typeElement.getSimpleName().toString();//获取被注解的元素所在类的类名

            TypeMirror mirror = el.asType();//获取被注解元素的引用类型如TextView

            String parameterName = el.getSimpleName().toString();//获取被注解元素的变量名

            BindField bindView = el.getAnnotation(BindField.class);//获取注解类对象
	    //根据上面获取到的信息,拼接方法体,此处的语法规则可参考[javaopet的基本使用](http://blog.csdn.net/crazy1235/article/details/51876192)
            findViewBuilder.addStatement("(($N)activity).$N = ($N)((android.app.Activity)activity).findViewById($L)", className, parameterName, mirror.toString(), bindView.value());
        }
	
	//创建方法
        MethodSpec findView = findViewBuilder.build();
        
	//创建类,这里要注意自定义的命名规则,要与后续的使用统一“原类名_Binder”
        TypeSpec helloWorld = TypeSpec.classBuilder(className + "_Binder")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                //.addSuperinterface(ClassName.get("com.namcooper.apt.bind_api","Binder"))//添加该类实现的接口,需要指定包名和接口名
                .addMethod(findView)//将刚刚生成的方法写入
                .build();

    //因为process方法会多次执行以保证将新生成的代码中的注解也扫描到,如果包名类名都是空,则可以截断。
        if (packageName.equals("") || className.equals(""))
            return true;
        JavaFile javaFile = JavaFile.builder(packageName, helloWorld)//指定包名和类,生成javaFile
                .build();
        try {
            javaFile.writeTo(processingEnv.getFiler());//将JavaFile写入默认路径:项目build-generated-source-apt-debug下,也可以自由指定写入路径

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

        return false;
    }

    /**
     * 获取PackageElement
     *
     * @throws NullPointerException 如果element为null
     */
    private static PackageElement getPackage(Element element) {
        while (element.getKind() != ElementKind.PACKAGE) {
            element = element.getEnclosingElement();
        }
        return (PackageElement) element;
    }

    /**
     * 获取TypeElement
     *
     * @throws NullPointerException 如果element为null
     */
    private static TypeElement getClass(Element element) {
        while (element.getKind() != ElementKind.CLASS) {
            element = element.getEnclosingElement();
        }
        return (TypeElement) element;
    }

}

           

####+  尝试使用

app中添加依赖

compile project(':annotation')
    annotationProcessor project(':compiler')
           

在app下的MainActivity的布局中添加两个TextView,并在MainActivity中使用刚刚定义的注解。

TextView
        android:id="@+id/xxx"
        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"

    TextView
        android:id="@+id/ddd"
        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"

           
package com.namcooper.apt.androidapt;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

import com.namcooper.BindField;

public class MainActivity extends Activity {

    @BindField(R.id.xxx)
    TextView demoX;
    @BindField(R.id.ddd)
    TextView demoD;

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

下面是见证奇迹的时刻:Rebuild工程

利用注解实现简单的ButterKnife

这个位置下发现了以我们设定的命名规则命名的java文件,打开看看!

package com.namcooper.apt.androidapt;

import java.lang.Object;

public final class MainActivity_Binder {
  public void findView(Object activity) {
    ((MainActivity)activity).demoX = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427422);
    ((MainActivity)activity).demoD = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427423);
  }
}
           

很眼熟吧,这里就进行了findViewById的操作了。让我们用用看!

package com.namcooper.apt.androidapt;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

import com.namcooper.BindField;

public class MainActivity extends Activity {

    @BindField(R.id.xxx)
    TextView demoX;
    @BindField(R.id.ddd)
    TextView demoD;

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

        MainActivity_Binder binder = new MainActivity_Binder();
        binder.findView(this);
        demoX.setText("测试成功啦!");
        demoD.setText("这是第二个控件");
    }
}
           

运行结果:

利用注解实现简单的ButterKnife

成功规避了频繁的findViewById操作!但问题也很明显,那就是使用者需要知道你的命名逻辑后才能调用,这个就太Low了!ButterKnife仅仅是向外提供了一个.bind方法就可以实现这些操作,所以我们要继续进行优化。

####+  自定义调用Api

  • 创建一个java library “compiler”,注意是Android library,不需要更改build.gradle的内容。创建成功后将其添加到app的依赖中。
    利用注解实现简单的ButterKnife
  • 创建Binder接口备用
package com.namcooper.apt.bind_api;

public interface Binder {

    void findView(Object activity);
}

           

这里需要解除上文中process方法中关于添加接口的注释

//创建类,这里要注意自定义的命名规则,要与后续的使用统一“原类名_Binder”
        TypeSpec helloWorld = TypeSpec.classBuilder(className + "_Binder")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addSuperinterface(ClassName.get("com.namcooper.apt.bind_api","Binder"))//添加该类实现的接口,需要指定包名和接口名
                .addMethod(findView)//将刚刚生成的方法写入
                .build();
           

然后我们Rebuild项目看看MainActivity_Binder的效果

package com.namcooper.apt.androidapt;

import com.namcooper.apt.bind_api.Binder;
import java.lang.Object;

public final class MainActivity_Binder implements Binder {
  public void findView(Object activity) {
    ((MainActivity)activity).demoX = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427422);
    ((MainActivity)activity).demoD = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427423);
  }
}
           

这个自行生成的类,就实现了Binder接口,并且实现了findView方法。

  • 创建对外暴露的Api BuildTool
package com.namcooper.apt.bind_api;

import android.app.Activity;

public class BindTool {


    public static void bind(Activity activity) {
        //获取binder的名称
        String className = activity.getClass().getName() + "_Binder";
	
        try {
		//利用反射获取自动生成的类和对象
            Class binder = Class.forName(className);
            Binder b = (Binder) binder.newInstance();
		//调用方法
            b.findView(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
</pre>

然后去到项目中使用吧!  

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

        BindTool.bind(this);
        demoX.setText("测试成功啦!");
        demoD.setText("这是第二个控件");
    }
           

运行结果跟上文的一毛一样!如此一来,我们就基本实现了ButterKnife的findViewById功能了!

  • 参照ButterKnife原理进行优化
public class BindTool {
    //初始化一个集合,以被绑定的activity为key,对应的Binder为Value
    private static Map<String, Binder> map = new HashMap<>();

    public static void bind(Activity activity) {
        //获取binder的名称
        String className = activity.getClass().getName() + "_Binder";
    //先尝试在集合中获取Binder,获取不到,再通过反射获取。这样可以保证在程序运行期间,每个被绑定的对象只会触发一次反射,从而提高性能。
        Binder b = map.get(className);
        if (b == null)
            try {
                Class binder = Class.forName(className);
                b = (Binder) binder.newInstance();
                b.findView(activity);
                map.put(className, b);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
    }
}
           

###3、进阶功能

  当然,ButterKnife中还有诸如onclick,bindLaypout,bindfragment等等很方便的功能。

  这些功能看似复杂,其实与bindView原理一致,下面以onClick为例进行简单实现。

  • 创建另一个注解类
package com.namcooper;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface BindClick {
    int value();
}
           
  • 在处理器中增加逻辑:逻辑不再解释,很好理解
@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {
        /**处理field注解**/
        Set<? extends Element> elements = environment.getElementsAnnotatedWith(BindField.class);
        MethodSpec.Builder findViewBuilder = MethodSpec.methodBuilder("findView")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(Object.class, "activity");
        String packageName = "";
        String className = "";

        for (Element el : elements) {

            PackageElement packageElement = getPackage(el);
            packageName = packageElement.getQualifiedName().toString();

            TypeElement typeElement = getClass(el);
            className = typeElement.getSimpleName().toString();

            TypeMirror mirror = el.asType();

            String parameterName = el.getSimpleName().toString();

            BindField bindView = el.getAnnotation(BindField.class);

            findViewBuilder.addStatement("(($N)activity).$N = ($N)((android.app.Activity)activity).findViewById($L)", className, parameterName, mirror.toString(), bindView.value());
        }
        MethodSpec findView = findViewBuilder.build();
        /**处理click注解**/
        Set<? extends Element> elementsClick = environment.getElementsAnnotatedWith(BindClick.class);
        FieldSpec activity = FieldSpec.builder(TypeName.OBJECT,"activity").build();

        MethodSpec.Builder clickOverWrite = MethodSpec.methodBuilder("onClick")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(ClassName.get("android.view", "View"), "v")
                .returns(void.class);

        MethodSpec.Builder clickBuilder = MethodSpec.methodBuilder("click")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(Object.class, "activity")
                .returns(void.class);

        clickBuilder.addStatement("this.activity = activity");

        CodeBlock.Builder switchBlock = CodeBlock.builder().add("switch(v.getId()){\n");


        for (Element el : elementsClick) {
            PackageElement packageElement = getPackage(el);
            packageName = packageElement.getQualifiedName().toString();

            TypeElement typeElement = getClass(el);
            className = typeElement.getSimpleName().toString();

            String methodName = el.getSimpleName().toString();

            BindClick bindClick = el.getAnnotation(BindClick.class);
            clickBuilder.addStatement("((android.app.Activity)activity).findViewById($L).setOnClickListener(this)", bindClick.value());
            switchBlock.add("case $L:\n", bindClick.value());
            switchBlock.add("(($N)(activity)).$N();\n",className,methodName);
            switchBlock.add("break;\n");

        }
        switchBlock.add("\n}");
        clickOverWrite.addCode(switchBlock.build());
        MethodSpec mClickOverWrite = clickOverWrite.build();
        MethodSpec mClick = clickBuilder.build();
        TypeSpec helloWorld = TypeSpec.classBuilder(className + "_Binder")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addSuperinterface(ClassName.get("com.namcooper.apt.bind_api", "Binder"))
                .addSuperinterface(ClassName.get("android.view", "View.OnClickListener"))
                .addField(activity)
                .addMethod(findView)
                .addMethod(mClick)
                .addMethod(mClickOverWrite)
                .build();

        if (packageName.equals("") || className.equals(""))
            return true;
        JavaFile javaFile = JavaFile.builder(packageName, helloWorld)
                .build();
        try {
            javaFile.writeTo(filer);

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

        return false;
    }
           
  • 修改Binder
public interface Binder {

    void findView(Object activity);
    void click(Object activity);
}
           
  • 修改BindTool
public class BindTool {

    private static Map<String, Binder> map = new HashMap<>();

    public static void bind(Activity activity) {
        //获取binder的名称
        String className = activity.getClass().getName() + "_Binder";
        Binder b = map.get(className);
        if (b == null)
            try {
                Class binder = Class.forName(className);
                b = (Binder) binder.newInstance();
                b.findView(activity);
                b.click(activity);
                map.put(className, b);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
    }
}
           
  • 调用
public class MainActivity extends Activity {

    @BindField(R.id.xxx)
    TextView demoX;
    @BindField(R.id.ddd)
    TextView demoD;

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

        demoX.setText("测试成功啦!");
        demoD.setText("这是第二个控件");


    }

    @BindClick(value = R.id.xxx)
    public void xxxClick() {
        Toast.makeText(MainActivity.this, "xxx点击了", Toast.LENGTH_SHORT).show();
    }

    @BindClick(value = R.id.ddd)
    public void dddClick() {
        Toast.makeText(MainActivity.this, "ddd点击了", Toast.LENGTH_SHORT).show();
    }

}
           
  • rebuild后生成
public final class MainActivity_Binder implements Binder, View.OnClickListener {
  Object activity;

  public void findView(Object activity) {
    ((MainActivity)activity).demoX = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427422);
    ((MainActivity)activity).demoD = (android.widget.TextView)((android.app.Activity)activity).findViewById(2131427423);
  }

  public void click(Object activity) {
    this.activity = activity;
    ((android.app.Activity)activity).findViewById(2131427422).setOnClickListener(this);
    ((android.app.Activity)activity).findViewById(2131427423).setOnClickListener(this);
  }

  public void onClick(View v) {
    switch(v.getId()){
    case 2131427422:
    ((MainActivity)(activity)).xxxClick();
    break;
    case 2131427423:
    ((MainActivity)(activity)).dddClick();
    break;

    }}
}
           

屌不屌?

##三、结束语

  本文演示了ButterKnife的bindView和BindClick原理,其余更多功能原理类似,读者可以自行练习。

  当然,相对于ButterKnife来说,本文的例子还非常简单。而且在Android Studio中有专为ButterKnife开发的插件,可以快速生成注解,从而大大方便使用者。

  本文仅作学习交流使用,欢迎指正!

继续阅读