xUtils源码分析 + 手写一个IOC注解框架

2021-07-10  本文已影响0人  碧云天EthanLee
一、概念

说说IOC,控制反转(Inversion of Control)。概念的东西不做过多解释,比较典型的做法是使用了反射加注解。我们如果用过xUtils或者ButterKnife对这种形式应该不陌生,这些框架可以减少很多在开发过程当中的刻板代码的书写。而xUtils和ButterKnife的实现方式还不太一样,xUtils全面地使用了反射,而ButterKnife则主要使用了编译时注解轻量级反射。本文最后要全面使用反射搭建注解框架,所以下面对xUtils的源码稍作分析。下面是两个框架源码的地址:
xUtils : https://github.com/wyouflf/xUtils3
ButterKnife : https://github.com/JakeWharton/butterknife

二、xUtils 源码分析

xUtils 包含的内容比较多,有网络、数据库、IOC注入、网络图片使用等,这里主要讲讲IOC注入。
先看一下xUtils及基本使用:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       // 注入
        x.view().inject(this);
    }
    // type 这个参数可选,默认是OnClickListener
    @Event(R.id.main_layout, type = View.OnClickListener.class)
    private void showToast(){
        Toast.makeText(this, "xUtil test .", Toast.LENGTH_SHORT).show();
    }
}

使用比较简单,来看看源码 inject() 这个方法是怎么注入的。找到这个类 :ViewInjectorImpl.java 。里面代码不多,就一百多行,主要的注入逻辑在这个方法里 :injectObject() 。现在挑主要部分看看:

@SuppressWarnings("ConstantConditions")
    private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
        ..........
        //  注释 1 view 的注入
        Field[] fields = handlerType.getDeclaredFields();
        if (fields != null && fields.length > 0) {
            for (Field field : fields) {
                Class<?> fieldType = field.getType();
                ......
               // 注解 ViewInject.class   注释 3
                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

        // 注释 2 event 事件注入
        Method[] methods = handlerType.getDeclaredMethods();
        if (methods != null && methods.length > 0) {
            for (Method method : methods) {
                //// 注解 Event.class   注释 4
                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.addEventMethod(finder, info, event, handler, method);
                            }
                        }
                    } catch (Throwable ex) {
                        LogUtil.e(ex.getMessage(), ex);
                    }
                }
            }
        } // end inject event
    }

删除了一小部分。这就清晰明了了。用反射获取了对象里的所有属性以及所有的方法(包括私有的)。然后就是两个 for 循环,分别遍历了对象里的所有属性和所有方法。分别找到被ViewInject.class注解标记的属性以及被Event.class注解标记的方法(注释3、注释4)。然后分别对属性进行 View的注入(field.set(handler, view);)、对方法进行事件添加。可以看到,是框架为我们写了 findViewById,以及用反射为我们调用了方法。

三、手写一个简单IOC注解框架

下面仿照 xUtils手写一个IOC注解框架。主要实现 View的注入及事件的注入,除了点击事件外,还有一个网络检测的注解。
1.自定义注解
先自定义三个注解,分别标记 View的注入、点击事件及网络检查。

FindView.java
/******************************************************************/
/**
 * View 注入
 */
// RUNTIME-运行时,CLASS-编译时,SOURCE-元注解
@Retention(RetentionPolicy.RUNTIME)
//作用于 属性:FIELD , 方法:METHOD , 类:TYPE , 参数:PARAMETER
// 构造器:CONSTRUCTOR , 注解:ANNOTATION_TYPE
@Target(ElementType.FIELD)
public @interface FindView {
    // 穿 View的 id
    int value();
}

OnClick .java
/*********************************************************************/
/**
 * 点击事件
 */
// 运行时
@Retention(RetentionPolicy.RUNTIME)
// 方法
@Target(ElementType.METHOD)
public @interface OnClick {
    int value();
}
CheckNet .java
/****************************************************************/
/**
 * 网络检查
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckNet {

}

上面就是自定义框架使用到的三个注解,有注释。注解的定义就不细说了。

2.实现注入
看代码:

// CleanBinder.java
 /**
     * 绑定 View
     *
     * @param object
     */
    private static void bindView(ViewFinder viewFinder, Object object){
        if (object == null) return;
        Class<?> clazz = object.getClass();
        // 反射获取对象里所有属性
        Field [] fields = clazz.getDeclaredFields();
        // 遍历所有属性,找到添加了 FindView注解的,然后绑定
        for (Field field : fields){
            FindView findView = field.getAnnotation(FindView.class);
            if (findView != null){
                // 1 拿到注解里声明的 id
                int viewId = findView.value();
                // 2 根据 id 获取到 View
                View view = viewFinder.findViewById(viewId);
                // 设置属性可访问,包括私有的
                field.setAccessible(true);
                // 3 再将 View与属性绑定
                try {
                    field.set(object, view);
                } catch (IllegalAccessException e) {
                    Log.d("CleanBinder", "IllegalAccessException - " + field.getName().toString());
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 绑定点击事件注解
     *
     * @param object
     */
    private static void bindMethod(ViewFinder viewFinder, Object object){
        if (object == null) return;
        Class<?> clazz = object.getClass();
        // 获取所有方法
        Method[] methods = clazz.getDeclaredMethods();
        // 遍历所有方法 找到有注解的
        for (Method method : methods){
            OnClick onClick = method.getAnnotation(OnClick.class);
            if (onClick == null) continue;
            // 1 拿注解里声明的 ID
            int viewID = onClick.value();
            // 2 再拿 View
            View view = viewFinder.findViewById(viewID);
            // 3 监听
            view.setOnClickListener((View  view1) -> {
                CheckNet checkNet = method.getAnnotation(CheckNet.class);
                if ((checkNet != null) && (!isNetAvailable((Context) object))){
                    Toast.makeText((Context) object, "请检查网络连接", Toast.LENGTH_SHORT).show();
                    return;
                }
                // 访问权
                method.setAccessible(true);
                try {
                    // 调用点击事件绑定的方法
                    method.invoke(object);
                } catch (IllegalAccessException e) {
                    Log.d("CleanBinder", "IllegalAccessException - " + method.getName());
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    Log.d("CleanBinder", "InvocationTargetException - " + method.getName());
                    e.printStackTrace();
                }
            });
        }
    }

跟Utils差不多, 都是使用了反射遍历所有属性和方法。然后反射注入,就是上面用了两个方法。两个方法用来分别执行 View的注入及事件注入,其中第二个事件注入的方法实现了点击事件和网络检查。当点击事件来时,先进行网络检查。没有网络时就弹土司提示,而不再执行反射调用方法。

3.框架的使用

使用上肯定也要便捷:

public class MainActivity extends AppCompatActivity {
    @FindView(R.id.clean_id)
    private Button mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CleanBinder.bind(this);
        mTextView.setText("碧云天");
    }
    @OnClick(R.id.clean_id)
    @CheckNet
    private void showToast(){
        Toast.makeText(this, "碧云天", Toast.LENGTH_SHORT).show();
    }
}
Screenrecorder-2021-07-10-19-17-18-33120217102225241.gif
上面是使用方式和效果图。可以看到,点击事件和网络检查就分别使用了一个注解就实现,这种方式的意义就在于减少刻板代码量。断网后点击按钮时会在框架里弹出“请检查网络连接”的提示。整块功能主要部分就是上面两个方法。一共有5个类,其中3个是注解,有一个是工具类。源码:Github源码
上一篇 下一篇

猜你喜欢

热点阅读