Android View事件之IOC(依赖注入)实现

前言

在讲IOC(依赖注入)前 先来看一段代码 或者下载demo 进行细看

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    private final static String TAG=MainActivity.class.getSimpleName();
    @ViewInject(R.id.textview)
    private TextView textview;
    @ViewInject(R.id.button)
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtils.inject(this);
    }
    @OnClick({R.id.button})
    public void onClick(View view){
        Log.e(TAG,"点击事件");
        Toast.makeText(this,"我点击了button",Toast.LENGTH_SHORT).show();
    }
    @OnLongClick({R.id.textview})
    public void onLongClick(View view){
        Log.e(TAG,"长按事件");
        Toast.makeText(this,"我点击了textview",Toast.LENGTH_SHORT).show();
        return true;
    }
}

是不是很熟悉的感觉,目前网上能实现该功能比较火的库有:
XUtils
butterknife
dagger
daggerbutterknife是使用APT将.java文件在编译成.class文件的时候将注解对象添加到.class字节码当中 号称是无性能消耗的 这个不是本章重点 可以的话会在下一章进行介绍

XUtils是使用IOC(依赖注入)在运行时动态地将某种依赖关系注入到对象之中,因为使用到反射所以在性能上有点不及APT,本章将介绍如何使用IOC实现View事件对象的注入进行简单的案例讲解

IOC

什么是IOC呢?通俗的来讲:

指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。
我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言的的反射编程,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。

实际应用:

在实际项目开发过程中很多页面(Activity)都要去将各种View进行findViewById(...)将花费大量的时间去写重复性的代码而且代码整体看上去不够简洁不方便维护 利用IOC的原理我们可以通过注入的方式去将各个控件注入到每个Activity中 其好处有:

  • 整体代码变得简洁 容易维护
  • IOC是工厂模式的升华 是一种容器 把工厂和对象生成这两者独立分隔开来 通过注解的方式生成对象

使用

下面就教大家如何使用IOC架构进行事件注入,同时本篇使用到的相关知识有:
注解反射动态代理 若有某个知识点不会的朋友,因篇幅有限 请自行百度或Gooogle,同时我也会尽量把注释写明白,到这里那就开车吧:

自定义注解

//自定义ContentView注解 参数为int类型
@Retention(RetentionPolicy.RUNTIME)//运行时
@Target(ElementType.TYPE)//该注解使用在类上
public @interface ContentView {
    int value();
}

@Retention(RetentionPolicy.RUNTIME)//运行时
@Target(ElementType.FIELD)//应用在成员变量上
public @interface ViewInject {
    int value();
}

创建InjectUtils工具类做一些初始化和反射的操作

