Android_IOC容器实现View点击注入

什么是IOC?

看了很多文章说IOC是依赖注入,或者说是控制反转,其实这里有多个联系比较紧密的概念,纯理论概念的内容,感兴趣可以大概了解下:

DIP 依赖倒置原则(Dependency Inverse Rrinciple)

强调系统的“高层组件”不应当依赖于“底层组件”,并且不论是“高层组件”还是“底层组件”
都应当依赖于抽象。抽象不应当依赖于实现,实现应当依赖于抽象(软件设计原则)

IOC 控制反转( Inverse of Control)

一种反转流、依赖和接口的方式。就是将控制权“往高处/上层”转移,
控制反转是实现依赖倒置的一种方法(DIP的具体实现方 式)

DI 依赖注入(-Dependency Injection)

组件通过构造函数或者setter方法,将其依赖暴露给.上层,上层要设法取得组件的依赖, 并将其传递给组件
依赖注入是实现控制反转的一种手段(IloC的具 体实现方式)

IOC容器

依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架)

注: 本文主要是借IOC实现事件注入,简单串一下几个只是点,属于比较基础的点,所以写的不是很详细,都写在备注上了:

①实现布局的注入

布局的注入比较简单:
主要实现的是用注解的方式,取代Activity的 setContentView()方法:
创建一个注解,作用在类上(Demo中是Activity),用于取注解的值,也就是对应(Activity)要设置的布局的值
MainActivity只有两个控件,一个TextView,一个Button,代码就不贴了,有需看文末完整代码,

/**
 * ContentView注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
    //返回int类型的布局值
    int value();
}

MainActivity和BaseActivity中代码比较简单:
MainActivity

//使用ContentView注解
@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {
}

BaseActivity

/**
 * desc   : BaseActivity
 */
public class BaseActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //帮助子类实现布局,控件,点击事件注入
        InjectManager.inject(this);
    }
}

所以代码逻辑主要在InjectManager中实现,布局注入比较简单,直接看代码和备注吧,不做赘述,可以在Activity中的onResume()等方法中检验代码是否有效~:

/**
 * desc   : InjectManager
 * 注解管理类
 */
public class InjectManager {

    /**
     * @param activity
     */
    public static void inject(Activity activity){
        injectLayout(activity);
    }

