自己动手打造一套IOC注解框架

红橙Darren
• 阅读 1510

1.概述


这是我们的内涵段子系统架构的第一期分享,。在介绍内涵段子整个项目的时候我们也说好了会分析系统源码设计模式,第三方框架源码解析,然后自己动手一点一点打造一套内涵段子框架。这一期的内容对于部分哥们可能有点麻烦,如果觉得抽象请看视频讲解。
  那么什么是IOC,控制反转(Inversion of Control,英文缩写为IOC),其实就是反射加注解如果你学过Java后台这个在三大框架中会经常使用。过多的去解释其实也没什么意思,我们主要来看有什么用处。
  附视频讲解地址:http://pan.baidu.com/s/1kVFMRQJ

2.第三方IOC框架源码解析


今天主要讲的就是Android中IOC框架就是注入控件和布局或者说是设置点击监听,如果你用过xUtils,afinal,butterknife类的框架,你肯定不陌生~   我们挑两个做一下对比和源码分析,我们就挑xUtils和butterknife这两个代表:

2.1 xUtils的IOC注解使用

xutils如果大家使用过的话它里面的内容会比较多,包括网络,数据库,IOC注入,网络图片使用,那么我们这里主要看看xutils3.0的IOC注解:https://github.com/wyouflf/xUtils3

public class MainActivity extends AppCompatActivity {

    @ViewInject(R.id.icon)
    private ImageView mIconIv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        x.view().inject(this);

        mIconIv.setImageResource(R.drawable.icon);
    }

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

自己动手打造一套IOC注解框架

这里写图片描述

  
  我就是设置一张图片和一个点击事件而已,其实主要解决的就是我们不再需要findViewById()和setOnClickListener(),我们简单的来看一下源码到底是怎么实现的:
  
 2.2 xUtils的IOC注解源码解析    
  我就挑一些关键的代码来分析一下,视频里面会给大家讲得非常详细,我们主要看一下x.view().inject(this);到底是干了什么:

 @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));
    } 
private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
     // .......

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

        // inject view  注入控件View
        Field[] fields = handlerType.getDeclaredFields();
        if (fields != null && fields.length > 0) {
            for (Field field : fields) {
              // ......
               // 获取viewInject 注解
                ViewInject viewInject = field.getAnnotation(ViewInject.class);
                if (viewInject != null) {
                    try {
                       // 其实最终还是调用findViewById的方法
                        View view = finder.findViewById(viewInject.value(),
                             viewInject.parentId());
                        if (view != null) {
                            // 利用反射 把View注入到该属性中
                            field.setAccessible(true);
                            field.set(handler, view);
                        } else {
                           // ......
                        }
                    } 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) {
          // ......
                //检查当前方法是否是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

    } 

关键的源码大概就这么多,动态代理的部分没有贴出来,这个写可能写不清楚大家可以自己去看看源码或者自己去网上搜搜动态代理的设计模式分析,视频里面会给大家讲清楚。动态代理我记得我刚刚自学那会还真是一道坎但是这个坎我们得迈过去,后面我们讲Android的Hook技术以及插件开发会反复的讲到。
  xutils相对来说应该不难理解吧,其实就是我们利用类的反射循环获取属性的注解值然后通过findViewById之后,动态的注入到控件属性里面;事件注入也是类似首先findViewById然后利用动态代理去反射执行方法。

2.3 butterknife的使用
  
  相比于xutils来讲它可能更受大家的欢迎,第一在性能方面xutils完全是利用的反射,butterknife是轻量级的反射使用的注解都是编译时注解,而且它还提供了一个Android Studio的插件不需要我们去写任何的代码,作者JakeWharton很出名的写过很多大型的第三方框架,https://github.com/JakeWharton/butterknife

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.icon)
    ImageView icon;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }

    @OnClick(R.id.icon)
    public void onClick() {
        Toast.makeText(this, "图片点击了", Toast.LENGTH_LONG).show();
    }
} 

上面是由插件给我们自动生成的代码,我们再也不用手动去写这些代码了。属性是不能private,onClick()方法也不能private,否则会报错的待会我们看源码的实现就知道为什么只能这样,插件自动生成的名字和方法总感觉怪怪的和我们的Android源码规范不一致,当然后面我们会自己去写一个Android Studio插件来配合我们自己的IOC注解框架。

2.4 butterknife的源码解析

源码阅读相对于xutils的也麻烦很多,如果我们按照常规的逻辑去找ButterKnife.bind(this);会发现里面好像没什么东西只有这个:

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
      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);
    }
  } 

