手写注入框架以ButterKnife为例(详)

2018-01-09  本文已影响0人  南北嘉木

前言

在开发经常碰到注解形式,比如我们代码中常见@Override,也比如我之前发表的Retrofit之解析xml (详细)全是注解操作,还有很多第三方注解注入框架,像ButterKnife,Dagger2等,注解的使用很广泛,今日以ButterKnife为例,来探其究竟.

准备知识

java注解是在java5之后引入的,对于java注解完全不了解的请看秒懂,Java 注解 (Annotation)你可以这样学,看完可了解注解的基本用法.注解分为运行时注解和编译时注解,此处ButterKnife为编译时注解.

核心原理

编译时注解是通过APT(Annotation Processing Tools)实现. APT是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

项目架构

在Inject中关联inject-annotion 在Inject-compiler中引入jar包,如图

Android库Inject处理

Inject为使用方提供方法,先看是MainAcitity中是如何调用的

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.text)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        Toast.makeText(this,"--->  "+textview, Toast.LENGTH_SHORT).show();
        textview.setText("6666666");
    }
}

所以此处在InjectView对应处理是,提供相应方法

public class InjectView {
    public  static  void bind(Activity activity)
    {
        String clsName=activity.getClass().getName();//反射拿到使用方类名
        try {
            Class<?> viewBidClass= Class.forName(clsName+"$$ViewBinder");//反射拿到新生成的内部类
            ViewBinder viewBinder= (ViewBinder) viewBidClass.newInstance();//反射后类型
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }
}

ViewBinder是为了接收拿到的反射后的类型

public interface ViewBinder <T>{
    void  bind(T tartget);
}

Java库inject-annotion处理

inject-annotion负责提供注解,此处只创建了BindView注解

@Target(ElementType.FIELD)//表示可以给属性进行注解
@Retention(RetentionPolicy.CLASS)//代表运行在编译时
public @interface BindView {
    int value();
}

Java库Inject-compiler处理

Inject-compiler中处理核心逻辑,基本原理是,在编译时javac编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理.
此处BindViewProcessor继承了AbstractProcessor,并通过@AutoService(Processor.class),表明自身为注解处理器,其中主要有四个方法,在init方法中做初始化;在getSupportedAnnotationTypes方法中指明支持哪些注解,此处指明的注解即为BindView;在getSupportedSourceVersion方法中返回最新的JDK版本;在process方法中做主逻辑处理

//用@AutoService 指定注解处理器
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    /**
     * 处理Element工具类
     * 主要获取包名
     */
    private Elements elementUtils;
    /**
     * Java文件输出类(非常重要)生成它是用来java文件
     */
    private Filer    filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elementUtils = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();
    }

    /**
     * @return 当前注解处理器  支持哪些注解
     * 返回的是set字符串集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        //        types.add(Override.class.getCanonicalName());
        return types;
    }

    /**
     * 支持jdk版本
     * 一般选择最新版本
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * javac编译器  遇到含有BindView注解的java文件时,就会调用这个process方法
     *
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //声明一个缓存的map集合   键  是Activity   值  是当前Activity 里面含有BndView成员变量的集合
        Map<TypeElement, List<FieldViewBinding>> targetMap = new HashMap<>();
        //编译时只能通过文件输出打印,不能通过Log,System.out.print打印
        FileUtils.print("------------>    ");
        //for循环带有BindView注解的Element
        //将每个Element进行分组。分组的形式 是将在一个Activit的Element分为一组
        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
            FileUtils.print("elment   " + element.getSimpleName().toString());
            //  enClosingElement可以理解成Element
            TypeElement enClosingElement = (TypeElement) element.getEnclosingElement();
            // List<FieldViewBinding>   当前Activity 含有注解的成员变量集合
            List<FieldViewBinding> list = targetMap.get(enClosingElement);
            if (list == null) {
                list = new ArrayList<>();
                targetMap.put(enClosingElement, list);//
            }
            //得到包名
            String packageName = getPackageName(enClosingElement);
            //得到id
            int id = element.getAnnotation(BindView.class).value();
            //            得到成员变量名  TextView  text;  这里得到的是text字符串
            String fieldName = element.getSimpleName().toString();
            //            当前成员变量的类类型   可以理解成  TextView
            TypeMirror typeMirror = element.asType();
            //            封装成FieldViewBinding  类型
            FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, typeMirror, id);
            list.add(fieldViewBinding);
        }
        //遍历每一个Activity  TypeElement代表类类型
        for (Map.Entry<TypeElement, List<FieldViewBinding>> item : targetMap.entrySet()) {
            List<FieldViewBinding> list = item.getValue();

            if (list == null || list.size() == 0) {
                continue;
            }
            //enClosingElement 表示 activity
            TypeElement enClosingElement = item.getKey();
            //            得到包名
            String packageName = getPackageName(enClosingElement);
            //截取字符串    MainActivity
            String complite = getClassName(enClosingElement, packageName);
            //遵循Javapoet规范,MainActivity为类类型  在这里封装成ClassName
            ClassName className = ClassName.bestGuess(complite);
            //          ViewBinder类型
            ClassName viewBinder = ClassName.get("com.base.inject", "ViewBinder");
            //            开始构建java文件
            //            从外层包名  类名开始构建
            TypeSpec.Builder result = TypeSpec.classBuilder(complite + "$$ViewBinder")
                    .addModifiers(Modifier.PUBLIC)
                    .addTypeVariable(TypeVariableName.get("T", className))
                    .addSuperinterface(ParameterizedTypeName.get(viewBinder, className));
            //          构建方法名
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID)
                    .addAnnotation(Override.class)
                    .addParameter(className, "target", Modifier.FINAL);
            //            构建方法里面的具体逻辑,这里的逻辑代表findViewById
            for (int i = 0; i < list.size(); i++) {
                FieldViewBinding fieldViewBinding = list.get(i);
                //-->android.text.TextView
                String pacckageNameString = fieldViewBinding.getType().toString();
                ClassName viewClass = ClassName.bestGuess(pacckageNameString);
                //$L  代表占位符  和StringFormater类似。$L代表基本类型  $T代表  类类型
                methodBuilder.addStatement
                        ("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName()
                                , viewClass, fieldViewBinding.getResId());
            }

            result.addMethod(methodBuilder.build());

            try {
                //生成Java文件头部的注释说明,装逼用
                JavaFile.builder(packageName, result.build())
                        .addFileComment("auto create make")
                        .build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }


        }
        return false;
    }

    //enClosingElement.getQualifiedName().toString()返回的是com.example.administrator.butterdepends.MainActivity
    private String getClassName(TypeElement enClosingElement, String packageName) {
        int packageLength = packageName.length() + 1;
        //        replace(".","$")的意思是  如果当前的enClosing为内部类的话
        //       裁剪掉包名和最后一个点号,将去掉包名后,后面还有点号则替换成$符号
        return enClosingElement.getQualifiedName().toString().substring(packageLength).replace(".", "$");
    }

    private String getPackageName(TypeElement enClosingElement) {
        //返回的是  com.example.administrator.butterknifeframwork。通过工具类获取的
        return elementUtils.getPackageOf(enClosingElement).getQualifiedName().toString();
    }
}

此处重点说明process方法,第一个for循环用来拿到所有使用方class,以及该class调用的所有注解,所以建立一个map集合,TypeElement对应的是使用方class文件,List<FieldViewBinding>>代表该classs文件中用到的所有注解.最早返回的是Element形式,此处将其转化为FieldViewBinding来存储.
FieldViewBinding代码

public class FieldViewBinding {
   private String name;//  textview
   private TypeMirror type ;//--->TextView
   private int resId;//--->R.id.textiew

   public FieldViewBinding(String name, TypeMirror type, int resId) {
       this.name = name;
       this.type = type;
       this.resId = resId;
   }

   public String getName() {
       return name;
   }

   public TypeMirror getType() {
       return type;
   }

   public int getResId() {
       return resId;
   }
}
第二个for循环是生成每个调用方class相对应的内部类class文件,是由外往内拼接生成的,先拼接创建包名类名,然后拼接构建方法名,最终的拼接效果为

代码运行成功可以看到这个新生成的class文件,其实就是一个对应的内部类
注意:在jar包中测试代码不能使用log日志,所以此处使用FileUtils,以文件输出打印,会在桌面生成一个log.txt文件显示所有log信息,以便于代码出bug时进行调试,奉上FileUtils代码

public class FileUtils {
    public static void print(String text)
    {
        File file=new File("C:\\Users\\Administrator\\Desktop\\log.txt");
        if(!file.exists())
        {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            FileWriter fileWriter=new FileWriter(file.getAbsoluteFile(),true);
            fileWriter.write(text+"\n");
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

以上为手写注入ButterKnife的全部流程,其实并不复杂,因为只是简单实现效果,Dagger2等也是相同的道理

此框架和ButterKnife使用方法是一致的,使用时别忘了在gradle中加入

结尾

奉上代码地址https://github.com/HaowangSmith/HaowangButterKnife
相关知识链接ButterKnife源码分析

上一篇下一篇

猜你喜欢

热点阅读