    /**
     * @param activity
     * 布局注入
     */
    public static void injectLayout(Activity activity){
        //获取类
        Class<? extends Activity> clazz = activity.getClass();
        //获取作用在类上的注解,这里取的是ContentView类型的注解
        ContentView contentView = clazz.getAnnotation(ContentView.class);
        if (contentView!=null) {
            //获取ContentView注解的值,如Demo中Mainactivity取到的是 R.layout.activity_main
            int layoutId = contentView.value();
            try {
                //获取setContentView方法
                Method method = clazz.getMethod("setContentView", int.class);
                //执行setContentView方法
                method.invoke(activity,layoutId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

②实现控件的注入

控件的注入就是为了通过注解取代findViewById方法,类似ButterKnife,只不过实现方式不同,ButterKnife采用的是APT,这里主要就是使用的反射,实现上首先定义个注解,并在MainActivity中使用,代码如下:
InjectView注解

/**
 * desc   : InjectView
 * InjectView注解,取代findViewById
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
    int value();
}

MainActivity中使用

  @InjectView(R.id.btn)
    Button btn;

具体实现实在InjectManager中增加了一个方法 用于处理控件的注入,这里上面的布局注入相差不大,主要注意下需要将findViewById方法的返回值赋值给View,具体可以看下注释:

 /**
     * @param activity
     * 控件的注入
     */
    private static void injectView(Activity activity) {
        //获取类
        Class<? extends Activity> clazz = activity.getClass();
        //获取所有属性,包括private等
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            //获取属性上的InjectView类型的注解
            InjectView injectView = field.getAnnotation(InjectView.class);
            if (injectView != null) {
                //获取注解的值,就是View的id值
                int viewId = injectView.value();
                try {
                    //获取findViewById方法
                    Method method = clazz.getMethod("findViewById", int.class);
                    //执行findViewById方法
                    Object view = method.invoke(activity, viewId);
                    //打开私有访问权限,即private属性可以修改
                    field.setAccessible(true);
                    //将findViewById方法的返回值赋值给控件
                    field.set(activity,view);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

③点击事件的注入

首先我们都知道Android的点击事件和长按事件是这样的:

//点击事件
  btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });
//长按事件
   btn.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                return false;
            }
        });

可以看到他们的共同点

  • 监听方法名 setOnClickListener / setOnLongClickListener
  • 监听对象 View.OnClickListener() / View.OnLongClickListener()
  • 返回值 onClick(View v) / onLongClick(View v)
    那么我们定义一个EventBase注解需要将这是哪个特点体现出来,另外还要定义一个注解OnClick,EventBase作用在OnClick注解之上,而OnClick作用在回调的方法之上,就是响应View的点击事件或者长按事件的回调,也就是最终要在MainActivity中触发的方法~
    下面以注入点击事件为例
    首先我们定义两个注解:

EventBase注解主要是拿到三个必要信息,

/**
 * desc   : EventBase
 * 事件注入的注解
 */

@Target(ElementType.ANNOTATION_TYPE)//作用在其它注解之上
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
    //1.监听方法名 setOnClickListener / setOnLongClickListener
    String listenerSetter();

    //2.监听对象 View.OnClickListener() / View.OnLongClickListener()
    Class<?> listenerType();

    //3. 返回值  onClick(View v) /  onLongClick(View v)
    String callBackListener();
}
/**
 * desc   : OnClick
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter="setOnClickListener",listenerType= View.OnClickListener.class,callBackListener="onClick")
public @interface OnClick {
    int[] value();
}

然后在InjectManager中添加一个injectEvent的方法用于处理方法事件的注入,这里用到了动态代理,
目的是确定回调的方法,因为回调的方法有可能是onClick,也可能是onLongClick代码如下:

 /**
     * @param activity
     * 事件注入
     */
    private static void injectEvent(Activity activity) {
        //获取类
        Class<? extends Activity> clazz = activity.getClass();
        //获取方法
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            //获取方法上的注解
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                //获取注解的类型
                Class<? extends Annotation> annotationType = annotation.annotationType();
                if (annotationType != null) {
                    //获取EventBase注解
                    EventBase eventBaseAnnotation = annotationType.getAnnotation(EventBase.class);
                    //获取三个信息
                    if (eventBaseAnnotation!=null) {
                        //listenerSetter = setOnClickListener
                        String listenerSetter = eventBaseAnnotation.listenerSetter();
                        //listenerType = View.OnClickListener
                        Class<?> listenerType = eventBaseAnnotation.listenerType();
                        //
                        String callBackListener = eventBaseAnnotation.callBackListener();

                        try {
                            //获取OnClick注解的value方法
                            Method valueMethod = annotationType.getDeclaredMethod("value");
                            //获取OnClick注解的的值
                            int[] viewIds = (int[])valueMethod.invoke(annotation);
                            //代理的方式,根据注解类型,匹配对应的回调方法
                            //即View.OnClickListener 回调 onClick(View view)
                            //View.OnLongClickListener() 回调 onLongClick(View v)
                            ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(activity);
                            //添加需要拦截的方法,和替换执行的方法
                            //callBackListener onClick
                            //method clickEvent
                            Log.e("callBackListener:",callBackListener+"");
                            Log.e("method:",method.getName()+"");
                            listenerInvocationHandler.addMethod(callBackListener,method);
                            Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, listenerInvocationHandler);

                            for (int viewId : viewIds) {
                                //获取findViewById方法
                                Method findViewMethod = clazz.getMethod("findViewById", int.class);
                                //执行findViewById方法
                                View view = (View) findViewMethod.invoke(activity, viewId);

                                Method setter = view.getClass().getMethod(listenerSetter, listenerType);
                                setter.invoke(view,listener);
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

贴一下代理的实现:


/**
 * desc   : ListenerInvocationHandler
 * 代理的实现
 * AOP切面思想
 */
public class ListenerInvocationHandler implements InvocationHandler {
    //拦截的对象,可能是Activity可能是Fragment
    //这里是MainActivity的OnClick方法
    private Object target;
    //存储方法名和方法
    //这里key:OnClick value : 预期回调的目标方法 clickEvent
    private HashMap<String,Method> methodHashMap=new HashMap<>();

    public ListenerInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (target!=null) {
            String name = method.getName();
            method=methodHashMap.get(name);//集合中有需要拦截的方法名直接拦截替换
            if (method!=null) {
                return method.invoke(target,args);
            }
        }
        return null;
    }

    /**
     * @param methodName 要拦截的方法名methodName:OnClick
     * @param method 需要替换执行的方法method:clickEvent
     * 添加要拦截的方法
     */
    public void addMethod(String methodName,Method method){
        methodHashMap.put(methodName,method);
    }
}

另外OnLongClick的注解如下,和上面OnClick不同的是如果替换onLongClick方法,需要有个boolean返回值

/**
 * desc   : OnLongClick
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter="setOnLongClickListener",listenerType= View.OnLongClickListener.class,callBackListener="onLongClick")
public @interface OnLongClick {
    int[] value();
}

完整代码可见:github

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

推荐阅读更多精彩内容