如果从这里看我们好像看不到任何的东西,其实工作流程是怎样的呢?我们可以看一下Bind这个Annotation注解类和ButterKnifeProcessor这两个类其实就能找到线索:

@Retention(CLASS) @Target(FIELD)
public @interface Bind {
  /** View ID to which the field will be bound. */
  int[] value();
} 

@Retention 为 CLASS 的 Annotation,由apt(Annotation Processing Tool) 解析自动解析。ButterKnife便是用了Java Annotation Processing技术,就是在Java代码编译成Java字节码的时候就已经处理了@Bind、@OnClick(ButterKnife还支持很多其他的注解)这些注解了。

你可以你定义注解,并且自己定义解析器来处理它们。Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。下面我们应该就大概知道工作流程了吧?

2.5 ButterKnife 工作流程

当你编译你的Android工程时,ButterKnife工程中ButterKnifeProcessor类的process()方法会执行以下操作:

  • 开始它会扫描Java代码中所有的ButterKnife注解@Bind、@OnClick、@OnItemClicked等。

  • 当它发现一个类中含有任何一个注解时,ButterKnifeProcessor会帮你生成一个Java类,名字类似$$ViewBinder,这个新生成的类实现了ViewBinder接口。

  • 这个ViewBinder类中包含了所有对应的代码,比如@Bind注解对应findViewById(), @OnClick对应了view.setOnClickListener()等等。

  • 最后当Activity启动ButterKnife.bind(this)执行时,ButterKnife会去加载对应的ViewBinder类调用它们的bind()方法。

现在我们总该明白为什么我们的生成的属性和方法不能私有了吧?我们最后看一下编译时生成的class类吧

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131427372, "field 'icon' and method 'onClick'");
    target.icon = finder.castView(view, 2131427372, "field 'icon'");
    view.setOnClickListener(
      new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(View p0) {
           target.onClick();
        }
      });
  }

  @Override public void unbind(T target) {
    target.icon = null;
  }
} 

如果现在要我们来选,我肯定会选butterknife这个主要是因为它有一个插件我完全不用写任何的代码,而且没有利用不是全反射在性能方面也有一点提升,但是有的时候我想加一个检测网络的注解,这就有点麻烦了,插件生成的属性名称和方法名也有点蛋疼,我们自己来试着写写吧。

3.自己动手丰衣足食


接下来我们自己来实现一套IOC注解框架吧,采用的方式反射加注解和Xutils类似,但我们尽量不写那么麻烦,也不打算采用动态代理,我们扩展一个检测网络的注解,比如没网的时候我们不去执行方法而是给予没有网络的提示同时也不允许用户反复点击。
  这个时候有人就开始喷了,明知道反射会影响性能为什么还要用?这里我就随便说说吧,我承认反射会影响性能但是问题不大我们可以自己去测试反射1万次大概会怎样,如果你非得去纠结那我也没办法,我们还是多花时间在UI渲染和Bitmap以及Service和Handler上面吧,我还从来没有遇到过反射调用gc或者内存溢出的情况,而且后面讲插件化开发的时候也会用到反射那砸门就不做了?不管了开工。
  
 3.1 控件属性注入
  
  这里我就不在介绍Annotation的使用了,如果对于这个不是特别了解的大家可以自己去查一查资料或者看一下我录制的视频吧,先来处理控件属性的注入,但是需要考虑各种情况:

/**
 * Created by Darren on 2017/2/4.
 * Email: 240336124@qq.com
 * Description:  IOC的View属性注解类
 */
// RUNTIME 运行时检测,CLASS 编译时butterKnife使用是这个  SOURCE 源码资源的时候
@Retention(RetentionPolicy.RUNTIME)
// FIELD 注解只能放在属性上    METHOD 方法上  TYPE 类上  CONSTRUCTOR 构造方法上
@Target(ElementType.FIELD)
public @interface ViewById {
    // 代表可以传值int类型  使用的时候:ViewById(R.id.xxx)
    int value();
} 
/**
 * Created by Darren on 2017/2/4.
 * Email: 240336124@qq.com
 * Description: IOC 注入 ViewUtils
 */
public class ViewUtils {

    public static void inject(Activity activity) {
        inject(new ViewFinder(activity), activity);
    }

    // 兼容View
    public static void inject(View view) {
        inject(new ViewFinder(view), view);
    }

    // 兼容Fragment
    public static void inject(View view, Object object) {
        inject(new ViewFinder(view), object);
    }

