ButterKnife源码分析
Posted showCar
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ButterKnife源码分析相关的知识,希望对你有一定的参考价值。
使用Butter Knife己经很长时间了,一直很喜欢用。因为它能帮我节省很多重复性的绑定控件代码。今天就来讲讲它的源码吧。只是总结我对它源码的一个理解。先看下它的使用效果:
public TestActivity extends Activity
@Bind(R.id.top_pic)
ImageView topPic;
@Bind(R.id.top)
TextView top;
@Override
public void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
// TODO: add setContentView(...) invocation
ButterKnife.bind(this);
如上它就会自动生成以下的代码。非常方便。
TextView toPic = (TextView)super.findViewById(R.id.top_pic);
Annotation 注解
对这个不了解的同学可以看下java编程思想中的相关内容,这里简单介绍下,Annotation包含几个注解符:@Retention, @Target, @Inherited, @Documented 。
- @Documented 是否会保存到 Javadoc 文档中。
- @Retention 保留时间,可选值 SOURCE(源码时),CLASS(编译时),RUNTIME(运行时),默认为CLASS,SOURCE大都为Mark Annotation,这类Annotation大都用来校验,比如Override, SuppressWarnings。
- @Target 可以用来修饰哪些程序元素,如TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等,未标注则表示可修饰所有。
- @Inherited 是否可以被继承,默认为false。
重点关注下Retention中的参数的含义:
- SOURCE: 源码级别解析,例如@Override, @SupportWarnings。这类注解在编译成功后就不会再起作用,并且不会出现在.class文件中。
- RetentionPolicy.CLASS: 编译时解析,它解析出的代码会保留在最终的.class中,但是在运行时被忽略,所以无法在运行时获取。编译器在编译时自动查找所有继承自 AbstractProcessor 的类,然后调用他们的process方法去处理
- RetentionPolicy.RUNTIME: 运行时解析,会保留在最终的.class文件中。这类注解可以用反射API中的getAnnotations()获取到。
讲完这些知识后,就开始我们的源码之旅吧。
源码解析
解析注解
首先我们添加了@Bind(R.id.top_pic)这样的注解,看下@Bind这个注解类:
@Retention(CLASS) @Target(FIELD)
public @interface Bind
/** View ID to which the field will be bound. */
int[] value();
明白了,CLASS,编译时解析。
上面说过编译时自动查找它会调用AbstractProcessor 类中的process方法去处理相关逻辑,这个注解处理什么事情呢?关键找到AbstractProcessor 类。找找找,找到ButterKnifeProcessor。看下它的process方法:
public final class ButterKnifeProcessor extends AbstractProcessor
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env)
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env) ;
for (Map.Entry<TypeElement , BindingClass> entry : targetClassMap.entrySet())
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue() ;
try
JavaFileObject jfo = filer .createSourceFile(bindingClass.getFqcn() , typeElement);
Writer writer = jfo.openWriter() ;
writer.write(bindingClass.brewJava()) ;
writer.flush() ;
writer.close() ;
catch (IOException e)
error(typeElement, "Unable to write view binder for type %s: %s" , typeElement,
e.getMessage()) ;
return true;
上面代码主要分两步。
- findAndParseTargets解析注解。
- bindingClass.brewJava()生成ViewBinder类。
先看下findAndParseTargets具体的实现方法:
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env)
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>() ;
Set<String> erasedTargetNames = new LinkedHashSet<String>();
// Process each @Bind element.
for (Element element : env.getElementsAnnotatedWith( Bind.class ))
try
parseBind(element, targetClassMap, erasedTargetNames) ;
catch (Exception e)
logParsingError(element, Bind.class, e);
// Process each annotation that corresponds to a listener.
for (Class<? extends Annotation> listener : LISTENERS)
findAndParseListener(env, listener, targetClassMap , erasedTargetNames);
// Process each @BindBool element.
for (Element element : env.getElementsAnnotatedWith( BindBool.class ))
try
parseResourceBool(element, targetClassMap, erasedTargetNames) ;
catch (Exception e)
logParsingError(element, BindBool.class, e);
// Process each @BindColor element.
for (Element element : env.getElementsAnnotatedWith( BindColor.class ))
try
parseResourceColor(element, targetClassMap, erasedTargetNames) ;
catch (Exception e)
logParsingError(element, BindColor.class, e);
// Process each @BindDimen element.
for (Element element : env.getElementsAnnotatedWith( BindDimen.class ))
try
parseResourceDimen(element, targetClassMap, erasedTargetNames) ;
catch (Exception e)
logParsingError(element, BindDimen.class, e);
// Process each @BindDrawable element.
for (Element element : env.getElementsAnnotatedWith( BindDrawable.class ))
try
parseResourceDrawable(element, targetClassMap, erasedTargetNames) ;
catch (Exception e)
logParsingError(element, BindDrawable.class, e);
// Process each @BindInt element.
for (Element element : env.getElementsAnnotatedWith( BindInt.class ))
try
parseResourceInt(element, targetClassMap, erasedTargetNames) ;
catch (Exception e)
logParsingError(element, BindInt.class, e);
// Process each @BindString element.
for (Element element : env.getElementsAnnotatedWith( BindString.class ))
try
parseResourceString(element, targetClassMap, erasedTargetNames) ;
catch (Exception e)
logParsingError(element, BindString.class, e);
// Try to find a parent binder for each.
for (Map.Entry<TypeElement , BindingClass> entry : targetClassMap.entrySet())
String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames) ;
if (parentClassFqcn != null)
entry.getValue().setParentViewBinder(parentClassFqcn + SUFFIX) ;
return targetClassMap;
解析Binder注释,解析完信息存在targetClassMap中。上面代码有很多解析方式,如解析drawable,解析color等。这里只以parseBind为例进行分析。
private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<String> erasedTargetNames)
// Verify common generated code restrictions.
if (isInaccessibleViaGeneratedCode( Bind.class, "fields", element)
|| isBindingInWrongPackage(Bind .class, element))
return;
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.ARRAY)
parseBindMany(element, targetClassMap, erasedTargetNames) ;
else if (LIST_TYPE.equals(doubleErasure(elementType)))
parseBindMany(element, targetClassMap, erasedTargetNames) ;
else if (isSubtypeOfType(elementType, ITERABLE_TYPE))
error(element, "@%s must be a List or array. (%s.%s)" , Bind. class.getSimpleName(),
((TypeElement) element.getEnclosingElement()).getQualifiedName() ,
element.getSimpleName()) ;
else
parseBindOne(element, targetClassMap, erasedTargetNames) ;
最终都会调用parseBindOne::
private void parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<String> erasedTargetNames)
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement() ;
// Verify that the target type extends from View.
TypeMirror elementType = element.asType() ;
if (elementType.getKind() == TypeKind.TYPEVAR)
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound() ;
if (!isSubtypeOfType(elementType , VIEW_TYPE) && !isInterface(elementType))
error(element, "@%s fields must extend from View or be an interface. (%s.%s)" ,
Bind .class.getSimpleName() , enclosingElement.getQualifiedName(), element.getSimpleName()) ;
hasError = true;
// Assemble information on the field.
int [] ids = element.getAnnotation( Bind.class ).value();
if (ids. length != 1 )
error(element, "@%s for a view must only specify one ID. Found: %s. (%s.%s)" ,
Bind .class.getSimpleName() , Arrays.toString (ids), enclosingElement.getQualifiedName() ,
element.getSimpleName()) ;
hasError = true;
if (hasError)
return;
int id = ids[0] ;
BindingClass bindingClass = targetClassMap.get(enclosingElement) ;
if (bindingClass != null)
ViewBindings viewBindings = bindingClass.getViewBinding(id);
if (viewBindings != null)
Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator() ;
if (iterator.hasNext())
FieldViewBinding existingBinding = iterator.next();
error(element , "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)" ,
Bind .class.getSimpleName() , id, existingBinding.getName() ,
enclosingElement.getQualifiedName() , element.getSimpleName());
return;
else
bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
String name = element.getSimpleName().toString();
String type = elementType.toString() ;
boolean required = isRequiredBinding(element) ;
FieldViewBinding binding = new FieldViewBinding(name, type, required) ;
bindingClass.addField(id , binding); //保存这个bind,后面在生成类文件时候要用到!
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement.toString()) ;
所有应用ButterKnife的类都有一个单独的BindingClass与其挂钩,它里面包含了很多信息:Field绑定、Drawable绑定等等很多信息。以类名+”$$ViewBinder”包名标识。FieldViewBinding存放于BindingClass内,用于绑定变量View(与之对应还有绑定Drawable、Bitmap、List、Method等)。记录每个Field的绑定信息,如变量名、类名。每个FieldViewBinding都与一个id对应。注解生成就分析完了,接下来看看是如何生成findViewById之类的代码的。
解析完了,看第二步,brewJava 生成代码。
String brewJava ()
StringBuilder builder = new StringBuilder();
builder.append( "// Generated code from Butter Knife. Do not modify! \\n") ;
builder.append( "package ").append(classPackage ).append(";\\n\\n ");
if (! resourceBindings.isEmpty())
builder.append("import android.content.res.Resources; \\n") ;
if (!viewIdMap.isEmpty() || ! collectionBindings.isEmpty())
builder.append("import android.view.View; \\n") ;
builder.append("import butterknife.ButterKnife.Finder; \\n") ;
if ( parentViewBinder == null )
builder.append("import butterknife.ButterKnife.ViewBinder; \\n") ;
builder.append(' \\n') ;
builder.append( "public class " ).append(className) ;
builder.append( "<T extends " ).append(targetClass).append( ">");
if ( parentViewBinder != null )
builder.append(" extends " ).append(parentViewBinder).append( "<T>");
else
builder.append(" implements ViewBinder<T>" );
builder.append(" \\n") ;
emitBindMethod(builder) ;
builder.append( '\\n' );
emitUnbindMethod(builder) ;
builder.append( "\\n" );
return builder.toString() ;
其实就是生成ViewBinder类,看下emitBindMethod方法:
private void emitBindMethod(StringBuilder builder)
builder.append(" @Override " )
.append("public void bind(final Finder finder, final T target, Object source) \\n") ;
// Emit a call to the superclass binder, if any.
if (parentViewBinder != null)
builder.append(" super.bind(finder, target, source); \\n\\n") ;
if (!viewIdMap.isEmpty() || ! collectionBindings.isEmpty())
// Local variable in which all views will be temporarily stored.
builder.append( " View view; \\n") ;
// Loop over each view bindings and emit it.
for (ViewBindings bindings : viewIdMap.values())
emitViewBindings(builder, bindings);
// Loop over each collection binding and emit it.
for (Map.Entry<FieldCollectionViewBinding , int[]> entry : collectionBindings .entrySet())
emitCollectionBinding(builder, entry.getKey(), entry.getValue()) ;
if (!resourceBindings.isEmpty())
builder.append(" Resources res = finder.getContext(source).getResources(); \\n") ;
for (FieldResourceBinding binding : resourceBindings)
builder.append(" target." )
.append(binding.getName())
.append(" = res." )
.append(binding.getMethod())
.append('(' )
.append(binding.getId())
.append("); \\n") ;
builder.append(" \\n") ;
利用上面解析保存的ViewBindings 来拼凑类。每次编译时ButterKnife都会针对你写的代码生成一个ViewBinder类,它里面有这样的代码:
public class MainActivity$$ViewBinder<T extends demo.lbb.test.MainActivity> implements ViewBinder<T>
@Override public void bind(final Finder finder, final T target, Object source)
View view;
view = finder.findRequiredView(source, 2131492969, "field 'TextView' ");
target.textview= finder.castView(view, 2131492969, "field 'imageview'");
解析生成ViewHolder完成了,接下来看如何动态生成我们要的那些代码。
进行绑定
肯定是从bind方法开始分析,来看下它的具体实现:
static void bind(Object target, Object source, Finder finder)
Class<?> targetClass = target.getClass();
try
if (debug) Log. d(TAG, "Looking up view binder for " + targetClass.getName()) ;
ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass) ;
if (viewBinder != null)
viewBinder.bind(finder, target, source) ; //真正生成代码的地方
catch (Exception e)
throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e) ;
首先通过findViewBinderForClass来获得ViewHolder对象,然后调用ViewHolder的Binder来生成我们需要生成的代码。这里ViewHolder是什么鬼,我们先留个疑问在这。来看下findViewBinderForClass的实现:
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
throws IllegalAccessException, InstantiationException
ViewBinder<Object> viewBinder = BINDERS.get(cls) ;//查缓存。节省时间
if (viewBinder != null)
if (debug) Log. d(TAG, "HIT: Cached in view binder map." );
return viewBinder ;
String clsName = cls.getName();
if (clsName.startsWith( ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) //android. 和java.
if (debug) Log. d(TAG, "MISS: Reached framework class. Abandoning search." );
return NOP_VIEW_BINDER;
try
Class<?> viewBindingClass = Class.forName (clsName + ButterKnifeProcessor.SUFFIX) ; //SUFFIX = "$$ViewBinder" ;
//noinspection unchecked
viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance() ;
if ( debug) Log.d( TAG, "HIT: Loaded view binder class." );
catch (ClassNotFoundException e)
if (debug) Log. d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName()) ;
viewBinder = findViewBinderForClass(cls.getSuperclass()) ;
BINDERS .put(cls, viewBinder) ;//进行缓存
return viewBinder ;
通过BINDERS.get(cls)先判断缓存里是否存在,如果存在己经缓存的viewBinder对象就直接读取出来。不存在就Class.forName反射调用获取ViewBinder。findViewBinderForClass(cls.getSuperclass())用于在子类找不到继续找父类。
继续看 viewBinder.bind实现的功能。先看下它的参数Finder:
public enum Finder
VIEW
@Override protected View findView (Object source, int id)
return ((View) source).findViewById(id) ;
@Override public Context getContext (Object source)
return ((View) source).getContext() ;
,
ACTIVITY
@Override protected View findView (Object source, int id)
return ((Activity) source).findViewById(id) ;
@Override public Context getContext (Object source)
return (Activity) source;
,
DIALOG
@Override protected View findView (Object source, int id)
return ((Dialog) source).findViewById(id) ;
@Override public Context getContext (Object source)
return ((Dialog) source).getContext() ;
;
什么,一个枚举,什么return ((View) source).findViewById(id) ;不就是我们要生成的代码吗。到此我们可以猜测ButterKinfe根据传进来的context类型不同枚举生成不同的代码。是不是这样,往下看。上面说过我们生成了一个ViewHolder,ViewHolder.binder会调用以下代码:
view = finder.findRequiredView(source, 2131296337, "field 'toolbar'");
target.toolbar = finder.castView(view, 2131296337, "field 'toolbar'");
看下findRequiredView的具体实现:
public <T> T findRequiredView(Object source , int id, String who)
T view = findOptionalView(source , id, who);
if (view == null)
String name = getContext(source).getResources().getResourceEntryName(id);
throw new IllegalStateException( "Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' annotation." );
return view;
通过findOptionView获得一个View,继续看看findOptionView的实现:
public <T> T findOptionalView(Object source, int id, String who)
View view = findView(source, id);
return castView(view, id, who);
看到findView,是不是就明白了,通过Finder.findView()来获得要生成的代码,比如如果是Activity就调用((Activity) source).findViewById(id) ,到此绑定成功。
这篇博客只是简单分析下生成控件绑定代码的源码,其实ButterKnife能做的不仅如此,它还可以生成触发事件代码等,是一个非常好用的工具。
以上是关于ButterKnife源码分析的主要内容,如果未能解决你的问题,请参考以下文章
ButterKnife编译时生成代码原理:butterknife-compiler源码分析
ButterKnife编译时生成代码原理:butterknife-compiler源码分析
ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码