ButterKnife可以大量的减少android开发过程中findViewById之类的枯燥代码。推荐在每一个android项目中使用,可以提高不少工作效率。ButterKnife的官网地址为:ButterKnife

重要类和结构

butterknife-annotations module

该子项目定义了butterknife所需要的注解,其中注解包括两类,一类用于绑定resource(例如:BindArray, BindBitmap, BindBool…), 一类用来view绑定回调(例如:OnClick, OnCheckedChanged,onItemclick…)

butterknife-compiler

该子项目定义了注解编译器ButterKnifeProcess,用于在编译阶段,对项目代码进行注解处理,生成一系列对应的java文件

1
2
3
QualifiedId:
packageName:资源所在项目的包名
id:某个资源对应的编号

1
2
3
4
Id:
value:某个资源对应的编号
code:资源名称对应的CodeBlock,例如com.im.demo.R.string.title
qualifed:如果code是CodeBlock方式引用,则为true,否则如果为直接使用编号值,则为false
1
2
3
4
5
6
7
8
FieldResourceBinding (implement ResourceBinding):
定义enum类型Type,用于描述获取资源的方式,例如getInteger,getStringArray等
Field:
id:Id类型
name:目标变量名称,即使用bind资源注解的变量
type:Type
Method:
render(int sdk):返回CodeBlock,即生成赋值name对应资源的代码
1
2
3
4
5
6
BindingSet:
Builder:
该类用于配置所有需要生成ViewBinding类的信息。包括绑定目标类信息,以及有各类注解中获取的绑定resource,view,listener等信息
Field:
viewIdMap:用于记录对应viewID以及绑定到的目标域的信息的映射
resourceBindings:用于记录绑定到改类的所有资源绑定信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ButterKnifeProcessor:
Field:
symbols:Map<QualifiedId, Id>
Method:
继承AbstractProcessor的process方法中开始处理注解信息。
调用findAndParseTargets
调用scanForRClasses:通过RClassScanner生成一个映射关系package->Set<R className>
调用parseRClass遍历解析scanForRClasses结果中每一个RClass。
其中通过IDScanner,获取每一个R class下定义的子类,
然后再次通过VarScanner获取子类中定义的resource key和对应的编号,统一保存在symbols字段中
生成symbols是非常重要的一个步骤,因为通过symbols,就可以生成任何一个资源id对应的绑定代码
分别调用parseResourceArrayparseResourceBitmap等接口解析每一个资源类注解,
调用findAndParseListener处理回调类注解,其中使用了一个缓存类BuilderMap
缓存目标字段所在类对应的ViewBinding类信息,实际上为BindingSet.Builder,该核心类下面给出了源代码
通过队列的方式处理BuilderMap,生成BindingMap
之所以只用队列的方式,是需要保证BuilderMap中的存在parent-child关系的类时,parent类被先处理
下方给出了源代码
轮训BindingMap,调用BindingSetbrewJava生成javaFile(主要利用javapoet库生成对应的ViewBinding类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//核心代码,将BuilderMap生成BindinMap
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
// which starts at the roots (superclasses) and walks to the leafs (subclasses).
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
核心类BindingSet.Builder
static final class Builder {
private final TypeName targetTypeName;
private final ClassName bindingClassName;
private final boolean isFinal;
private final boolean isView;
private final boolean isActivity;
private final boolean isDialog;
private BindingSet parentBinding;
private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();
private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings =
ImmutableList.builder();
private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder();
private Builder(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,
boolean isView, boolean isActivity, boolean isDialog) {
this.targetTypeName = targetTypeName;
this.bindingClassName = bindingClassName;
this.isFinal = isFinal;
this.isView = isView;
this.isActivity = isActivity;
this.isDialog = isDialog;
}
void addField(Id id, FieldViewBinding binding) {
getOrCreateViewBindings(id).setFieldBinding(binding);
}
void addFieldCollection(FieldCollectionViewBinding binding) {
collectionBindings.add(binding);
}
boolean addMethod(
Id id,
ListenerClass listener,
ListenerMethod method,
MethodViewBinding binding) {
ViewBinding.Builder viewBinding = getOrCreateViewBindings(id);
if (viewBinding.hasMethodBinding(listener, method) && !"void".equals(method.returnType())) {
return false;
}
viewBinding.addMethodBinding(listener, method, binding);
return true;
}
void addResource(ResourceBinding binding) {
resourceBindings.add(binding);
}
void setParent(BindingSet parent) {
this.parentBinding = parent;
}
String findExistingBindingName(Id id) {
ViewBinding.Builder builder = viewIdMap.get(id);
if (builder == null) {
return null;
}
FieldViewBinding fieldBinding = builder.fieldBinding;
if (fieldBinding == null) {
return null;
}
return fieldBinding.getName();
}
private ViewBinding.Builder getOrCreateViewBindings(Id id) {
ViewBinding.Builder viewId = viewIdMap.get(id);
if (viewId == null) {
viewId = new ViewBinding.Builder(id);
viewIdMap.put(id, viewId);
}
return viewId;
}
BindingSet build() {
ImmutableList.Builder<ViewBinding> viewBindings = ImmutableList.builder();
for (ViewBinding.Builder builder : viewIdMap.values()) {
viewBindings.add(builder.build());
}
return new BindingSet(targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog,
viewBindings.build(), collectionBindings.build(), resourceBindings.build(),
parentBinding);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//这里给出一个生成的ViewBinding类的实例
public class G_ViewBinding extends E_ViewBinding {
private G target;
private View view16908290;
@UiThread
public G_ViewBinding(final G target, View source) {
super(target, source);
this.target = target;
View view;
target.button2 = Utils.findRequiredView(source, android.R.id.button2, "field 'button2'");
view = Utils.findRequiredView(source, android.R.id.content, "method 'onClick'");
view16908290 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onClick();
}
});
Context context = source.getContext();
target.grayColor = ContextCompat.getColor(context, android.R.color.darker_gray);
}
@Override
public void unbind() {
G target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.button2 = null;
view16908290.setOnClickListener(null);
view16908290 = null;
super.unbind();
}
}