public class InjectUtils {
    public static void inject(Context context){
        injectLayout(context);
        injectView(context);       
    }
    /**
     * 注解Layout
     * @param context
     */
    private static void injectLayout(Context context) {
        int layoutId=0;
        Class<?> clazz=context.getClass();
        //获取类对象的 ContentView注解
        ContentView contentView=clazz.getAnnotation(ContentView.class);
        if (contentView!=null){
            //获取ContentView注解的返回值--->R.layout.xxx
            layoutId=contentView.value();
            try {
                //反射获取 context下的setContentView方法
                Method method=clazz.getMethod("setContentView",int.class);
                //执行该方法 ---->setContentView(layoutId)
                method.invoke(context,layoutId);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 注解view
     * @param context
     */
    public  static  void injectView(Context context){
        Class<?> aClass=context.getClass();
        //获取该类下的所有成员变量
        Field[] fields=aClass.getDeclaredFields();
        for(Field field:fields){
            //遍历所有带有ViewInject注解的成员变量
            ViewInject annotation = field.getAnnotation(ViewInject.class);
            if(annotation!=null){
                //获取注解的返回值---》R.id.xxx
                int valueID = annotation.value();
                try {
                    //反射获取findViewById函数
                    Method findViewById = aClass.getMethod("findViewById", int.class);
                    //执行findViewById函数获取--->View=findViewById(R.id.xxx)
                    View view = (View) findViewById.invoke(context, valueID);
                    //允许改变该成员变量的值
                    field.setAccessible(true);
                    //给该成员变量赋值--->xxxActivity.textView=view
                    field.set(context,view);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}    

使用

@ContentView(R.layout.activity_main)//自定义注解
public class MainActivity extends AppCompatActivity {
   
    @ViewInject(R.id.textview)//自定义注解
    private TextView textview;

    @ViewInject(R.id.button)//自定义注解
    private Button button;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //初始化
        InjectUtils.inject(this);
        textview.setText("IOC控制反转");
        button.setText("IOC依赖注入");
        }
 }       

一个简单的IOC架构就写好了 其原理就是InjectUtils去找到我们的自定义的注解@ContentView获取返回值(布局ID)去反射执行setContentView(layoutId)方法 同时获取所有含有@ViewInject注解的成员变量 并进行反射调用findViewById(R.id.xxx)为控件赋值 通过下图就可以看出 TextView和Button通注解实现了对象的创建

下面将来点有难度的 将View的事件进行注解注入 首先我们需要了解一个事件对象执行的要素:

  • 事件监听对象
  • 事件类型(点击还是长按?)
  • 监听方法(setOnLongClickListener/setOnClickListener)
  • 监听回调以及是否有返回值(onLongClick/onClick)

问题分析:

  • 事件监听对象 由上面的小例子可以想到我们可以通过自定义注解来获取到需要进行事件监听的View同时也可以通过反射调用该View的事件监听函数 区别在于怎么去判断调用哪一个监听函数(点击/长按?)
//(getMethod)类的所有共有方法 获取findViewById函数
Method findViewById = clazz.getMethod("findViewById", int.class);
//执行findViewById得到View
View view = (View) findViewById.invoke(context, viewId); 
//反射获取view的事件监听方法(事件函数,事件类型)
Method setListener = view.getClass().getMethod(listenerSetter, listenerType);
  • 同时因为不同的事件监听函数,所需要的事件对象类型也不同 以及不同的回调函数和返回值等这些都要考虑解决

解决思路:
在开篇的那段代码中可以看到最理想的解决方案是这样的 通过定义不同的自定义注解比如@OnClick({R.id.button})@OnLongClick({R.id.textview})来区分事件类型(点击/长按),这样我们可以拿到需要监听事件的View对象:

@OnClick({R.id.button})
public void onClick(View view){
       Log.e(TAG,"点击事件");
   }
   
@OnLongClick({R.id.textview})
public void onLongClick(View view){
       Log.e(TAG,"长按事件");
      return true;
   }
       

到这里我们就有2条思路去解决后面的问题:如何判断反射调用哪一个监听函数(点击/长按?)

  • 根据注解名称的不同来区分反射调用哪一个监听函数
........
String name=annotation.getClass().getSimpleName()
swicth(name){
   case OnClick:
       .........
       break;
  case OnLongClick:
       .........
       break;
}
  • 通过注解特性我们知道注解不但可以用在 类,方法,成员变量等上面 还可以用在注解上面 而且还可以可以设置元数据供程序读取 所以可以根据注解函数返回值来做区别 开始写代码:
//自定义EventBase 并且有3个返回值函数(事件监听的方法、事件类型、事件回调方法)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)//定义在注解上使用
public @interface EventBase {
    /**
     * 事件监听的方法
     * @return
     */
    String listenerSetter();

    /**
     * 事件类型
     * @return
     */
    Class<?> listenerType();

    /**
     * 事件回调方法
     * 事件触发后 执行回调方法
     * @return
     */
    String callBackMethod();
}

使用@EventBase自定义注解到自定义注解上面 并且根据不同的自定义事件注解 设置不同的注解函数返回值 (这样的扩展性非常好)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnClickListener",//点击事件
          listenerType = View.OnClickListener.class,//接口
          callBackMethod = "onClick")//接口回调
public @interface OnClick {
   /**
    * 需要点击事件的View
    * @return
    */
   int[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnLongClickListener",
          listenerType = View.OnLongClickListener.class,
          callBackMethod = "onLongClick")
public @interface OnLongClick {
   /**
    * 需要长按事件的View
    * @return
    */
   int [] value() default -1;
}

到这里问题解决一大半 还有最后一个问题就是回调函数的执行 这里需要用到动态代理去实现

public class InjectUtils {
    public static void inject(Context context){
        injectLayout(context);//--->setContentView(layoutId)
        injectView(context);//--->findViewById(R.id.xxx)
        injectEvents(context);
    }

    private static void injectEvents(Context context) {
        Class<?> clazz=context.getClass();
        //保存函数对应的事件回调方法
        Map<String,Method> methodMap=new HashMap<>();
        //获取Activtiy里面所有的方法
        Method[] methods=clazz.getDeclaredMethods();
        //遍历
        for(Method method:methods){
            //获取该函数上的所有注解
            Annotation[] annotations=method.getAnnotations();
            //遍历该函数注解
            for (Annotation annotation:annotations){
                //获取该注释的注释类型
                Class<?> anntionType=annotation.annotationType();
                //获取注解上面的EventBase的注解
                EventBase eventBase=anntionType.getAnnotation(EventBase.class);
                //判空
                if(null==eventBase) {continue;}
                //获取EventBase注解3个函数的返回值
                // 也就是事件三要素(监听的方法,事件类型,回调函数)
                String listenerSetter = eventBase.listenerSetter();
                //事件类型 长按 还是点击
                Class<?> listenerType = eventBase.listenerType();
                //事件回调--onClick()
                String backMethod = eventBase.callBackMethod();
                //将该函数与对应的事件回调方法保存到map中
                methodMap.put(backMethod,method);
                try {
                    //(getDeclaredMethod)获取的是类自身声明的所有方法,包含public、protected和private方法
                    Method valueMethod = anntionType.getDeclaredMethod("value");
                    //获取函数注解的返回值(view id)
                    int[] viewIds = (int[]) valueMethod.invoke(annotation);
                    for(int viewId:viewIds){
                        //(getMethod)类的所有共有方法 获取findViewById函数
                        Method findViewById = clazz.getMethod("findViewById", int.class);
                        //执行findViewById得到View
                        View view = (View) findViewById.invoke(context, viewId);
                        if (null==view){continue;}
                        //反射获取view的事件监听方法(事件函数,事件类型)
                        Method setListener = view.getClass().getMethod(listenerSetter, listenerType);
                        //创建代理类对象
                        ListenerInvocationHandler handler=new ListenerInvocationHandler(context,methodMap);
                        //proxyInstance实现listenerType(事件类型)接口
                        Object proxyInstance = Proxy.newProxyInstance(anntionType.getClassLoader(), new Class[]{listenerType}, handler);
                        //执行事件回调方法
                        setListener.invoke(view,proxyInstance);
                    }
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

其中这段代码使用了动态代理 关于动态代理这里不做详细介绍:

 //创建代理类对象
ListenerInvocationHandler handler=new ListenerInvocationHandler(context,methodMap);
//proxyInstance实现listenerType(事件类型)接口
Object proxyInstance = Proxy.newProxyInstance(anntionType.getClassLoader(), new Class[]{listenerType}, handler);
//执行事件回调方法
setListener.invoke(view,proxyInstance);

动态代理类:

public class ListenerInvocationHandler implements InvocationHandler {
    private Context context;
    private Map <String,Method> methodMap;
    public ListenerInvocationHandler(Context context, Map<String, Method> methodMap) {
        this.context = context;
        this.methodMap = methodMap;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName=method.getName();//获取绑架事件监听的函数
        Method med= methodMap.get(methodName);//根据函数名获取事件监听方法
        if(null!=med){
            Log.e(TAG,"代理开始");
            return med.invoke(context,args);
        }else {
            return method.invoke(proxy,args);
        }
    }
}

大概的执行流程图:



Activtiy中使用:

@OnClick({R.id.button})
    public void onClick(View view){
        Log.e(TAG,"点击事件");
        Toast.makeText(this,"我点击了button",Toast.LENGTH_SHORT).show();
    }

@OnLongClick({R.id.textview})
public boolean onLongClick(View view){
        Log.e(TAG,"长按事件");
        Toast.makeText(this,"我长按了textview",Toast.LENGTH_SHORT).show();
        return true;
    }

最后看效果:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 清康熙年间,有艺人携一瓮,瓮藏小人,长约尺许。投以钱,则启盖令小人出,唱曲而退。至掖,掖令索瓮入署,细审小人出处。...
    百丑阅读 592评论 0 0
  • 天高云阔,微风吹来,空气中有淡淡的花香。赵亮心情大好,他升副处了!干了好几年的主任科员,好几次晋升都与他擦肩而过,...
    蜀月秋窗阅读 504评论 0 3
  • 早晨开了年终会议。让我印象较深的是董事长的几句反思:1、开始觉得自己不会带人。只管讲理念,从来没想过我们能不能接受...
    田田kyle阅读 129评论 0 0