使用注解打造自己的IOC框架

作者: GitLqr | 来源:发表于2017-11-28 19:43 被阅读1173次

    一、简述

    IoC和AOP可谓是后台开发入门必学的知识(Spring相关),但这两者都仅仅只是概念而已,并非具体技术实现,同样的,Android也可以使用IoC和AOP,之前已经写过如何在Android开发中使用AOP了,有兴趣的朋友可以看我之前的博客(顺便点个关注吧),所以,本文主题便是IoC。

    控制反转(Inversion of Control,英文缩写为IoC)是框架的重要特征,并非面向对象编程的专用术语。它包括依赖注入(Dependency Injection,简称DI)和依赖查找(Dependency Lookup)。

    上述源至百度百科,对于第一次接触IoC的人可能有些晦涩难懂,其实,通俗来讲,就是本来我可以做的事我现在不想做了,交给框架来做。举个实际的例子,就是ButterKnife,它就是Android上IoC的典型,实现了控件的动态注入及点击事件的绑定。所以,下面我们就来打造一个类似ButterKnife的IoC框架吧。

    二、框架实现

    下面是ButterKnife在GitHub上的代码示例:

    class ExampleActivity extends Activity {
      @BindView(R.id.user) EditText username;
      @BindView(R.id.pass) EditText password;
    
      @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...
      }
    }
    

    它包含3部分:

    • 控件注入使用@BindView注解
    • 点击事件的绑定使用@OnClick注解
    • 在onCreate()方法中调用ButterKnife.bind(this)

    所以,我们要模仿ButterKnife,先从@BindView和@OnClick这两个注解入手。

    1、注解

    注意,不管是控件注入还是点击事件绑定,都必须跟控件的id扯上关系,所以这两个注解中都会有一个属性用于表示控件的id。代码如下:

    // 控件注入注解
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface BindView {
        int value();
    }
    
    // 控件点击事件注解
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ClickView {
        int value();
    }
    

    因为我不想事件绑定的注解名为OnClick,所以这里的注解命名为ClickView,效果一样的。

    其中,BindView注解用于控件的注入,即类字段,所以其Target取值ElementType.FIELD,而ClickView注解用于控件的点击事件绑定,即方法,所以其Target取值ElementType.METHOD;并且,这两个注解都是在App运行期间被框架所使用,即运行时可见,所以,Retention取值为RetentionPolicy.RUNTIME。这俩注解在编码上的使用见如下代码:

    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.btn_hello)
        Button mBtnHello;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        @ClickView(R.id.btn_hello)
        public void sayHello() {
            Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show();
        }
    }
    

    但这样是不够的,因为注解可以认为只是一个标记,是静态的,它并没有实现控件注入与事件绑定的功能,控件的获取实际上还是需要findViewById()来实现,而事件的绑定同样也需要setOnClickListener()来实现,这也正是框架要为我们所做的工作。

    2、控件注入与事件绑定的实现

    ButterKnife不是这么实现的,这只是我个人的想法而已。

    1. 控件注入:实际上是框架调用了activity的findViewById()方法拿到id对应的控件,再通过反射的方式,对控件(类字段)进行赋值。
    2. 事件绑定:实际上也是框架调用了activity的findViewById()方法拿到id对应的控件,再调用控件的setOnClickListener()设置控件的点击事件,在这个点击事件里通过反射调用Activity中被ClickView注解的sayHello()方法而已。

    下面就来动手实现它吧:

    public class ViewUtil {
    
        public static void inject(final Activity activity) {
            // 拿到Activity的class对象
            Class clazz = activity.getClass();
    
            // 遍历属性
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                // 找到有BindView注解的属性
                BindView bindView = field.getAnnotation(BindView.class);
                if (bindView != null) {
                    try {
                        // 让属性可被访问(如果属性使用final和jprivate,则必须使其可访问,否则以下操作会报错)
                        field.setAccessible(true);
                        // 通过id获取到View,再对属性赋值
                        field.set(activity, activity.findViewById(bindView.value()));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            // 遍历方法
            Method[] methods = clazz.getDeclaredMethods();
            for (final Method method : methods) {
                // 找到有ClickView注解的方法
                ClickView clickView = method.getAnnotation(ClickView.class);
                if (clickView != null) {
                    // 通过id获取到View,再对view设置点击事件
                    activity.findViewById(clickView.value()).setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            try {
                                method.setAccessible(true);
                                // 调用这个被ClickView注解的方法
                                method.invoke(activity);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        }
    }
    

    3、试试

    功能既已实现,下来就来试试看,是否真的有效,在原先代码的onCreate()方法中加上ViewUtil.inject(this):

    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.btn_hello)
        Button mBtnHello;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ViewUtil.inject(this);
        }
    
        @ClickView(R.id.btn_hello)
        public void sayHello() {
            Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show();
        }
    }
    

    如果控件注入成功,则当点击控件时,会吐司"hello"。

    三、拓展

    上面的测试很成功啊,不过,这个框架目前只能给Activity使用,而ButterKnife可不只如此,不管Activity还是Fragment都能通吃,所以,我们这个框架也要适用于Fragment。

    1、Activity与Fragment获取控件的不同

    不管是控件注入还是事件绑定,都离不开最初始的一点,那就是控件的获取,即findViewById()。Activity获取控件只需要调用自己的findViewById()方法即可,但Fragment可不是这样,先来看看Fragment是如何设置布局的:

    public class MyFragment extends Fragment {
        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            if (mRootView == null) {
                mRootView = inflater.inflate(R.layout.fragment_my, null, false);
            }
            return mRootView;
        }
    }
    

    之所以Activity可以调用自己的findViewById()方法来获取控件,是因为Activity本身就是布局,而Fragment则不是这样的,Fragment的布局是它自己的一个View(mRootView),所以要获取Fragment中的控件,就必须调用mRootView的findViewById()方法来获取。

    2、代码抽取

    回顾ViewUtil的inject(Activity activity)方法,其实这个activity参数在这个方法中是担任两个角色的,一个是类(容器),另一个是布局。当作为容器这个角色时,是为了使用反射获得其中的字段和方法并赋值或执行。而作为布局这个角色时,是为了通过id获取布局控件(findViewById)。再看看Fragment,是不是有点端倪了呢?Fragment就是容器角色,而它的mRootView则是布局角色,所以,inject()的方法体可以这么抽:

    private static Context context;
    private static void injectReal(final Object container, Object rootView) {
        if (container instanceof Activity) {
            context = (Activity) container;
        } else if (container instanceof Fragment) {
            context = ((Fragment) container).getActivity();
        } else if (container instanceof android.app.Fragment) {
            context = ((android.app.Fragment) container).getActivity();
        }
    
        Class clazz = container.getClass();
        // 遍历属性
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null) {
                try {
                    field.setAccessible(true);
                    field.set(container, findViewById(rootView, bindView.value()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    
        // 遍历方法
        Method[] methods = clazz.getDeclaredMethods();
        for (final Method method : methods) {
            ClickView clickView = method.getAnnotation(ClickView.class);
            if (clickView != null) {
                findViewById(rootView, clickView.value()).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        try {                            
                            method.setAccessible(true);
                            method.invoke(container);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        }
    }
    
    private static View findViewById(Object layout, int resId) {
        if (layout instanceof Activity) {
            return ((Activity) layout).findViewById(resId);
        } else if (layout instanceof View) {
            return ((View) layout).findViewById(resId);
        }
        return null;
    }
    

    如此抽取之后,Activity与Fragment对应的inject()方法就可以共同使用这个injectReal()方法了:

    // Activity
    public static void inject(Activity activity) {
        injectReal(activity, activity);
    }
    
    // v4 Fragment
    public static void inject(Fragment container, View rootView) {
        injectReal(container, rootView);
    }
    
    // app Fragment
    public static void inject(android.app.Fragment container, View rootView) {
        injectReal(container, rootView);
    }
    

    相当清晰,而且是可以成功的,这里就不测试了。

    最后贴下Demo地址

    https://github.com/GitLqr/IocDemo

    相关文章

      网友评论

        本文标题:使用注解打造自己的IOC框架

        本文链接:https://www.haomeiwen.com/subject/hhnebxtx.html