手把手教你实现仿ButterKnife依赖注入框架

如需转载请评论或简信,并注明出处,未经允许不得转载

目录

前言

目前Android社区涌现出越来越多的IOC框架,ButterKnifeDagger2EventBus3,这些框架往往能有效帮助我们简化代码,模块解耦,相信很多人也或多或少的用过其中一些框架。但是,有没有人想过这些框架的内部原理都是怎么样的呢?本文就从ButterKnife入手,手把手教你实现一个仿ButterKnife的IOC框架

知识准备

Annotation

我们知道annotation有三个保留级别

  • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
  • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
  • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们

annotation实际上就是一个标签,单独存在的时候没有任何实际意义。为了便于理解,这里再延伸一下另一个词语—Hook,Hook的英文解释是钩子。依我的理解,注解实际上就像这个钩子,勾住”类“、”方法“、”字段“,为了后续想对这些被“勾住”的东西做一些操作提供了方便

更多关于注解的知识可以自己查看相关资料,这里就不多做介绍了

AnnotationProcessor

annotationProcessorAPT工具中的一种,他是Google开发的内置框架,不需要引入,可以直接在build.gradle文件中使用,如下:

  dependencies {
    annotationProcessor project(':compiler') 
  }
APT的工作

APT简单的说就是注解处理器,主要作用是可以编写一些规则在编译期间找出项目中的特定注解,以注解中的参数作为输入,生成文件.java文件作为输出。注意,这里的重点是生成.java文件,而不能修改已经存在的Java类,例如不能向已有的类中添加方法

开始ButterKnife之旅

ButterKnife使用简单介绍

先来看一下ButterKnife的常规使用方法,我们可以在Activity中的任意方法中直接使用这个textView,省去了findViewById的操作

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.txt_test)
    TextView textView;
    @BindView(R.id.btn_test)
    Button button

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //调用框架方法
        ButterKnife.bind(this);
        //业务代码
        textView.setText("Hello World");
        button.setOnClickListenr(new OnClickListener(View view){})
    }
}

问题分析

我们先不去看源码,我们可以设想一下ButterKnife.bind(this)做了什么事情,我认为大概是像下面这样:

public class ButterKnife{
        public static void bind(MainActivity activity){
            activity.textView = activity.findViewById(R.id.txt_test);
            activity.button = activity.findViewById(R.id.btn_test)
        }
}

接下来会遇到几个问题:

问题一:我们如何将控件的引用和控件的id关联起来?我想我们应该很快有答案了,@BindView注解其实就是起到了关联的作用

问题二:前面说到,APT只能生成.java文件,而不能直接在方法中插入代码。那么怎么办呢,我们可以通过APT生成.java文件,然后在运行时通过反射调用它,如下所示

  1. 创建一个接口(接口是一种约束),这里用到了泛型,因为我们要适用所有Activity
public interface BindAdapter<T> {
    void bind(T activity);
}
  1. 我们通过APT生成BindAdapterImp类,实现BindAdapter接口
public class BindAdapterImp implement BindAdapter<MainActivity>{
        public void bind(MainActivity activity) {
        activity.textView = activity.findViewById(R.id.txt_test);
            activity.button = activity.findViewById(R.id.btn_test)
    }
}
  1. ButterKnifebind()里,通过反射生成BindAdapterImp,在调用其bind()
public class ButterKnife {
   private static final String CLASS_NAME = "";
    public static void bind(Activity activity){
        //反射拿到class
        Class<?> bindAdapterClass = Class.forName(CLASS_NAME);
        //通过class拿到BindAdapterImp对象
        BindAdapterImp adapter = (BindAdapterImp) bindAdapterClass.newInstance();
        //调用bind
        adapter.bind(activituy)
        }
}

问题三:问题又来了,我们把生成的BindAdapterImp类放到哪个包下面能让所有类都能调用到呢?答案是内部类

内部类在编译期间生成的实际上是单独一个.java文件

所以我们会为每一个调用了ButterKnife.bind()Activity生成一个BindAdapterImp内部类,根据这个思路,我们对上面的代码进行了一些优化,如下所示

//这里的 MainActivity 是根据不同的Activity进行变化的
public class MainActivity$BindAdapterImp implement BindAdapter<MainActivity>{
        public void bind(MainActivity activity) {
        activity.textView = activity.findViewById(R.id.txt_test);
        activity.button = activity.findViewById(R.id.btn_test)
    }
}
public class ButterKnife {
    private static final String SUFFIX = "$BindAdapterImp";
        //做了一个缓存,只有第一次bind时才通过反射创建对象
    static Map<Class, BindAdapter> mBindCache = new HashMap();

