Skip to content
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.idea
app
gradle/wrapper
screenshot
.gitignore
README.md
build.gradle
gradle.properties
gradlew
gradlew.bat
settings.gradle

README.md

IOCPractice

控制反转 IoC(Inversion of Control) 意思是把创建对象的权利交给框架,是框架的重要特性,并非面向对象编程的专业术语。----百度百科

在 Java 开发中最著名的 IoC 框架莫过于 Spring,在 Android 开发中,IoC 框架也有很多,比如:ButterKnife、EventBus、Dagger、Dagger2 等框架都运用了 IoC 的思想。本篇文章即介绍 IoC 在 Android 中的使用,通过开发一个简单的 IoC 框架帮助你更加深刻的理解 IoC 的思想及其相关知识。本篇中涉及到的代码均在 Github 上面IOCPractice

1. IoC 简介

IoC 控制反转不是一种技术,而是一种编程思想,体现着“不要找我们,我们找你”的思想。

在传统的编程写法中,大多都是在类内部通过关键字 new 主动创建类的实例对象并赋给引用,但是这样写会造成类和类之间耦合度过高的后果,难于测试。有了 IoC 容器之后,创建对象和查找依赖的工作就交给框架完成,并由容器注入对象,这样通过 IoC 容器实现了类和类之间的解耦。

IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源。 ---- 谈谈对Spring IOC的理解

提到 IoC 就不得不提到另一个概念:依赖注入(Dependence Injection,简称DI),DI 的概念是指组件之间的依赖关系由容器在运行期间决定。看完对 DI 的定义也许觉得很模糊,不清楚到底讲了什么意思。其实 IoC 和 DI 表达的是同一个意思,只不过是从不同的角度去描述。

1.1 IoC

在传统程序的编写过程中,在一个类中如果需要另外一个对象,就需要通过关键字 new 出另一个类的对象,然后才可以开始使用这个对象,使用完成之后,再将这个对象销毁,在这个过程中对象和对象之间耦合度很高。

有了 IoC 容器之后,所有的类都会在 IoC 容器中注册,并告诉 IoC 容器我是什么类,我需要什么对象,并且在运行到适当的时刻,将需要的对象创建并赋给我的引用,在适当的时刻再将该对象进行销毁。同时也会将创建我的对象,并赋给需要我的对象的类中。

在使用 IoC 容器框架编程时,对象的创建、销毁都是由 IoC 容器控制,而不是由引用它的对象控制。对于某个对象来说,以前是由它控制某个对象,现在是所有的对象都由 IoC 容器控制,所以叫控制反转。

1.2 DI

IoC 是指在程序运行期间,动态地向某个对象提供它所需要的对象,而 DI 就是实现这个思想的方法。比如,在 User 对象中需要使用到 Address 对象,我只需要告诉 IoC 容器,我需要一个 Address 对象,然后 IoC 容器会创建一个 Address 对象,然后像打针一样注入到 User 对象中,依赖注入就是这样来的。

在 Java 1.3 之后,可以通过反射在运行期间动态的创建对象、调用对象的方法、改变对象的属性,DI 就是通过反射实现注入的。

2. IoC 相关知识

在学习 IoC 之前,需要具备几点基础的 Java 知识,分别是注解、反射和动态代理,下面分别介绍。

2.1 注解

在平时写代码的时候,多多少少都会接触到注解,比如:@Override@Deprecated 等等,还有一些第三方库中也会有,比如 ButterKnife、EventBus、Dagger2 等。

用完之后只有一个感受:真是太方便了,其实注解并没有多么复杂,和写普通的类(Class)、接口(Interface)差不了多少。

首先,看一下我们平时使用注解的时候的代码,比如使用 ButterKnife 绑定一个控件或为一个控件添加点击事件的代码如下所示:

class ExampleActivity extends Activity {
  @BindView(R.id.user) EditText username;
  @BindView(R.id.pass) EditText password;

  @BindString(R.string.login_error) String loginErrorMessage;

  @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...
  }
}

那如果我们自己想写一个这样的注解,该怎么写呢?代码如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}

