注解框架源码分析(XUtilsButterKnife)

Posted Steadyoung

tags:

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

1.前言

频繁的findViewById是一件挺烦人的事,IOC注解框架应运而生,android中IOC框架就是注入控件和布局或者说是设置点击监听。

现在主流的有ButterKnife、xUtils、afinal等,IOC注解框架刚出来的时候,粗略看过源码后我是摒弃使用的,因为里面有反射、循环遍历寻找控件等操作,但是慢慢的接收了这种快捷方式,发现对实际性能影响并不大,不是在特别强迫症的情况下完全可行,特别是ButterKnife的出现,ButterKnife没有利用全反射,而是在同包下动态编译生成代码,性能几乎和自己findViewById性能是一样的!

2.源码分析

下面我们来实际分析xUtils和ButterKnife的源码:

2.1 xUtils 使用

xUtils源码地址:https://github.com/wyouflf/xUtils3

xUtils 包含了orm, http(s), image, view注解,下面我们重点讲解View注解(IOC注入):

public class MainActivity extends AppCompatActivity 

    @ViewInject(R.id.test_tv)
    private TextView test_tv;

    @ViewInject(R.id.test_btn)
    private Button test_btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        x.view().inject(this);
        test_tv.setText("测试文本");
        test_btn.setText("测试按钮");
    

    /**
     * 1. 方法必须私有限定,
     * 2. 方法参数形式必须和type对应的Listener接口一致.
     * 3. 注解参数value支持数组: value=id1, id2, id3
     * 4. 其它参数说明见@link org.xutils.view.annotation.Event类的说明.
     **/
    @Event(value = R.id.test_btn,
            type = View.OnClickListener.class/*可选参数, 默认是View.OnClickListener.class*/)
    private void iconIvClick(View view) 
        test_tv.setText("点击了按钮,修改测试文本");
        Toast.makeText(this, "点击了按钮,修改测试文本", Toast.LENGTH_LONG).show();
    


注解省去了findViewById和setOnClickListener等简单操作。

2.2 xUtils源码分析

我们挑一些关键的源码分析,先分析x.view().inject(this);这句代码做了什么:


点击进入查看是一个接口ViewInjector,看不到实现代码,我就Ctrl+鼠标左键查看实现类:


找到实现类ViewInjectorImpl中inject(Activity activity)方法代码:
 @Override
    public void inject(Activity activity) 
        //获取Activity的ContentView的注解
        Class<?> handlerType = activity.getClass();
        try 
            // 找到ContentView这个注解,在activity类上面获取
            ContentView contentView = findContentView(handlerType);
            if (contentView != null) 
                int viewId = contentView.value();
                if (viewId > 0) 
                    // 如果有注解获取layoutId的值,利用反射调用activity的setContentView方法注入视图
                    Method setContentViewMethod = handlerType.getMethod("setContentView", int.class);
                    setContentViewMethod.invoke(activity, viewId);
                
            
         catch (Throwable ex) 
            LogUtil.e(ex.getMessage(), ex);
        
        // 处理 findViewById和setOnclickListener的注解
        injectObject(activity, handlerType, new ViewFinder(activity));
    

根据源码可知,先判断是否有ContentView这个注解,在activity类上面获取,有则setContentView(R.layout.xxxx),然后再处理 findViewById和setOnclickListener的注解(injectObject方法):