    public static void bind(Activity target){
        BindAdapter bindAdapter;
        if (mBindCache.get(target) != null) {
            //如果缓存中有activity,从缓存中取
            bindAdapter = mBindCache.get(target);
        } else {
            //缓存中没有,创建一个
            String adapterClassName = target.getClass().getName() + SUFFIX;
            Class<?> aClass = Class.forName(adapterClassName);
            bindAdapter = (BindAdapter) aClass.newInstance();
            mBindCache.put(aClass, bindAdapter);
        }
                //调用bind
        bindAdapter.bind(target);
    }   
}

Tips:从上面的代码我们发现,为了尽量避免反射的性能消耗,ButterKnife内部会有一个缓存,这是一种典型的空间换时间的做法。在做内存优化的时候,我们往往会提到尽量少用ButterKnife这种依赖注入框架其实就是这个原因。这个还需要大家对各自项目作出一个折中的选择

最后,我们面临的问题实际上就是如何在编译期生成上面BindAdapterImp类,接下来跟着我一步步来吧

创建一个项目

注意如果这里勾选了androidx,而且想要使用Kotlin,需要用kapt取代AnnotationProcessor
关于androidx与kotlin兼容问题具体参考:
当ButterKnife8.8.1碰到AndroidX怎么办
看懂编译注解annotationProcessor和kapt

创建一个注解类

新建一个java module,命名为annotation

创建编译器注解类@BindView,这是一个属性注解,只有在编译期有效,经过编译后,注解信息会被丢弃,不会保留到编译好的class文件里

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

创建AnnotationProcessor

新建一个java module,命名为processor

创建注解处理器,在编译期间去扫描@BindView所标注的属性

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.geekholt.annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class GeekKnifeProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}
  • @AutoService(Processor.class):向javac注册我们这个自定义的注解处理器,这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法
  • @SupportedAnnotationTypes():表示我们这个注解处理器所要处理的注解
  • @SupportedSourceVersion():代表JDK版本号,这里是代表java8
  • init():初始化时会自动被调用,并传入processingEnvironment参数,通过该参数可以获取到很多有用的工具类: Elements , Types , Filer 等等
  • process()AnnotationProcessor扫描出的结果会存储进roundEnvironment中,可以从中获取注解所标注的内容信息
  • ProcessingEnvironment
/**用于提供工具类**/
public interface ProcessingEnvironment {
        //返回注解处理器的配置参数
    Map<String, String> getOptions();
  
        //Message用来报告错误,警告和其他提示信息
    Messager getMessager();
  
        //Filer用于创建新的源文件,class文件或辅助文件(可以用JavaPoet简化创建文件操作)
    Filer getFiler();
  
        //Elements包含用于操作Element的工具方法
    Elements getElementUtils();
  
        //Types包含用于操作TypeMirror的工具方法
    Types getTypeUtils();
  
        //返回Java版本
    SourceVersion getSourceVersion();
  
        //返回当前语言环境或者null(没有语言环境)
    Locale getLocale();
}
  • RoundEnvironment
/**用于获取注解所标注的内容信息**/
public interface RoundEnvironment {
    boolean processingOver();
        
    //返回上一轮注解处理器是否产生错误
    boolean errorRaised();

    //返回上一轮注解处理器生成的根元素
    Set<? extends Element> getRootElements();

    //返回包含指定注解类型的元素的集合
    Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);
        
    //返回包含指定注解类型的元素的集合
    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);
}
  • Element

Element代表一个静态的,语言级别的构件,对于Java源文件来说,Element代表程序元素:包,类,方法都是一种程序元素

VariableElement:代表一个字段,枚举常量,方法或者构造方法的参数,局部变量及异常参数等元素

PackageElement:代表包元素

TypeElement:代表类或接口元素

ExecutableExement:代表方法,构造函数,类或接口的初始化代码块等元素,也包括注解类型元素

  • TypeMirror

TypeMirror代表java语言中的类型。Types包括基本类型、声明类型(类类型和接口类型)、数组、类型变量和空类型。 也代表通配类型参数,可执行文件的签名和返回类型等。TypeMirror类中最重要的是getKind()方法, 该方法返回TypeKind类型

简单来说,Element代表源代码,TypeElement代表的是源码中的类型元素,比如类。虽然我们可以从TypeElement中获取类名, 但是TypeElement中不包含类本身的信息,比如它的父类,要想获取这信息需要借助TypeMirror,可以通过Element中的asType() 获取元素对应的TypeMirror

创建BindAdapter接口

新建一个android module,命名为butterknife

创建BindAdapter接口

