5.Android框架ButterKnife源代码分析-创新互联

一. ButterKnife介绍

成都创新互联公司是创新、创意、研发型一体的综合型网站建设公司,自成立以来公司不断探索创新,始终坚持为客户提供满意周到的服务,在本地打下了良好的口碑,在过去的十载时间我们累计服务了上千家以及全国政企客户,如卫生间隔断等企业单位,完善的项目管理流程,严格把控项目进度与质量监控加上过硬的技术实力获得客户的一致赞扬。

在Android编程过程中,我们会写大量的布局和点击事件,像初始view、设置view监听这样简单而重复的操作,这些代码繁琐而又不雅观,比如:

TextView tvSetName = findViewById(R.id.xxx);
tvSetName.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //xxxx
    }
});
TextView tvSetAge = findViewById(R.id.xxx);
tvSetAge.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //xxxx
    }
});
TextView tvSetArea = findViewById(R.id.xxx);
tvSetArea.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //xxxx
    }
});

Activity中这种代码多了之后,很不雅观。

二. 使用简介

ButterKnife使用方法比较简单,主要包括以下步骤:

  1. 引用ButterKnife包

  2. 在onCreate里面bind(setContentView之后)

  3. 绑定各种事件

  4. onDestroy里面解绑释放资源

ButterKnife使用方法

三. ButterKnife源代码下载

ButterKnife github源代码地址

直接git clone或者下载zip即可。

四. 编译

  1. Android studio打开ButterKnife源代码

    AndroidStudio->File->open->ButterKnife源代码路径->确认

  2. Build->Rebuild Project

五. 生成的aar和jar包

生成的包主要有两个

  1. butterknife-annotations-8.5.2-SNAPSHOT.jar

    路径:butterknife-annotations->build->libs

  2. butterknife-release.aar

    路径: butterknife->build->outputs->aar

六. 其他应用引用自定义ButterKnife包

  1. 删除原来ButterKnife包引用,因为要使用自己编译的包

  2. //    compile 'com.jakewharton:butterknife:8.4.0'

  3. 拷贝文件

    拷贝上面两个文件到自己项目app模块的libs 目录

  4. 添加aar的关联

    打开app模块的build.gradle文件,添加:

  5. compile fileTree(include: ['*.jar'], dir: 'libs')
    compile(name: 'butterknife-release', ext: 'aar')
  6. Build->Rebuild Project

七. 源代码分析

1. ButterKnife.bind(Activity target)过程

文件名:ButterKnife.java

static final Map, Constructor> BINDINGS = new LinkedHashMap<>();

public static Unbinder bind(@NonNull Activity target) {
    Log.d("Sandy", "ButterKnife bind.. target: " + target);
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
}




private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.", cause);
    }
}


private static Constructor findBindingConstructorForClass(Class cls) {
    Constructor bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
        Log.d("Sandy", "findBindingConsForClass: " + clsName + " vindBinding name: " +
                clsName + "_ViewBinding");
      Class bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor) 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;
}

上面这段代码有几个注意点:

a. sourceView代表是DecorView,也就是我们窗口的顶级View。

b. findBindingConstructorForClass有个BINDINGS缓存,key是class,value是缓存的Unbinder对象,这样做可以加快bind速度。

因为每个类的ButterKnife注解在运行期间是不会变的,比如MainActivity有3个ButterKnife注解,那么它就是3个。除非有新的apk安装。

所以适合用缓存来实现。

c. findBindingConstructorForClass使用了递归的方法

这个方法使用了递归,不断调用父类,也就是

catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
}

那为什么要这么处理呢?

因为有些Activity没有ButterKnife的注解,但是它的父类可能有,比如BaseActivity。所以需要往上递归。那什么时候递归结束呢?

if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
}

如果缓存里面找到了结果,那么结束,同时返回结果;

或者类名以"android."或者"java."开头,也结束,返回null;

以Activity为例,Activity的类名是android.app.Activity,所以你的MainActivity如果递归到Activity还没有找到ButterKnife注解,那就说明你的MainActivity是没有包含ButterKnife注解的。

d. 如果子Activity和父Activity都有ButterKnife注解怎么办?

答案是返回子Activity以及其对应的 Constructor bindingCtor对象

那它的父Activity如果也有ButterKnife注解怎么办?怎么解析父Activity的ButterKnife注解呢? 这个问题我们待会再讲。

记为问题1。

e. Constructor 是个什么东西?

调用ButterKnife.bind(Activity target)方法后会返回一个Unbinder对象,可以在onDestroy中调用unbind()方法,那个Unbinder是什么东西呢?这个问题待会再讲。

记为问题2.

f. clsName + "_ViewBinding"是什么类?

Class bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");

这个问题记为问题3.

2. ButterKnifeProcessor.java

这个类是ButterKnife里面很重要的一个类了,它继承自AbstractProcessor。

看来不懂的问题越来越多,那么有必要来学习下Java注解的知识。

