Android ButterKnife框架理解-编译时注解实践

写在前面

由于文章和代码写的比较久了
再次翻看阅读,打开工程运行的时候,发现注解处理器不生效
浪费了很多时间才搞清楚问题所在,所以先记录一下
之前使用的是

 classpath 'com.android.tools.build:gradle:3.0.0'

distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

由于AndroidStudio更新,再次clone代码后,工程自动更新配置
gradle 5.4.1版本,android tools3.5版本,发现注解处理器不生效

对比了ButterKnife 源码的配置,发现还是使用的gradle4.10.3,android tools 3.4
索性降低版本试了下,发现是可以的

最终定位问题原因:在Gradle 5.0将忽略compile classpath中的annotation processor,
需要手动添加到annotation processor path

implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

ButterKnife优势

Android之神Jake Wharton写的ButterKnife

ButterKnife是一个应用于Android系统的View注入框架
可以减少大量的findViewById以及setOnClickListener代码

优势:

  1. 代码清晰,可读性强
  2. 简化Adapter中ViewHolder绑定
  3. 运行时不会影响APP效率,使用配置方便
  4. View绑定和Click事件处理功能,简化代码,提高开发效率

具体用法就不做介绍了,参考官方使用即可

可能有人会对第三点有疑问,觉得使用了反射,影响了效率
其实看了ButterKnife的源码就知道
反射只用在了创建XXXActivity_ViewBinding对象时
ButterKnife注解是编译时注解并非运行时注解

ButterKnife框架使用到的技术

自定义注解

参考链接:https://blog.csdn.net/kaifa1321/article/details/79622715

APT

APT(Annotation Process Tool),是一种在代码编译时处理注解,按照一定的规则,生成相应的java文件
多用于对自定义注解的处理,目前比较流行的Dagger2, EventBus3,包括 ButterKnife,都是采用APT技术
对运行时的性能影响很小

Androidstudio配置 APT

工程结构.png

android module : app, butterknife
java module : butterknife-compiler, butterknife-annotations
app 依赖butterknife
butterknife 依赖 java工程 butterknife-compiler
butterknife-annotations 里面就是放了自定义注解

工程的 build.gradle

buildscript {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        //新版本的Androidstudio 已经不用这么配置了
        //classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter()
    }
}
task clean(type: Delete) {
    delete rootProject.buildDir
}

java-library butterknife-compiler的build.gradle

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    compile 'com.squareup:javapoet:1.9.0'
    compile project(path: ':butterknife-annotations')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

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

自定义注解 BindView 标记属性

