Android Butterknife浅分析

Posted 鲨鱼丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Butterknife浅分析相关的知识,希望对你有一定的参考价值。

今天很顺利的完成了公司的任务,干嘛呢,当然是写写代码看看书了。

发现个问题,一个APP中很多次的使用了一段代码,而且这行代码还非常不好省略,这个就是findViewId()和onClick,一个app肯定有界面和按钮,有见面就有控件,有控件就有点击需求,而有这些需求和控件就必须要在Activity或者Fragment中使用这段findViewId()和onClick(),真是太烦了,现在github上大神那么多,去看看有什么好的解决办法呗。

首先我发现了thinkAndroid这个框架,这是个很好的框架,集成了很多的模块:

  • MVC模块:实现了视图和模型的分离,当然不用说
  • IOC模块:这个就是我们所需要的,下面我们会认真的去看下这个模块,他们的github上的介绍是说:完全注解方式就可以进行UI的绑定,res中的资源的读取,以及对象的初始化。
  • http模块:通过httpclient进行封装http数据请求,支持异步和同步方式加载。呃呃呃,这个什么情况,现在的android studio上已经不支持httpclient了,要与时俱进啊大神...
  • 缓存模块:通过简单的配置及设计可以很好的实现缓存,对缓存可以随意的配置
  • 图片缓存模块:imageview加载图片的时候无需考虑图片加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象。
  • 配置器模块:可以对简易的实现配对配置的操作,目前配置文件可以支持Preference、Properties对配置进行存取。
  • 日志打印模块:可以较快的轻易的是实现日志打印,支持日志打印的扩展,目前支持对sdcard写入本地打印、以及控制台打印
  • 下载器模块:可以简单的实现多线程下载、后台下载、断点续传、对下载进行控制、如开始、暂停、删除等等。
  • 网络状态检测模块:当网络状态改变时,对其进行检测。

模块很多,上面这个东西是从他们的github上复制过来的,其他的我们不说,先看看所谓了ioc模块:

又给出例子:

    @TAInject 
    Entity entity; //目前只能对无参构造函数进行初始化
    @@TAInject(id=R.string.app_name)
    String appNameString;
    @TAInjectResource(id=R.attr.test)
    int[] test; 
    @TAInjectView(id=R.id.add);
    Button addButton;
我们分析代码可以看到这个框架是通过@TAInjectView来快速初始化控件的。

看下@TAInjectView的代码:

package com.ta.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TAInjectView

	/** View的ID */
	public int id() default -1;

	/** View的单击事件 */
	public String click() default "";

	/** View的长按键事件 */
	public String longClick() default "";

	/** View的焦点改变事件 */
	public String focuschange() default "";

	/** View的手机键盘事件 */
	public String key() default "";

	/** View的触摸事件 */
	public String Touch() default "";

是一个注解类用RetentionPloicy.RUNTIME做修饰,而且制定了修饰的类别Field,在这里看到大神很贴心的贴上了注释,几乎包含了控件的所有操作,很是方便,看下下面这行代码:

package com.ta.util;

import java.lang.reflect.Field;

import com.ta.annotation.TAInject;
import com.ta.annotation.TAInjectResource;
import com.ta.annotation.TAInjectView;

import android.app.Activity;
import android.content.res.Resources;

