美文网首页
04 IOC架构设计【ButterKnife原理、自己手撸一个】

04 IOC架构设计【ButterKnife原理、自己手撸一个】

作者: 西瓜家族 | 来源:发表于2020-03-04 02:00 被阅读0次

    三分钟的介绍

    相信很多人都在开发中都使用过ButterKnife吧!没有用过的也都听过。ButterKnife是一个专注于Android系统的View注入框架,以前总是要写很多findViewById来找到View对象,有了ButterKnife可以很轻松的省去这些步骤。

    说说人家的优点

    1. 简化代码,提升开发效率
      强大的View绑定和Click事件处理功能

    2. 不会影响app运行效率
      ButterKnife采用编译时注解的方式生成代码,运行是不会影响App效率

    用法

    GitHub地址(Star 25.1k):https://github.com/JakeWharton/butterknife

    class ExampleActivity extends Activity {
      @BindView(R.id.user) EditText username;
      @BindView(R.id.pass) EditText password;
    
      @BindString(R.string.login_error) String loginErrorMessage;
    
      @OnClick(R.id.submit) void submit() {
        // TODO call server...
      }
    
      @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.simple_activity);
        ButterKnife.bind(this);
        // TODO Use fields...
      }
    }
    

    原理

    仅仅一个注解加一行代码可以实现 findViewById,它们都做了哪些事情呢?
    1、给元素加注解标记
    2、收集注解的元素生成Java类(编译器执行)
    3、动态注入
    源码走一波
    第一步:加注解没啥好说的,过
    第二步:收集注解生成Java类

    在编译时,通过处理注解元素,生成新的 Java 代码类,该Java代码 里面包含了我们的 findViewById(R.id.xxx)、view.setonclickListener(new lis... )的这些动作;

    ButterKnifeProcessor.java(GitHub中的源码)

    public boolean process(Set<? extends TypeElement> set, RoundEnvironment env) {
            print("process:");
            print("env"+env.getRootElements());
            Map<TypeElement, List<FieldBinding>> map = new HashMap<>();
    
            for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
                //get the Activity
                TypeElement activityElement = (TypeElement) element.getEnclosingElement();
                print(" activityElement:"+ activityElement.toString());
                List<FieldBinding> list = map.get(activityElement);
                if (list == null) {
                    list = new ArrayList<>();
                    map.put(activityElement, list);
                }
                //get  id
                int id = element.getAnnotation(BindView.class).value();
                //get fieldName
                String fieldName = element.getSimpleName().toString();
                //get mirror
    
                TypeMirror typeMirror = element.asType();
                print(" typeMirror:"+ typeMirror);
                FieldBinding fieldBinding = new FieldBinding(fieldName, typeMirror, id);
                list.add(fieldBinding);
            }
    
            for (Map.Entry<TypeElement, List<FieldBinding>> item :
                    map.entrySet()) {
                TypeElement activityElement = item.getKey();
    
                //get packageName
                String packageName = elementUtils.getPackageOf(activityElement).getQualifiedName().toString();
                //get  activityName
                String activityName = activityElement.getSimpleName().toString();
    
                //transfrom type Activity with system can discern
                ClassName activityClassName = ClassName.bestGuess(activityName);
                ClassName viewBuild = ClassName.get(ViewBinder.class.getPackage().getName(), ViewBinder.class.getSimpleName());    //
    
                TypeSpec.Builder result = TypeSpec.classBuilder(activityClassName + "$$ViewBinder")
                        .addModifiers(Modifier.PUBLIC)
                        .addTypeVariable(TypeVariableName.get("T", activityClassName))
                        .addSuperinterface(ParameterizedTypeName.get(viewBuild,activityClassName));
    
                MethodSpec.Builder method = methodBuilder("bind")      //methodName
                        .addModifiers(Modifier.PUBLIC)                          // modifier
                        .returns(TypeName.VOID)
                        .addAnnotation(Override.class)
                        .addParameter(activityClassName, "target", Modifier.FINAL);
    //
                List<FieldBinding> list = item.getValue();
                for (FieldBinding fieldBinding : list) {
                    //
                    String pacageName = fieldBinding.getType().toString();
                    ClassName viewClass = ClassName.bestGuess(pacageName);
    
                    method.addStatement("target.$L=($T)target.findViewById($L)", fieldBinding.getName(), viewClass, fieldBinding.getResId());
    
                }
    //
                result.addMethod(method.build());
    
                try {
                    JavaFile.builder(packageName, result.build()).build().writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return true;
        }
    

    第三步:就是动态注入, ButterKnife.bind(this); 源码中最后会通过反射加载一个***_ViewBinding这个类

    private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
        Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
        if (bindingCtor != null || BINDINGS.containsKey(cls)) {
          if (debug) Log.d(TAG, "HIT: Cached in binding map.");
          return bindingCtor;
        }
        String clsName = cls.getName();
        if (clsName.startsWith("android.") || clsName.startsWith("java.")
            || clsName.startsWith("androidx.")) {
          if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
          return null;
        }
        try {
          Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
          //noinspection unchecked
          bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
          if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
        } catch (ClassNotFoundException e) {
          if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
          bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
        } catch (NoSuchMethodException e) {
          throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
        }
        BINDINGS.put(cls, bindingCtor);
        return bindingCtor;
      }
    

    瞧一瞧看一看MainActivity_ViewBinding .java

    public class MainActivity_ViewBinding implements Unbinder {
     private MainActivity target;
    
     private View view7f070022;
    
     @UiThread
     public MainActivity_ViewBinding(MainActivity target) {
       this(target, target.getWindow().getDecorView());
     }
    
     @UiThread
     public MainActivity_ViewBinding(final MainActivity target, View source) {
       this.target = target;
    
       View view;
       view = Utils.findRequiredView(source, R.id.button, "field 'button' and method 'click'");
       target.button = Utils.castView(view, R.id.button, "field 'button'", Button.class);
       view7f070022 = view;
       view.setOnClickListener(new DebouncingOnClickListener() {
         @Override
         public void doClick(View p0) {
           target.click();
         }
       });
     }
    
     @Override
     @CallSuper
     public void unbind() {
       MainActivity target = this.target;
       if (target == null) throw new IllegalStateException("Bindings already cleared.");
       this.target = null;
    
       target.button = null;
    
       view7f070022.setOnClickListener(null);
       view7f070022 = null;
     }
    }
    

    看到这里已经完全明白了,为什么只需要短短的两行代码了。。。

    开启手撸模式(三步走 模式)

    第一步:我们需要创建注解,在项目中New Module -- Java Library(Library name: injectAnnotations)

    创建一个注解
    // 具体可以去了解一下注解的使用
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface InjectView {
        // 我们这里也使用了android提供的一些注解
        @IdRes
        int value();
    }
    

    官方提供了很多特别好用的类或注解,这里说的support annotation就是特别好的工具,多使用其中的注解,需要在gradle中加入

      implementation 'com.android.support:support-annotations:25.2.0'
    

    第二步:注解生成器,收集所有的注解,生成Java文件
    在项目中New Module -- Java Library(Library name: injectCompiler)
    思考一下,我们的注解Module需要提供给app使用,注解生成器Module也提供给app
    那么我们需要在app的gradle中加入

         implementation project(':injectAnnotations')
        // annotationProcessor表示这是编译时的注解处理器
        annotationProcessor project(':injectCompiler')
    

    注解生成器Module也需要知道我都需要处理哪些注解,所以需要在gradle中引入inject-annotations

    dependencies {
        implementation fileTree(include: ['*.jar'], dir: 'libs')
        implementation project(':injectAnnotations')
    
        implementation "com.google.auto.service:auto-service:1.0-rc4"//自动配置的
        annotationProcessor "com.google.auto.service:auto-service:1.0-rc4" //这个在gradle5.0以上需要的
        implementation 'com.squareup:javapoet:1.11.1'//方便编写代码的
    }
    
    //  解决build 错误:编码GBK的不可映射字符
    tasks.withType(JavaCompile) {
        options.encoding = "UTF-8"
    }
    

    注解生成器配置OK了,接下来我们创建 ButterKnifeProcessor.class

    @AutoService(Processor.class)
    public class ButterKnifeProcessor extends AbstractProcessor {
    
        private Filer filer;
        private Elements mElementUtils;
    
        // 使用之前需要初始化三个动作
        // 1、支持的java版本
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        // 2、当前APT能用来处理哪些注解
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> supportTypes = new LinkedHashSet<>();
            supportTypes.add(InjectView.class.getCanonicalName());
            supportTypes.add(InjectClick.class.getCanonicalName());
            return supportTypes;
        }
    
        // 3、需要一个用来生产文件的对象
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            filer = processingEnvironment.getFiler();
            mElementUtils = processingEnvironment.getElementUtils();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            // 这里会把所有跟注解有关的field全部拿到,我们需要手动进行分类
            Set<? extends Element> viewElements = roundEnvironment.getElementsAnnotatedWith(InjectView.class);
            Set<? extends Element> clickElements = roundEnvironment.getElementsAnnotatedWith(InjectClick.class);
            // 将所有注解集合分离出以activity为单元的注解集合
            Map<Element, List<Element>> viewElementsMap  = set2Map(viewElements);
            // 将所有注解集合分离出以activity为单元,再以控件ID为单元的注解
            Map<Element, List<Element>> clickElementsMap  = set2Map(clickElements);
    
            //------------生成代码,使用Java代码生成框架-JavaPoet解析-----------
            for (Map.Entry<Element, List<Element>> entry : viewElementsMap.entrySet()) {
                Element activityElement = entry.getKey();
                List<Element> viewFieldElementList = entry.getValue();
    
                //得到类名的字符串
                String activityName = activityElement.getSimpleName().toString();
                ClassName activityClassName = ClassName.bestGuess(activityName);
    
                // 拼装这一行代码:public final class xxx_ViewBinding implements IBinder
                ClassName targetTypeName = ClassName.get("com.demo.james","IBinder");
                TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"_ViewBinding")
                        //类名前添加public final
                        .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                        //添加类的实现接口,并指定泛型的具体类型
                        .addSuperinterface(ParameterizedTypeName.get(targetTypeName, activityClassName))
                        //添加一个成员变量target
                        .addField(activityClassName, "target", Modifier.PRIVATE);
    
                // 实现IBinder的方法
                //  拼装这一行代码:public final void bind(ButterknifeActivity target)
                MethodSpec.Builder bindMethod = MethodSpec.methodBuilder("bind")//和你创建的bind中的方法名保持一致
                        .addAnnotation(Override.class)
                        .addParameter(activityClassName, "activity")
                        .addStatement("this.target = activity")
                        .addModifiers(Modifier.FINAL, Modifier.PUBLIC);
    
                // 存储已findView的控件,为添加点击事件的时候判断是否需要重新findViewById
                Map<Integer, String> findViewMap = new LinkedHashMap<>();
                // 遍历注解的字段生成findViewById
                for (Element fieldElement : viewFieldElementList) {
                    String fieldName = fieldElement.getSimpleName().toString();
                    //在构造方法中添加初始化代码
    
                    //  在bind方法中添加
                    //  target.btn = target.findViewById(2131230762);
                    // 获取注解里面的值,也就是id
                    InjectView annotation = fieldElement.getAnnotation(InjectView.class);
                    int resId = annotation.value();
                    findViewMap.put(resId, fieldName);
                    bindMethod.addStatement("target.$L = target.findViewById($L)",fieldName,resId);
                }
    
                List<Element> clickFieldElementList = clickElementsMap.get(activityElement);
                if (clickFieldElementList != null){
                    for (Element fieldElement : clickFieldElementList) {
                        System.out.println("clickFieldElementList : "+ fieldElement.getSimpleName().toString());
                        // 添加onCLickListener
                        //  target.btn.setOnClickListener(new View.OnClickListener() {
                        //      @Override
                        //      public void onClick(View view) {
                        //        target.test();
                        //      }
                        //   });
                        ClassName viewClass = ClassName.get("android.view","View");
                        TypeSpec onCLick = TypeSpec.anonymousClassBuilder("")
                                .superclass(ClassName.bestGuess("android.view.View.OnClickListener"))
                                .addMethod(MethodSpec.methodBuilder("onClick")
                                        .addAnnotation(Override.class)
                                        .addModifiers(Modifier.PUBLIC)
                                        .addParameter(viewClass, "view")
                                        .returns(void.class)
                                        .addStatement("target.$L()", fieldElement.getSimpleName().toString())
                                        .build())
                                .build();
                        InjectClick annotation = fieldElement.getAnnotation(InjectClick.class);
                        int resId = annotation.value();
                        if (findViewMap.get(resId) == null){
                            bindMethod.addStatement("target.findViewById($L).setOnClickListener($L)",resId,onCLick);
                        }else{
                            bindMethod.addStatement("target.$L.setOnClickListener($L)",findViewMap.get(resId),onCLick);
                        }
                    }
                }
                classBuilder.addMethod(bindMethod.build());
    
                //开始生成
                try {
                    //得到包名
                    String packageName = mElementUtils.getPackageOf(activityElement)
                            .getQualifiedName().toString();
                    JavaFile.builder(packageName,classBuilder.build())
                            //添加类的注释
                            .addFileComment("butterknife 自动生成")
                            .build().writeTo(filer);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return false;
        }
    
        private Map<Element, List<Element>> set2Map(Set<? extends Element> viewElements) {
            Map<Element, List<Element>> viewElementsMap  = new LinkedHashMap<>();
            for (Element fieldElement : viewElements) {
                //element.getSimpleName()得到的是这个field注解名, Button btn;  输出 btn
                System.out.println("field name : "+fieldElement.getSimpleName());
                Element activityElement = fieldElement.getEnclosingElement();
                //得到的是这个field所在类的类名
                System.out.println("activityElement name : "+activityElement.getSimpleName());
    
                //以类对象为key值存储一个类中所有的field到集合中
                List<Element> elementList = viewElementsMap.get(activityElement);
                if (elementList == null){
                    elementList = new ArrayList<>();
                    viewElementsMap.put(activityElement, elementList);
                }
                elementList.add(fieldElement);
            }
            return viewElementsMap;
        }
    }
    

    第三步:动态注入,需要提供一个给用户使用的东东

    public class JettButterKnife{
        public static void bind(Activity activity){
            String name = activity.getClass().getName() + "_ViewBinding" ;
            try {
                Class<?> clazz = Class.forName(name);
                IBinder iBinder = (IBinder) clazz.newInstance();
                iBinder.bind(activity);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
    public class ButterknifeActivity extends AppCompatActivity {
    
        @InjectView(R.id.button6)
        Button btn;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_butterknife);
            JettButterKnife.bind(this);
            btn.setText("我要赋值咯!。。。。");
        }
    
        @InjectClick(R.id.button8)
        public void test(){
            Toast.makeText(this, "点的就是我", Toast.LENGTH_LONG).show();
        }
    }
    

    基本上都已经注释了,自己手撸一个BufferKnife,实现了findViewById与onClick的注解功能。

    总结

    一晃已经凌晨两点了,熬不牢!!!
    确实一个插件需要考虑的事情非常多,不动手去做是想不到的。之前只是实现了findViewById,但是要正在加onClick的时候,还需要考虑更多。代码中还有很多验证的地方没有去做,比如:一个ID多个注解、ID的有效性等等,代码还存在很多优化的地方,今天就到这里了,有问题可以留言一起探讨探讨。。。

    相关文章

      网友评论

          本文标题:04 IOC架构设计【ButterKnife原理、自己手撸一个】

          本文链接:https://www.haomeiwen.com/subject/xfdikhtx.html