butterknife module

  1. Butterknife
    暴露给用户的主要接口类,主要提供各种bind接口
    • BINDINGS:Map, Constructor<? extends Unbinder>>
      映射关系,class->unbinder,用于缓存之前解析过的class的结果。
    • createBinding(Object target, View source)
      所有bind接口最终都会进入该方法中。该方法会调用findBindingConstructorForClass方法,该方法通过反射机制获取对应ViewBinding类,进而获取对应的ViewBinding类的Constructor。在createBinding的最后调用改constructor实例化对应的Viewbinding类。由于每一个ViewBinding类都集成了Unbinder接口,因此返回的是一个Unbinder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//通过反射机制获取对应目标类的viewbinding类
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> 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 {
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//找到ViewBinding类之后,通过ViewBinding类帮助target类进行资源的绑定以及回调的绑定
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<? extends Unbinder> 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);
}
}

学习知识点

android-apt切换为annotationProcessor

当gradle插件版本升级到2.2之上后,可以使用androidProcessor替换android-apt。具体做法如下

  1. 修改根目录的gradle插件版本,并移除android-apt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'com.android.tools.build:gradle:2.2.2'
    //remove android-apt
    //classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
    }
  2. 移除对应Module的build.gradle中对android-apt配置

    1
    2
    //remove
    //apply plugin: 'com.neenbedankt.android-apt'
  3. 修改对应build.gradle的dependencies中原使用apt地方,换成annotationProcessor

    1
    2
    3
    4
    5
    6
    dependencies {
    // update apt to annotationProcessor
    //apt 'com.google.dagger:dagger-compiler:2.2'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.2'
    compile 'com.google.dagger:dagger:2.2
    }

gradle plugin checkstyle

  1. checkstyle plugin提供了一些task用于对project质量进行测试,使用方式如下:

    1
    2
    3
    4
    5
    6
    7
    ...
    apply plugin: 'checkstyle'
    ...
    checkstyle { //指定checkstyle一些配置
    configFile rootProject.file('checkstyle.xml')
    showViolations true
    }
  2. 具体的checkstyle说明可以参考checkstyle

gradle from的使用

  1. 当多个子项目具有重复的gradle配置代码时,可以将重复部分提取出来,例如将多个子项目的upload maven代码统一提取出来,放到project项目的如下,例如放到项目根目录的gradle/gradle-maven-push.gradle:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    apply plugin: 'maven'
    apply plugin: 'signing'
    afterEvaluate { project ->
    uploadArchives {
    repositories {
    mavenDeployer {
    beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
    pom.groupId = GROUP
    pom.artifactId = POM_ARTIFACT_ID
    pom.version = VERSION_NAME
    repository(url: RELEASE_REPOSITORY_URL) {
    authentication(userName: NEXUS_USERNAME, password: NEXUS_PASSWORD)
    }
    snapshotRepository(url: SNAPSHOT_REPOSITORY_URL) {
    authentication(userName: NEXUS_USERNAME, password: NEXUS_PASSWORD)
    }
    pom.whenConfigured { pom ->
    pom.dependencies.forEach { dep ->
    if (dep.getVersion() == "unspecified") {
    dep.setGroupId(GROUP)
    dep.setVersion(VERSION_NAME)
    }
    }
    }
    }
    }
    }
    signing {
    required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
    sign configurations.archives
    }
    }
  2. 在子项目的build.gradle中进行gradle from引用,例如

    1
    apply from: rootProject.file('gradle/gradle-mvn-push.gradle')

第三方类库

  1. AutoService
    A configuration/metadata generator for java.util.ServiceLoader-style service providers

  2. JavaPoet
    JavaPoet is a Java API for generating .java source files.
    Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.

  3. Auto-common
    The Auto project has a set of common utilities to help ease use of the annotation processing environment.

  4. robolectric
    Robolectric is a testing framework that de-fangs the Android SDK so you can test-drive the development of your Android app.