如果对注解不熟悉,看见上面的代码可能会感觉到懵逼,没关系,我们一一介绍其中的含义:

  • @Target(ElementType.FIELD):表示该注解的作用域,其取值有以下几个:

    public enum ElementType {
      TYPE,               // 类、接口(包括注解类型)、枚举类,
      FIELD,              // 属性
      METHOD,             // 方法
      PARAMETER,          // 参数
      CONSTRUCTOR,        // 构造方法
      LOCAL_VARIABLE,     // 局部变量
      ANNOTATION_TYPE,    // 注解
      PACKAGE,            //
    }
  • @Retention(RetentionPolicy.RUNTIME):表示在什么级别保留此信息:

    public enum RetentionPolicy {
        SOURCE,          // 源码注解,注解仅存在源码级别,在编译的时候丢弃该注解
    
        CLASS,           // 编译时注解,注解会在 class 文件中存在,但是在运行时会被丢弃
    
        RUNTIME          // 运行时注解,注解会被记录在 class 文件中,在 VM 运行时也会保留该注解,所以可以通过反射获得该注解
    }
  • @interface:表示此文件是一个注解,和 ClassInterface 一个级别

  • int value();:表示此注解的一个方法,可以通过注解的这个方法得到一个 int 注解值

2.2 反射

通俗的讲,反射就是把一个类、类的属性和类的方法当做对象一样来操作,在运行态的时候可以动态地创建对象、调用对象的方法、修改对象的属性。

比如现在有一个类,如下所示:

package com.lijiankun24.iocpractice;

public class User {

    private String name;

    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

通过普通的 new 的方式创建一个 User 的对象,并使用它,这种方式应该都非常熟悉了,但是用反射的方式该怎么实现呢?可以通过如下代码实现:

public class Main {

    public static void main(String[] args) {
        User user = newInstanceUser();
        invokeMethod(user);
        setField(user);
    }