public class TAInjector

	private static TAInjector instance;

	private TAInjector()
	

	

	public static TAInjector getInstance()
	
		if (instance == null)
		
			instance = new TAInjector();
		
		return instance;
	

	public void inJectAll(Activity activity)
	
		// TODO Auto-generated method stub
		Field[] fields = activity.getClass().getDeclaredFields();
		if (fields != null && fields.length > 0)
		
			for (Field field : fields)
			
				if (field.isAnnotationPresent(TAInjectView.class))
				
					injectView(activity, field);
				 else if (field.isAnnotationPresent(TAInjectResource.class))
				
					injectResource(activity, field);
				 else if (field.isAnnotationPresent(TAInject.class))
				
					inject(activity, field);
				
			
		
	

	private void inject(Activity activity, Field field)
	
		// TODO Auto-generated method stub
		try
		
			field.setAccessible(true);
			field.set(activity, field.getType().newInstance());
		 catch (Exception e)
		
			e.printStackTrace();
		
	

	private void injectView(Activity activity, Field field)
	
		// TODO Auto-generated method stub
		if (field.isAnnotationPresent(TAInjectView.class))
		
			TAInjectView viewInject = field.getAnnotation(TAInjectView.class);
			int viewId = viewInject.id();
			try
			
				field.setAccessible(true);
				field.set(activity, activity.findViewById(viewId));
			 catch (Exception e)
			
				e.printStackTrace();
			
		
	

	private void injectResource(Activity activity, Field field)
	
		// TODO Auto-generated method stub
		if (field.isAnnotationPresent(TAInjectResource.class))
		
			TAInjectResource resourceJect = field
					.getAnnotation(TAInjectResource.class);
			int resourceID = resourceJect.id();
			try
			
				field.setAccessible(true);
				Resources resources = activity.getResources();
				String type = resources.getResourceTypeName(resourceID);
				if (type.equalsIgnoreCase("string"))
				
					field.set(activity,
							activity.getResources().getString(resourceID));
				 else if (type.equalsIgnoreCase("drawable"))
				
					field.set(activity,
							activity.getResources().getDrawable(resourceID));
				 else if (type.equalsIgnoreCase("layout"))
				
					field.set(activity,
							activity.getResources().getLayout(resourceID));
				 else if (type.equalsIgnoreCase("array"))
				
					if (field.getType().equals(int[].class))
					
						field.set(activity, activity.getResources()
								.getIntArray(resourceID));
					 else if (field.getType().equals(String[].class))
					
						field.set(activity, activity.getResources()
								.getStringArray(resourceID));
					 else
					
						field.set(activity, activity.getResources()
								.getStringArray(resourceID));
					

				 else if (type.equalsIgnoreCase("color"))
				
					if (field.getType().equals(Integer.TYPE))
					
						field.set(activity,
								activity.getResources().getColor(resourceID));
					 else
					
						field.set(activity, activity.getResources()
								.getColorStateList(resourceID));
					

				
			 catch (Exception e)
			
				e.printStackTrace();
			
		
	

	public void inject(Activity activity)
	
		// TODO Auto-generated method stub
		Field[] fields = activity.getClass().getDeclaredFields();
		if (fields != null && fields.length > 0)
		
			for (Field field : fields)
			
				if (field.isAnnotationPresent(TAInject.class))
				
					inject(activity, field);
				
			
		
	

	public void injectView(Activity activity)
	
		// TODO Auto-generated method stub
		Field[] fields = activity.getClass().getDeclaredFields();
		if (fields != null && fields.length > 0)
		
			for (Field field : fields)
			
				if (field.isAnnotationPresent(TAInjectView.class))
				
					injectView(activity, field);
				
			
		
	

	public void injectResource(Activity activity)
	
		// TODO Auto-generated method stub
		Field[] fields = activity.getClass().getDeclaredFields();
		if (fields != null && fields.length > 0)
		
			for (Field field : fields)
			
				if (field.isAnnotationPresent(TAInjectResource.class))
				
					injectResource(activity, field);
				
			
		
	


这才是具体让findViewId()消失的类,一模了然的类,是一个单例模式,里面injectView,injectResource来实现具体的方法,代码不是很难理解,我就不对说了。

这个框架其实很不错的,但是看情况大神好像已经不更新这个框架了,只能放弃,看下代码好了,学学大神思路也是进步。

上面已经把大神的github给出了,有兴趣的可以点击进去看看。

看完这个之后我又开始找已经在Android studio上更新了的框架,发现了这个:Butterknife来自JakeWharton大神。

看下这个框架的实现代码:

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...
  

看到这个我就想,这就是我要找的框架。