八. Java注解

在分析ButterKnife代码前,需要了解Java的注解,需要了解Annotation Processor,因为ButterKnifer用到这个知识。

这里面很重要的一个知识点就是你可以编写一定的规则,让它在应用程序编译时执行你的规则,然后生成Java代码;并且生成的Java还可以参与编译。

Java注解

九. 自己定义的注解框架

1. Eclipse实现

主要是参考这篇帖子完成的,大家可以参考这篇帖子:

Eclipse中使用Java注解Processor

主要说下不同的地方:

a. source folder的创建,直接File->New->source folder一直创建不成功,后面用另外一种方法创建成功了。

项目->右击->Properties->Java Build Path->Source->Add Folder->Create New Folder->输入resources/META-INF/services->finish->ok->ok

5. Android 框架ButterKnife源代码分析

2. Android studio实现

AndroidStudio下面使用Java注解Processor

十. 调试自己的自定义框架

有个时候需要调试自己写的框架是否正常运行,下面介绍下调试:

1. 在项目gradle.properties里面添加

org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8011

2.Edit Configureations

5. Android 框架ButterKnife源代码分析

3. 增加远程调试

5. Android 框架ButterKnife源代码分析

4. 启动远程调试

5. Android 框架ButterKnife源代码分析

下面的控制台会出现下面的提示:

Connected to the target VM, address: 'localhost:8011', transport: 'socket'

5. 打断点

在Processor里面打上断点,比如init, process

6. 连上手机,项目根目录命令行下执行

gradle clean connectedCheck

十一. ButterKnife使用Java注解

理解了Java注解Processor之后,ButterKnife就比较好理解了。

首先它的ButterKnifeProcessor.java继承自AbstractProcessor,重写了init和process之类的方法。

也就是说它在编译的时候会被执行,生成辅助代码。

它的辅助代码生成到哪里了呢?

在我们自己的应用程序里面搜索_ViewBinding,就可以找到已经生成好的辅助类,如下:

public class xxxx_ViewBinding extends BaseActivity_ViewBinding {
  private View view2131689593;

  @UiThread
  public xxxx_ViewBinding(final T target, View source) {
    super(target, source);

    View view;
    target.mEtUser = Utils.findRequiredViewAsType(source, R.id.et_user, "field 'mEtUser'", EditText.class);
    target.mEtPwd = Utils.findRequiredViewAsType(source, R.id.et_pwd, "field 'mEtPwd'", EditText.class);
    view = Utils.findRequiredView(source, R.id.btn_login, "method 'btn_login' and method 'btn_login_long'");
    view2131689593 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.btn_login();
      }
    });
    view.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View p0) {
        return target.btn_login_long();
      }
    });
  }

  @Override
  public void unbind() {
    T target = this.target;
    super.unbind();

    target.mEtUser = null;
    target.mEtPwd = null;

    view2131689593.setOnClickListener(null);
    view2131689593.setOnLongClickListener(null);
    view2131689593 = null;
  }
}

这个类在编译的时候会被自动生成,那么在运行的时候,它会被调用。

这个类的构造函数会去初始化那些控件,设置监听。

回到第七步 ButterKnife.bind()的过程

在createBinding的时候,它会初始化这个xxx_ViewBinding类,如下:

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
      ...
      
  }

那么就会走到xxx_ViewBinding的构造函数,那么就会初始化控件,同时也会设置监听。如下:

target.mEtUser = Utils.findRequiredViewAsType(source, R.id.et_user, "field 'mEtUser'", EditText.class);
    target.mEtPwd = Utils.findRequiredViewAsType(source, R.id.et_pwd, "field 'mEtPwd'", EditText.class);
    view = Utils.findRequiredView(source, R.id.btn_login, "method 'btn_login' and method 'btn_login_long'");
    view2131689593 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.btn_login();
      }
    });
    view.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View p0) {
        return target.btn_login_long();
      }
    });

它的调用方式直接是target.mEtpwd,所以也就是说Activity的mEtpwd控件不能是private的,否则会引用不到。

参考网址:

butterknife github源代码下载

ButterKnife源代码解析

Java注解处理器分析

Eclipse中使用Java注解处理器

Android studio使用java注解处理器

JavaPoet介绍

调试Java注解处理器出错

另外有需要云服务器可以了解下创新互联scvps.cn,海内外云服务器15元起步,三天无理由+7*72小时售后在线,公司持有idc许可证,提供“云服务器、裸金属服务器、高防服务器、香港服务器、美国服务器、虚拟主机、免备案服务器”等云主机租用服务以及企业上云的综合解决方案,具有“安全稳定、简单易用、服务可用性高、性价比高”等特点与优势,专为企业上云打造定制,能够满足用户丰富、多元化的应用场景需求。


当前文章:5.Android框架ButterKnife源代码分析-创新互联
文章路径:http://ybzwz.com/article/dggpgi.html