    /**
     * 通过反射创建一个 User 对象
     *
     * @return 返回该 User 对象
     */
    private static User newInstanceUser() {
        Class<?> clazz = User.class;
        User user = null;
        try {
            Constructor<?>[] constructors = clazz.getConstructors();
            user = (User) constructors[0].newInstance("lijiankun", 20);
            System.out.println("The name is " + user.getName());
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return user;
    }

    /**
     * 通过反射调用 User 中的方法
     *
     * @param user User 对象
     */
    private static void invokeMethod(User user) {
        Class<?> clazz = user.getClass();
        try {
            Method method = clazz.getDeclaredMethod("setName", String.class);
            method.invoke(user, "24");
            System.out.println("The name is " + user.getName());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     * 通过反射修改 User 对象中的私有属性
     *
     * @param user User 对象
     */
    private static void setField(User user) {
        Class<?> clazz = user.getClass();
        try {S
            Field field = clazz.getDeclaredField("name");
            field.setAccessible(true);  // 如果方法或者属性是私有的,则需要设置它的访问性为 true
            field.set(user, "newName");
            System.out.println("The name is " + user.getName());
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

上述代码简单讲了下通过反射怎么动态地创建对象、调用对象的方法、修改对象的属性,至于更详细的反射则需要看书去学习。

2.3 动态代理

代理模式的概念就是:为原对象提供一个代理对象,原对象和代理对象对外提供相同的 API 方法,客户通过代理对象间接地操作原对象,但是客户并不知道它操作的是代理对象。

如上图所示,简单示意了代理模式的概念,其中

  • Subject:表示一个接口类,有一个公共的方法 request()
  • RealObject:实现了 Subject 接口,向外提供 request() 方法,是原对象(我们把它称之为委托对象)
  • ProxyObject:也实现了 Subject 接口,向外提供 request() 方法,是代理对象
  • Client:表示一个客户对象,想要调用 RealObjectrequest() 方法,但是并不是直接调用 RealObjectrequest() 方法,而是通过 ProxyObjectrequest() 方法间接地调用 RealObjectrequest() 方法

代理的概念如上所述,但是有不同的实现方式,分为静态代理和动态代理:

  • 静态代理:代理类在编译后就已经实现好了,也就是在编译之后,代理类就是一个 class 文件
  • 动态代理:代理类是在运行期间生成的。也就是说,在编译之后,并没有代理类对应的 class 文件,而是在运行期间动态的生成字节码文件,并加载到虚拟机中的。

动态代理

在 IoC 中会用到 Java 的动态代理,所以在这里也是重点讲一个 Java 动态代理。

在 Java 的动态代理中一定会涉及到一个接口和一个类,分别是:InvocationHandler(Interface)Proxy(Class)。下面我们通过一个例子,更加形象地学习 Java 的动态代理。

首先定义一个接口如下所示:

public interface Subject{
    void request();

    int add(int a, int b);
}

接着定义一个类实现这个接口,这个类就是我们的委托对象,代码如下:

public class RealObject implements Subject {

    @Override
    public void request() {
      System.out.println("RealObject request invoked");
    }

    @Override
    public int add(int a, int b) {
        return a + b;
    }
}

然后有一个调用处理器的类,这个调用处理器的类需要实现 InvocationHandler 接口,代码如下所示:

public class ProxyHandler implements InvocationHandler {

    private Subject mRealObject = null;

    public ProxyHandler(Subject realObject) {
        mRealObject = realObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("ProxyHandler PreProcess");
        method.invoke(mRealObject, args);
        System.out.println("ProxyHandler PostProcess");
        return null;
    }
}

最后就是一个 Client 类

public class Client {

    public static void main(String[] args) {
        Subject realObject = new RealObject();
        InvocationHandler handler = new ProxyHandler(realObject);

        Subject proxyObject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realObject.getClass().getInterfaces(), handler);
        proxyObject.request();
    }
}

通过上面的例子,我想大家对 Java 中的动态代理心里大概已经有概念,其实也不难,可以分为以下几个步骤:

  • 首先需要有一个接口,其中定义了委托类和代理类都会实现的方法
  • 定义委托类,实现该接口
  • 定义调用处理器的类,实现 InvocationHandler 接口,InvocationHandler 的源码如下所示,其中只有一个方法,这个方法非常重要:
public interface InvocationHandler {

    /**
     *
     * @param   proxy  指委托对象
     * @param   method 指调用的委托对象的方法的对象
     * @param   args   指调用的委托对象的方法的参数
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
  • 通过 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 方法创建一个代理对象,就可以调用代理对象的方法了,如下所示:
Subject proxyObject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realObject.getClass().getInterfaces(), handler);
proxyObject.request();

好了,以上就介绍了 IoC 在 Android 中应用所需要用到的 Java 知识,下面就通过实现一个简单的 IoC 框架,更加深入地理解 IoC 在 Android 中的应用吧。

3. IoC 在 Android 中的应用

如果大家接触过 ButterKnife 框架的话,对下面这样的代码一定不陌生,

class ExampleActivity extends Activity {
  @BindView(R.id.user) EditText username;
  @BindView(R.id.pass) EditText password;

  @BindString(R.string.login_error) String loginErrorMessage;

  @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...
  }
}

在传统的写法中,我们要向获得在 xml 文件中声明的控件实例,需要通过 findViewById 来实现,但是 ButterKnife 只是用 @BindView(R.id.xxx) 就实现了同样的效果。同样的,ButterKnife 也用 OnClick(R.id.xxx) 为某个控件设置了监听回调事件,用 BindString(R.string.xxx) 实现了为某个 String 对象赋值,那我们也尝试着用 IoC 实现这样的一个框架。

3.1 注入布局文件

首先编写一个注解文件

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
    int value();
}

然后编写 ButterKnife 管理文件,如下所示:

public class ButterKnife {

    private static final String METHOD_SET_CONTENTVIEW = "setContentView";

    public static void inject(Activity activity) {
        injectContentView(activity);
    }

    private static void injectContentView(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        // 在 Activity 类上查找 ContentView.class 的注解
        ContentView contentView = clazz.getAnnotation(ContentView.class);
        if (contentView != null) {
            // 得到 布局文件的 Id 值
            int layoutId = contentView.value();
            if (layoutId != -1) {
                try {
                    // 通过反射调用 Activity 的 setContentView(int layoutId) 方法
                    Method method = clazz.getMethod(METHOD_SET_CONTENTVIEW, int.class);
                    method.setAccessible(true);
                    method.invoke(activity, layoutId);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }  
}

Activity 中就可以使用它注入布局文件了

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ButterKnife.inject(this);
    }
}

3.2 注入 Views

首先编写注入 Views 的注解文件

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}

ButterKnife 中同样的去实现这个注解

public class ButterKnife {

    private static final String METHOD_SET_CONTENTVIEW = "setContentView";

    private static final String METHOD_FIND_VIEW_BY_ID = "findViewById";

    public static void inject(Activity activity) {
        injectContentView(activity);
        injectView(activity);
    }

    ......

    private static void injectView(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        // 首先获取到 Activity 中所有的属性
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 遍历查找每个属性上是否存在 BindView.class 注解
            BindView viewInject = field.getAnnotation(BindView.class);
            if (viewInject != null) {
                // 得到控件的 id 值
                int viewId = viewInject.value();
                if (viewId != -1) {
                    try {
                        // 通过反射调用 Activity 的 findViewById(int id) 方法
                        Method method = clazz.getMethod(METHOD_FIND_VIEW_BY_ID, int.class);
                        Object resView = method.invoke(activity, viewId);
                        // 并将得到的控件对象赋予 Activity 中的该属性
                        field.setAccessible(true);
                        field.set(activity, resView);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

然后在 Activity 中也可以使用这个注解

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv1)
    private TextView TV1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ButterKnife.inject(this);
        TV1.setText("Change the text");
    }
}

3.3 注入 Views 的事件监听

和上面有点不同的是,为了扩展的方便,需要为监听注解编写一个注解,如下所示

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventType {
    Class<?> listenerType();  // 监听接口的类型

    String listenerSetter();  // 设置监听接口的 set 方法

    String methodName();      // 监听接口中的回调方法名称
}

然后就是编写事件监听注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventType(listenerType = View.OnClickListener.class, listenerSetter = "setOnClickListener",
        methodName = "onClick")
public @interface OnClick {
    int[] value();
}

编写在 ButterKnife 中的使用

public class ButterKnife {

    private static final String METHOD_SET_CONTENTVIEW = "setContentView";

    private static final String METHOD_FIND_VIEW_BY_ID = "findViewById";

    public static void inject(Activity activity) {
        injectContentView(activity);
        injectView(activity);
        injectEvent(activity);
    }

    ......

    private static void injectEvent(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        // 得到 Activity 中所有的方法
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            // 遍历所有的方法,并得到方法上所有的注解对象
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                // 遍历每一个方法的所有注解
                Class<? extends Annotation> annotationType = annotation.annotationType();
                EventType eventBase = annotationType.getAnnotation(EventType.class);
                // 查找是否存在 EventType.class 注解
                if (eventBase != null) {
                    String listenerSetter = eventBase.listenerSetter();
                    String methodName = eventBase.methodName();
                    Class listenerType = eventBase.listenerType();
                    try {
                        // 通过反射调用注解的 value() 方法得到 viewIds 值
                        Method method1 = annotationType.getDeclaredMethod("value");
                        int[] viewIds = (int[]) method1.invoke(annotation, null);
                        // 通过 Java 动态代理的方式为每个 View 设置监听器
                        DynamicHandler handler = new DynamicHandler(activity);
                        handler.addMethod(methodName, method);
                        Object listener = Proxy.newProxyInstance(listenerType.getClassLoader()
                                , new Class[]{listenerType}, handler);
                        for (int viewId : viewIds) {
                            View view = activity.findViewById(viewId);
                            Method method2 = view.getClass().getMethod(listenerSetter, listenerType);
                            method2.invoke(view, listener);
                        }
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }

                }
            }
        }
    }
}

在上述注入事件监听的过程中,用到了 Java 的动态代理,所以需要额外的编写一个 InvocationHandler 调用处理器类,如下所示:

class ButterKnifeDynamicHandler implements InvocationHandler {

    private WeakReference<Object> mObjectWR = null;

    private final HashMap<String, Method> mMethodHashMap = new HashMap<>();

    ButterKnifeDynamicHandler(Object object) {
        mObjectWR = new WeakReference<>(object);
    }

    void addMethod(String methodName, Method method) {
        mMethodHashMap.put(methodName, method);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object handler = mObjectWR.get();
        String name = method.getName();
        method = mMethodHashMap.get(name);
        if (handler != null && method != null) {
            return method.invoke(mObjectWR.get(), args);
        }
        return null;
    }
}

最后在 Activity 中使用它即可:

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv1)
    private TextView TV1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ButterKnife.inject(this);
        TV1.setText("Change the text");
    }

    @OnClick({R.id.tv1, R.id.tv2})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.tv1:
                Toast.makeText(MainActivity.this, "IOC For Test tv1", Toast.LENGTH_SHORT).show();
                break;
            case R.id.tv2:
                Toast.makeText(MainActivity.this, "IOC For Test tv2", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

这样,一个使用 IoC 思想实现的仿 ButterKnife 框架就实现了,上面涉及到的代码都在 Github 上面:IoCPractice.


参考资料:

谈谈对Spring IOC的理解 -- Acrash

Android 进阶 教你打造 Android 中的 IOC 框架 【ViewInject】 (上) -- Hongyang

Android 进阶 教你打造 Android 中的 IOC 框架 【ViewInject】 (下) -- Hongyang

如何实现自定义Java运行时注解功能 -- r17171709

代理模式及Java实现动态代理 -- xiazdong

java的动态代理机制详解 -- xiaoluo501395377

You can’t perform that action at this time.