我们接下来去分析这个的实现方法看下这个框架的代码结构:

额,很复杂的样子,看下onBindView之类的类在哪,去看下,

我们在butterknife-annotations模块找到了他们,分的非常仔细,看到名字就可以知道这个类是做什么的。我们首先找个典型的例子看下,BindView,上面的代码中表示这个类主要的是初始化控件的,我们看下这个代码:

package butterknife;

import android.support.annotation.IdRes;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.CLASS;

/**
 * Bind a field to the view for the specified ID. The view will automatically be cast to the field
 * type.
 * <pre><code>
 * @literal @BindView(R.id.title) TextView title;
 * </code></pre>
 */
@Retention(CLASS) @Target(FIELD)
public @interface BindView 
  /** View ID to which the field will be bound. */
  @IdRes int value();

不是很难哦,设置的保留策略为Class,注解用于Field上。传入一个IdRes,并且直接以value的形式进行设置。看下注解处理器的实现:

<pre name="code" class="java">package butterknife.compiler;

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor 
  static final Id NO_ID = new Id(-1);
  static final String VIEW_TYPE = "android.view.View";
  private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList";
  private static final String BITMAP_TYPE = "android.graphics.Bitmap";
  private static final String DRAWABLE_TYPE = "android.graphics.drawable.Drawable";
  private static final String TYPED_ARRAY_TYPE = "android.content.res.TypedArray";
  private static final String NULLABLE_ANNOTATION_NAME = "Nullable";
  private static final String STRING_TYPE = "java.lang.String";
  private static final String LIST_TYPE = List.class.getCanonicalName();
  private static final String R = "R";
  private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(//
      OnCheckedChanged.class, //
      OnClick.class, //
      OnEditorAction.class, //
      OnFocusChange.class, //
      OnItemClick.class, //
      OnItemLongClick.class, //
      OnItemSelected.class, //
      OnLongClick.class, //
      OnPageChange.class, //
      OnTextChanged.class, //
      OnTouch.class //
  );

  private static final List<String> SUPPORTED_TYPES = Arrays.asList(
      "array", "attr", "bool", "color", "dimen", "drawable", "id", "integer", "string"
  );

  private Elements elementUtils;
  private Types typeUtils;
  private Filer filer;
  private Trees trees;

  private final Map<Integer, Id> symbols = new LinkedHashMap<>();

  @Override public synchronized void init(ProcessingEnvironment env) 
    super.init(env);

    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    trees = Trees.instance(processingEnv);
  

  @Override public Set<String> getSupportedAnnotationTypes() 
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) 
      types.add(annotation.getCanonicalName());
    
    return types;
  

  private Set<Class<? extends Annotation>> getSupportedAnnotations() 
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  

  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) 
    //BindingClass类对象,代表生成代理类的信息
    //Map<>集合代理类对象集合
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

    //生成代理类
    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) 
      TypeElement typeElement = entry.getKey();
      BindingClass bindingClass = entry.getValue();

      for (JavaFile javaFile : bindingClass.brewJava()) 
        try 
          javaFile.writeTo(filer);
         catch (IOException e) 
          error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
              e.getMessage());
        
      
    

    return true;
  

  private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) 
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);

    //通过getElementsAnnotatedWith拿到我们注解的每个元素,返回值为Map<>集合
    for (Element element : env.getElementsAnnotatedWith(BindArray.class)) 
      if (!SuperficialValidation.validateElement(element)) continue;
      try 
        parseResourceArray(element, targetClassMap, erasedTargetNames);
       catch (Exception e) 
        logParsingError(element, BindArray.class, e);
      
    
   //代码省略
    return targetClassMap;
  
  //代码省略


 

代码很长只留核心代码,其他省略

