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

三分钟的介绍

相信很多人都在开发中都使用过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的有效性等等,代码还存在很多优化的地方,今天就到这里了,有问题可以留言一起探讨探讨。。。

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

推荐阅读更多精彩内容