@SuppressWarnings("ConstantConditions")
    private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) 

        if (handlerType == null || IGNORED.contains(handlerType)) 
            return;
        

        // 从父类到子类递归
        injectObject(handler, handlerType.getSuperclass(), finder);

        // inject view
        Field[] fields = handlerType.getDeclaredFields();
        if (fields != null && fields.length > 0) 
            for (Field field : fields) 

                Class<?> fieldType = field.getType();
                if (
                /* 不注入静态字段 */     Modifier.isStatic(field.getModifiers()) ||
                /* 不注入final字段 */    Modifier.isFinal(field.getModifiers()) ||
                /* 不注入基本类型字段 */  fieldType.isPrimitive() ||
                /* 不注入数组类型字段 */  fieldType.isArray()) 
                    continue;
                

                ViewInject viewInject = field.getAnnotation(ViewInject.class);
                if (viewInject != null) 
                    try 
                        View view = finder.findViewById(viewInject.value(), viewInject.parentId());
                        if (view != null) 
                            field.setAccessible(true);
                            field.set(handler, view);
                         else 
                            throw new RuntimeException("Invalid @ViewInject for "
                                    + handlerType.getSimpleName() + "." + field.getName());
                        
                     catch (Throwable ex) 
                        LogUtil.e(ex.getMessage(), ex);
                    
                
            
         // end inject view

        // inject event
        Method[] methods = handlerType.getDeclaredMethods();
        if (methods != null && methods.length > 0) 
            for (Method method : methods) 

                if (Modifier.isStatic(method.getModifiers())
                        || !Modifier.isPrivate(method.getModifiers())) 
                    continue;
                

                //检查当前方法是否是event注解的方法
                Event event = method.getAnnotation(Event.class);
                if (event != null) 
                    try 
                        // id参数
                        int[] values = event.value();
                        int[] parentIds = event.parentId();
                        int parentIdsLen = parentIds == null ? 0 : parentIds.length;
                        //循环所有id,生成ViewInfo并添加代理反射
                        for (int i = 0; i < values.length; i++) 
                            int value = values[i];
                            if (value > 0) 
                                ViewInfo info = new ViewInfo();
                                info.value = value;
                                info.parentId = parentIdsLen > i ? parentIds[i] : 0;
                                method.setAccessible(true);
                                // EventListenerManager 动态代理执行相应的方法
                                EventListenerManager.addEventMethod(finder, info, event, handler, method);
                            
                        
                     catch (Throwable ex) 
                        LogUtil.e(ex.getMessage(), ex);
                    
                
            
         // end inject event

    

源码中利用类的反射循环获取属性的注解值然后通过findViewById之后,动态的注入到控件属性里面;事件注入也是类似首先findViewById然后利用动态代理去反射执行方法,动态代理代码如下:

    public static void addEventMethod(
            //根据页面或view holder生成的ViewFinder
            ViewFinder finder,
            //根据当前注解ID生成的ViewInfo
            ViewInfo info,
            //注解对象
            Event event,
            //页面或view holder对象
            Object handler,
            //当前注解方法
            Method method) 
        try 
            View view = finder.findViewByInfo(info);

            if (view != null) 
                // 注解中定义的接口,比如Event注解默认的接口为View.OnClickListener
                Class<?> listenerType = event.type();
                // 默认为空,注解接口对应的Set方法,比如setOnClickListener方法
                String listenerSetter = event.setter();
                if (TextUtils.isEmpty(listenerSetter)) 
                    listenerSetter = "set" + listenerType.getSimpleName();
                


                String methodName = event.method();

                boolean addNewMethod = false;
                /*
                    根据View的ID和当前的接口类型获取已经缓存的接口实例对象,
                    比如根据View.id和View.OnClickListener.class两个键获取这个View的OnClickListener对象
                 */
                Object listener = listenerCache.get(info, listenerType);
                DynamicHandler dynamicHandler = null;
                /*
                    如果接口实例对象不为空
                    获取接口对象对应的动态代理对象
                    如果动态代理对象的handler和当前handler相同
                    则为动态代理对象添加代理方法
                 */
                if (listener != null) 
                    dynamicHandler = (DynamicHandler) Proxy.getInvocationHandler(listener);
                    addNewMethod = handler.equals(dynamicHandler.getHandler());
                    if (addNewMethod) 
                        dynamicHandler.addMethod(methodName, method);
                    
                

                // 如果还没有注册此代理
                if (!addNewMethod) 

                    dynamicHandler = new DynamicHandler(handler);

                    dynamicHandler.addMethod(methodName, method);

                    // 生成的代理对象实例,比如View.OnClickListener的实例对象
                    listener = Proxy.newProxyInstance(
                            listenerType.getClassLoader(),
                            new Class<?>[]listenerType,
                            dynamicHandler);

                    listenerCache.put(info, listenerType, listener);
                

                Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                setEventListenerMethod.invoke(view, listener);
            
         catch (Throwable ex) 
            LogUtil.e(ex.getMessage(), ex);
        
    