    private static void inject(ViewFinder viewFinder, Object object) {
        injectFiled(viewFinder, object);
        injectEvent(viewFinder, object);
    }

    // 注入事件
    private static void injectEvent(ViewFinder viewFinder, Object object) {

    }

    /**
     * 注入属性
     */
    private static void injectFiled(ViewFinder viewFinder, Object object) {
        // object --> activity or fragment or view 是反射的类
        // viewFinder --> 只是一个view的findViewById的辅助类

        // 1. 获取所有的属性
        Class<?> clazz = object.getClass();
        // 获取所有属性包括私有和公有
        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            // 2. 获取属性上面ViewById的值
            ViewById viewById = field.getAnnotation(ViewById.class);

            if (viewById != null) {
                // 获取ViewById属性上的viewId值
                int viewId = viewById.value();
                // 3. 通过findViewById获取View
                View view = viewFinder.findViewById(viewId);

                if (view != null) {
                    // 4. 反射注入View属性
                    // 设置所有属性都能注入包括私有和公有
                    field.setAccessible(true);
                    try {
                        field.set(object, view);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                } else {
                    throw new RuntimeException("Invalid @ViewInject for "
                            + clazz.getSimpleName() + "." + field.getName());
                }
            }
        }
    }
} 

3.2 点击事件注入
   事件的注入我们只打算setOnclickListener其他不常见的我们先不管,也不打算采用动态代理的设计模式。

// 事件注入
private static void injectEvent(ViewFinder viewFinder, Object object) {
        // 1.获取所有方法
        Class<?> clazz = object.getClass();
        Method[] methods = clazz.getDeclaredMethods();
        // 2.获取方法上面的所有id
        for (Method method : methods) {
            OnClick onClick = method.getAnnotation(OnClick.class);
            if (onClick != null) {
                int[] viewIds = onClick.value();
                if (viewIds.length > 0) {
                    for (int viewId : viewIds) {
                        // 3.遍历所有的id 先findViewById然后 setOnClickListener
                        View view = viewFinder.findViewById(viewId);
                        if (view != null) {
                            view.setOnClickListener(new DeclaredOnClickListener(method, object));
                        }
                    }
                }
            }
        }
    }


    private static class DeclaredOnClickListener implements View.OnClickListener {
        private Method mMethod;
        private Object mHandlerType;

        public DeclaredOnClickListener(Method method, Object handlerType) {
            mMethod = method;
            mHandlerType = handlerType;
        }

        @Override
        public void onClick(View v) {
            // 4.反射执行方法
            mMethod.setAccessible(true);
            try {
                mMethod.invoke(mHandlerType, v);
            } catch (Exception e) {
                e.printStackTrace();
                try {
                    mMethod.invoke(mHandlerType, null);
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
        }
    } 
public class MainActivity extends AppCompatActivity {

    @ViewById(R.id.icon)
    private ImageView mIconIv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewUtils.inject(this);
        mIconIv.setImageResource(R.drawable.icon);
    }

    @OnClick(R.id.icon)
    private void onClick(View view) {
        int i = 2 / 0;
        Toast.makeText(this, "图片点击了"+i, Toast.LENGTH_LONG).show();
    }
} 

使用起来和xutils类似,方法和属性可以私有,但是有一点我们在Onclick点击事件的方法里面无论做什么操作都是不会报错的,所以如果发现bug需要留意警告日志,这不是坑嗲吗?其实在我们的开发过程给用户或者老板玩的时候我们最怕的是闪退,现在我们就算有Bug也不会出现闪退的情况只是调试的时候需要留意警告日志还是蛮不错的。
   3.3 扩展动态检测网络注解

我们最后扩展一下加一个检测网络的注解,有的时候我们在点击的方法里面需要去检测网络,比如登陆注册,我们如果没网就没必要去调接口启动线程了,只需要提示用户当前无网络即可。当然这只是一个扩展而已。

public class MainActivity extends AppCompatActivity {

    @ViewById(R.id.icon)
    private ImageView mIconIv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewUtils.inject(this);
        mIconIv.setImageResource(R.drawable.icon);
    }

    @OnClick(R.id.icon)
    @CheckNet          // 检测网络
    private void onClick(View view) {
        Toast.makeText(this, "图片点击了", Toast.LENGTH_LONG).show();
    }
} 

扩展我们就写这么个例子吧,这是内涵段子框架搭建的第一期分享,希望大家可以先去了解一下我们所有的分享内容2017Android进阶之路与你同行,

附视频讲解地址:http://pan.baidu.com/s/1kVFMRQJ

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这