嗯哼,一般的写法,继承了AbstractProcessor,并实现了public synchronized void init(ProcessingEnvironment env),@Override public Set<String> getSupportedAnnotationTypes()  和private Set<Class<? extends Annotation>> getSupportedAnnotations()函数,在这些函数里,主要是将BindView之类的注解和注解处理器连接,一般情况下,这些函数的实现是通用的,主要是返回注解类型,返回源码版本,和初始化辅助类。处理器中还有一个非常重要的函数需要我们的实现:process(),这是核心,在我们的知道的process()函数的实现复杂有简单,其实就实现了两个功能而已:

  • 收集代理类信息使用的是getElementsAnnotatedWith函数 。返回代理类集合。
  • 生成咱们在使用BindView之后编译生成的代理类,想上面的例子,在MainActivity中的,我们会生成一个MainActivity_ViewBinder和MainActivity_ViewBining代理类,在Android studio的build文件夹下可以很清楚的看到这个类的存在.

上面的BindingClass类为生成java类的方法,通过收集到的信息,拼接完成代理类对象。

完成代理类对象之后,提供一个api供用户调用,ButterKnife的实现API为ButterKnife类,看下代码:

package butterknife;

public final class ButterKnife 

  //代码省略
  //传入当前对象,不管是Activity,fragment或者dialog全都行,寻找代理类
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) 
    return getViewBinder(target).bind(Finder.ACTIVITY, target, target);
  

 //代码省略
  //强制转换接口为统一接口,并调用接口提供的方法。
  @NonNull @CheckResult @UiThread
  static ViewBinder<Object> getViewBinder(@NonNull Object target) 
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
    return findViewBinderForClass(targetClass);
  

  @NonNull @CheckResult @UiThread
  private static ViewBinder<Object> findViewBinderForClass(Class<?> cls) 
    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.") || clsName.startsWith("java.")) 
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return NOP_VIEW_BINDER;
    
    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try 
      Class<?> viewBindingClass = Class.forName(clsName + "_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());
     catch (InstantiationException e) 
      throw new RuntimeException("Unable to create view binder for " + clsName, e);
     catch (IllegalAccessException e) 
      throw new RuntimeException("Unable to create view binder for " + clsName, e);
    
    BINDERS.put(cls, viewBinder);
    return viewBinder;
  

  //代码省略
  /** Simpler version of @link View#findViewById(int) which infers the target type. */
  @SuppressWarnings( "unchecked", "UnusedDeclaration" ) // Checked by runtime cast. Public API.
  @CheckResult
  public static <T extends View> T findById(@NonNull View view, @IdRes int id) 
    return (T) view.findViewById(id);
  

  /** Simpler version of @link Activity#findViewById(int) which infers the target type. */
  @SuppressWarnings( "unchecked", "UnusedDeclaration" ) // Checked by runtime cast. Public API.
  @CheckResult
  public static <T extends View> T findById(@NonNull Activity activity, @IdRes int id) 
    return (T) activity.findViewById(id);
  

  /** Simpler version of @link Dialog#findViewById(int) which infers the target type. */
  @SuppressWarnings( "unchecked", "UnusedDeclaration" ) // Checked by runtime cast. Public API.
  @CheckResult
  public static <T extends View> T findById(@NonNull Dialog dialog, @IdRes int id) 
    return (T) dialog.findViewById(id);
  


这个类中主要做了两件事情:

  • 传入当前对象,不管是Activity,fragment或者dialog全都行,寻找我们刚才生成的代理类
  • 强制转换接口为统一接口,并调用接口提供的方法。

至此,一个简化版的findViewById就算是完成了,可以省略代码写控件初始化了。。。。






以上是关于Android Butterknife浅分析的主要内容,如果未能解决你的问题,请参考以下文章

Android 注解框架 Butterknife的核心代码分析笔记

Android 注解框架 Butterknife的核心代码分析笔记

[Android] ButterKnifeProcessor 工作流程分析

[Android] ButterKnifeProcessor 工作流程分析

Android Studio上方便使用butterknife注解框架的偷懒插件Android Butterknife Z

Android APT编译时技术 ( 编译时注解 和 注解处理器 依赖库 )