在实例化DynamicHandler的时候穿递的是Activity/Fragment。然后调用EventListenerManager.addEventMethod(finder, info, event, handler, method);法的时候,将method(当前注解方法)传递进去了。完整类名有,方法名字有。动态代理的实现如下:

    public static class DynamicHandler implements InvocationHandler 
        // 存放代理对象,比如Fragment或view holder
        private WeakReference<Object> handlerRef;
        // 存放代理方法
        private final HashMap<String, Method> methodMap = new HashMap<String, Method>(1);

        private static long lastClickTime = 0;

        public DynamicHandler(Object handler) 
            this.handlerRef = new WeakReference<Object>(handler);
        

        public void addMethod(String name, Method method) 
            methodMap.put(name, method);
        

        public Object getHandler() 
            return handlerRef.get();
        

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
            Object handler = handlerRef.get();
            if (handler != null) 

                String eventMethod = method.getName();
                if ("toString".equals(eventMethod)) 
                    return DynamicHandler.class.getSimpleName();
                

                method = methodMap.get(eventMethod);
                if (method == null && methodMap.size() == 1) 
                    for (Map.Entry<String, Method> entry : methodMap.entrySet()) 
                        if (TextUtils.isEmpty(entry.getKey())) 
                            method = entry.getValue();
                        
                        break;
                    
                

                if (method != null) 

                    if (AVOID_QUICK_EVENT_SET.contains(eventMethod)) 
                        long timeSpan = System.currentTimeMillis() - lastClickTime;
                        if (timeSpan < QUICK_EVENT_TIME_SPAN) 
                            LogUtil.d("onClick cancelled: " + timeSpan);
                            return null;
                        
                        lastClickTime = System.currentTimeMillis();
                    

                    try 
                        return method.invoke(handler, args);
                     catch (Throwable ex) 
                        throw new RuntimeException("invoke method error:" +
                                handler.getClass().getName() + "#" + method.getName(), ex);
                    
                 else 
                    LogUtil.w("method not impl: " + eventMethod + "(" + handler.getClass().getSimpleName() + ")");
                
            
            return null;
        
    

2.3 ButterKnife的使用

ButterKnife源码https://github.com/JakeWharton/butterknife
相比起xutils来,ButterKnife更受欢迎,第一在性能方面xutils完全是利用的反射,butterknife是轻量级的反射使用的注解都是编译时注解,而且它还提供了一个Android Studio的插件不需要我们去写任何的代码,作者JakeWharton很出名的写过很多大型的第三方框架。
ButterKnife的代码自动生成的Android Studio插件: android-butterknife-zelezny
后期我会模仿这个插件编写一个自己的IOC注解框架的AndroidStudio插件。

public class MainActivity extends AppCompatActivity 

    @BindView(R.id.test_tv)
    TextView test_tv;

    @BindView(R.id.test_btn)
    Button test_btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        test_tv.setText("测试文本");
        test_btn.setText("测试按钮");
    

    @OnClick(R.id.test_tv, R.id.test_btn)
    public void onViewClicked(View view) 
        switch (view.getId()) 
            case R.id.test_tv:
                break;
            case R.id.test_btn:
                test_tv.setText("点击了按钮,修改测试文本");
                Toast.makeText(this, "点击了按钮,修改测试文本", Toast.LENGTH_LONG).show();
                break;
        
    

上面是ButterKnifeZelezny这个插件自动生成的代码,只需要自己实现业务逻辑代码就够了,基本 @BindView、@OnClick等注解自动生成非常好用的插件。

2.4 ButterKnife源码分析

源码入口从ButterKnife.bind(this)这里查起:

  /**
   * BindView annotated fields and methods in the specified @link Activity. The current content
   * view is used as the view root.
   *
   * @param target Target activity for view binding.
   */
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) 
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  

bind(activity)最后调用了createBinding(target, sourceView),其实bind(view)和bind(dialog)等方法都会调用createBinding(target, sourceView),源码如下:

/**
   * BindView annotated fields and methods in the specified @link View. The view and its children
   * are used as the view root.
   *
   * @param target Target view for view binding.
   */
  @NonNull @UiThread
  public static Unbinder bind(@NonNull View target) 
    return createBinding(target, target);
  

  /**
   * BindView annotated fields and methods in the specified @link Dialog. The current content
   * view is used as the view root.
   *
   * @param target Target dialog for view binding.
   */
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Dialog target) 
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  

  /**
   * BindView annotated fields and methods in the specified @code target using the @code source
   * @link Activity as the view root.
   *
   * @param target Target class for view binding.
   * @param source Activity on which IDs will be looked up.
   */
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull Activity source) 
    View sourceView = source.getWindow().getDecorView();
    return createBinding(target, sourceView);
  

  /**
   * BindView annotated fields and methods in the specified @code target using the @code source
   * @link View as the view root.
   *
   * @param target Target class for view binding.
   * @param source View root on which IDs will be looked up.
   */
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) 
    return createBinding(target, source);
  

  /**
   * BindView annotated fields and methods in the specified @code target using the @code source
   * @link Dialog as the view root.
   *
   * @param target Target class for view binding.
   * @param source Dialog on which IDs will be looked up.
   */
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) 
    View sourceView = source.getWindow().getDecorView();
    return createBinding(target, sourceView);
  

就是最终会用目标对象target和绑定的View进行处理,下面分析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<? 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);
    
  

源码中通过findBindingConstructorForClass(targetClass)获取构造方法,然后调用constructor.newInstance(target, source)通过反射调用构造方法,下面先分析findBindingConstructorForClass(targetClass):


  @Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) 
    //1.先从BINDINGS这个Map中获取缓存的Constructor
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    //2.如果有缓存那么就直接返回Constructor
    if (bindingCtor != null) 
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    
    //3.反射获取目标类对象,判断是否是android和java包下的框架类,框架类终止查询构造方法操作,返回空不再进行处理
    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 
      //4.通过反射加载(clsName + "_ViewBinding")名字的类
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      //bindingClass 中查询参数为(cls, View.class)的构造函数,cls为最初的目标类
      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());
      //报错后查询cls的父类,看是否在父类中有实现这个构造方法
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
     catch (NoSuchMethodException e) 
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    
    //将反射查询出的结果缓存在BINDINGS中
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  

根据源码分析得知(源码中我加了注释) Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding")也就是target类名+_ViewBinding,然后bindingClass.getConstructor(cls, View.class)获取这个类对应参数的构造方法,按照源码执行顺序最后要执行的也是这个方法。

我的target类是MainActivity,那么实现类是MainActivity_ViewBinding这个类,不过这个类是在编译运行时才会生成的,Ctrl+N 搜索类如下图


如果搜不到我们可以在项目目录..\\app\\build中找到,编译后在目录..\\app\\build\\intermediates\\classes\\debug\\你的包名(com\\xxx\\xxx)


那么下面查看MainActivity_ViewBinding的代码:

public class MainActivity_ViewBinding implements Unbinder 
    private MainActivity target;
    private View view2131165311;
    private View view2131165310;

    @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 = Utils.findRequiredView(source, 2131165311, "field 'test_tv' and method 'onViewClicked'");
        target.test_tv = (TextView)Utils.castView(view, 2131165311, "field 'test_tv'", TextView.class);
        this.view2131165311 = view;
        view.setOnClickListener(new DebouncingOnClickListener() 
            public void doClick(View p0) 
                target.onViewClicked(p0

以上是关于注解框架源码分析(XUtilsButterKnife)的主要内容,如果未能解决你的问题,请参考以下文章

自己简易打造的IOC注解框架:SteadyoungIOC

自己简易打造的IOC注解框架:SteadyoungIOC

Spring源码分析之 常见底层核心注解

Java注解与原理分析

Spring中依赖注入底层原理与源码分析

《极简笔记》源码分析

(c)2006-2024 SYSTEM All Rights Reserved IT常识