package com.geekholt.butterknife;

public interface BindAdapter<T> {
    void bind(T activity);
}

处理依赖关系

  • app module
compileOnly project(':annotation')
annotationProcessor project(':processor')
api project(':butterknife')
  • processor module
api project(':annotation')

编写AnnotationProcessor

基本工作都已经做好了,我们的目标也已经很明确了,我们最终想要生成的就是像下面这样一个文件

package com.geekholt.geekknife_example;

import com.geekholt.geekknife.adapter.BindAdapter;

public class MainActivity$BindAdapterImp implement BindAdapter<MainActivity>{
        public void bind(MainActivity activity) {
                activity.textView = activity.findViewById(R.id.txt_test);
            activity.button = activity.findViewById(R.id.btn_test)
    }
}

我们需要获取哪些内容呢?

  • 包名

  • 注解所在的类的类名(Activity名)

  • 注解的成员变量名(控件名)

  • 注解的元数据(资源Id)

所以,最终完成后的AnnotationProcessor就是下面这样,获取到我们需要的内容后,生成java文件,逻辑其实非常简单,只是相关的API不是很常用,可能需要熟悉一下

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.geekholt.annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ButterKnifeProcessor extends AbstractProcessor {

    private Filer mFiler;
    private Messager mMessager;
    private Elements mElementUtils;

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : bindViewElements) {
            //1.获取包名
            PackageElement packageElement = mElementUtils.getPackageOf(element);
            String packName = packageElement.getQualifiedName().toString();
            print(String.format("package = %s", packName));

            //2.注解所在的类的类名
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String className = enclosingElement.getSimpleName().toString();
            print(String.format("enclosindClass = %s", enclosingElement));


            //因为BindView只作用于filed,所以这里可直接进行强转
            VariableElement bindViewElement = (VariableElement) element;
            //3.获取注解的成员变量名
            String fieldName = bindViewElement.getSimpleName().toString();

            //4.获取注解元数据
            BindView bindView = element.getAnnotation(BindView.class);
            int id = bindView.value();
            print(String.format("%s = %d", fieldName, id));

            //4.生成文件
            createFile(packName, className, fieldName, id);
            return true;
        }
        return false;
    }

  
    /**创建文件**/
    private void createFile(String packName, String className, String fieldName, int id) {
        try {
            String newClassName = className + "$BindAdapterImp";
            JavaFileObject jfo = mFiler.createSourceFile(packName + "." + newClassName, new Element[]{});
            Writer writer = jfo.openWriter();
            writer.write("package " + packName + ";");
            writer.write("\n\n");
            writer.write("import com.geekholt.butterknife.BindAdapter;");
            writer.write("\n\n\n");
            writer.write("public class " + newClassName + " implements BindAdapter<" + className + "> {");
            writer.write("\n\n");
            writer.write("public void bind(" + className + " target) {");
            writer.write("target." + fieldName + " = target.findViewById(" + id + ");");
            writer.write("\n");
            writer.write("  }");
            writer.write("\n\n");
            writer.write("}");
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

        /**打印编译期间的日志**/
    private void print(String msg) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
    }


}

创建文件推荐使用javapoet:https://github.com/square/javapoet

rebuild一下项目,在相关目录下就可以看到我们想要的文件就已经成功生成了

反射调用生成的代码

接下来的内容其实我们一开始就已经说过了,我们需要在运行时通过反射调用我们编译期生成的类

butterKnife module下创建ButterKnife

public class ButterKnife {
    private static final String SUFFIX = "$BindAdapterImp";
    //做了一个缓存,只有第一次bind时才通过反射创建对象
    static Map<Class, BindAdapter> mBindCache = new HashMap();

    public static void bind(Activity target) {
        BindAdapter bindAdapter = null;
        if (mBindCache.get(target) != null) {
            //如果缓存中有activity,从缓存中取
            bindAdapter = mBindCache.get(target);
        } else {
            //缓存中没有,创建一个
            try {
                String adapterClassName = target.getClass().getName() + SUFFIX;
                Class<?> aClass = Class.forName(adapterClassName);
                bindAdapter = (BindAdapter) aClass.newInstance();
                mBindCache.put(aClass, bindAdapter);
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        //调用bind
        if (bindAdapter != null) {
            bindAdapter.bind(target);
        }
    }
}

在我们的MainActivity中调用ButterKnife.bind(this)

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.txt_main)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        textView.setText("Hello ButterKnife");
    }
}

运行一下项目,看,“ButterKnife”就顺利工作了!是不是比想象的简单呢!

运行结果

项目完整地址

https://github.com/Geekholt/ButterKnife

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