/**
 * 编译时注解  作用在属性上   BindView
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

MainActivity 使用注解 BindView

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv1)
    TextView mTextView1;
    @BindView(R.id.tv2)
    TextView mTextView2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        mTextView1.setText("hello android");
        mTextView2.setText("hello Python");
    }
}

ButterKnife的bind方法

public class ButterKnife {
    public static UnBinder bind(Activity activity) {
        //获取activity的Class
        Class<?> activityClass = activity.getClass();
        //获取activity的全路径名
        String activityName = activityClass.getName();
        //拼接className
        String className = activityName + "_ViewBinding";
        try {
            //获取生成类的Class
            Class clazz = Class.forName(className);
            //获取构造函数
            Constructor constructor = clazz.getDeclaredConstructor(activityClass);
            //创建生成类的对象
            UnBinder unBinder = (UnBinder) constructor.newInstance(activity);
            return unBinder;
        } catch (Exception e) {
            e.printStackTrace();
        }
        //如果出错返回EMPTY
        return UnBinder.EMPTY;
    }
}

activityName + "_ViewBinding这个类长什么样呢?
在app的build目录下\build\generated\source\apt\debug\com\qingguoguo\followbutterknife

public final class MainActivity_ViewBinding implements UnBinder {
  private MainActivity target;

  public MainActivity_ViewBinding(MainActivity target) {
    this.target = target;
    this.target.mTextView1 = Utils.findViewById(target,2131165315);
    this.target.mTextView2 = Utils.findViewById(target,2131165316);
  }

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

MainActivity_ViewBinding 怎么生成的

封装 findViewById方法

public class Utils {
    //封装 findViewById方法
    public static <T extends View> T findViewById(Activity activity, int id) {
        return (T) activity.findViewById(id);
    }
}

生成代码

//@AutoService(Processor.class)这个注解一定要
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

    private Filer mFile;
    private Elements mElementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //初始化 java文件生成的位置
        mFile = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
    }

    /**
     * 用来指定支持的 SourceVersion
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 用来指定支持的 AnnotationTypes
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    /**
     * 参考 ButterKnife 的写法
     *
     * @return
     */
    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        //BindView目前只写了这个注解
        annotations.add(BindView.class);
        return annotations;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 解析注解
        HashMap<Element, List<Element>> elementMap = new HashMap<>(16);
        //包含BindView注解的Set集合
        Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : elementSet) {
            System.out.println("----" + element.getSimpleName() + "-----" + element.getEnclosingElement());
            /**
             *比较乱 并没有给我们包裹好,需要自己组合,把一个类的所有属性集合到一起
             *----mTextView1-----com.qingguoguo.followbutterknife.LoginActivity
             *----mTextView2-----com.qingguoguo.followbutterknife.LoginActivity
             *----mTextView1-----com.qingguoguo.followbutterknife.MainActivity
             *----mTextView2-----com.qingguoguo.followbutterknife.MainActivity
             */
            //拿到类名
            Element enclosingElement = element.getEnclosingElement();
            //Map中查找是否已经存了该类
            List<Element> elements = elementMap.get(enclosingElement);
            if (elements == null) {
            //没有找到,创建集合
                elements = new ArrayList<>();
                elementMap.put(enclosingElement, elements);
            }
            //把该属性加到集合
            elements.add(element);
        }
        System.out.println("----" + elementMap + "-----");

        //生成代码,遍历Map集合
        Set<Map.Entry<Element, List<Element>>> entries = elementMap.entrySet();
        for (Map.Entry<Element, List<Element>> entry : entries) {
            Element element = entry.getKey();
            //父类
            ClassName unBinderClassName = ClassName.get("com.qingguoguo.butterknife", "UnBinder");
            //生成类
            //activity Class Name
            String activityClassNameStr = element.getSimpleName().toString();
            ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
            TypeSpec.Builder activityBuilder = TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC);

            //---------添加父接口-----------------//
            activityBuilder.addSuperinterface(unBinderClassName);

            //---------添加属性 private XXXActivity target ;-------//
            activityBuilder.addField(activityClassName, "target", Modifier.PRIVATE);

            //---------添加构造方法  public xxx_ViewBinding(xxx target) -------------------//
            MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder();
            constructorMethodBuilder.addParameter(activityClassName, "target").addModifiers(Modifier.PUBLIC);
            constructorMethodBuilder.addStatement("this.target = target");

            //-------------------添加实现接口UnBinder 要重写的unbind方法----------//
            ClassName callSuperClassName = ClassName.get("android.support.annotation",
                    "CallSuper");
            MethodSpec.Builder unbindMethodBuilder = MethodSpec
                    .methodBuilder("unbind")
                    .addAnnotation(Override.class)
                    .addAnnotation(callSuperClassName).addModifiers(Modifier.PUBLIC);
            unbindMethodBuilder.addStatement("$T target = this.target", activityClassName);
            unbindMethodBuilder.addStatement("if (target == null) throw new IllegalStateException(\"Bindings already cleared.\")");

            //-------------------给类中带有注解的属性赋值----------------------//
            List<Element> elementList = entry.getValue();
            for (Element viewBindIdElement : elementList) {
                String filedName = viewBindIdElement.getSimpleName().toString();
                int id = viewBindIdElement.getAnnotation(BindView.class).value();
                ClassName utilsClassName = ClassName.get("com.qingguoguo.butterknife",
                        "Utils");
                constructorMethodBuilder.addStatement("this.target.$L = $T.findViewById(target,$L)", filedName, utilsClassName, id);
                unbindMethodBuilder.addStatement("this.target.$L = null", filedName);
            }
            unbindMethodBuilder.addStatement("this.target = null");
            activityBuilder.addMethod(unbindMethodBuilder.build());
            activityBuilder.addMethod(constructorMethodBuilder.build());

            //-----------------包名----------------------//
            String packageName = mElementUtils.getPackageOf(element.getEnclosingElement()).getQualifiedName().toString();

            //----------------生成Java文件----------------//
            try {
                JavaFile.builder(packageName,
                        activityBuilder.build()).addFileComment("仿ButterKnife自动生成").build().writeTo(mFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

断点调试

刚开始接触APT不太熟悉的时候,肯定写出来有很多问题
如果你希望断点调试,按照以往的经验发现不起作用
因为编译时在单独的JVM里面执行的,所以需要建立远程调试

  • 在项目的根目录下gradle.properties 文件中加入如下两条语句
org.gradle.jvmargs= -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
org.gradle.parallel=true
  • run->Edit Config


    编辑配置
  • 创建远程Remote调试配置参数


    Remote调试.png
  • 点击刚刚创建的配置,debug运行


    debug运行
  • rebuild


    rebuild

参考链接:https://blog.csdn.net/hongxue8888/article/details/99710884

其他链接

腾讯音乐技术团队-浅析ButterKnife

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

推荐阅读更多精彩内容