练习手写IoC框架,深入理解反射、注解、动态代理

Posted by Jarrod on October 26, 2019

练习手写IoC框架,深入理解反射、注解、动态代理

实现功能

​ 通过依赖注入方式引入Activity布局、控件、点击事件,类似功能有个著名的框架ButterKnife 。不过他是通过APT (Annotation Processing Tool) 技术实现的。了解过Java反射的都知道它是个耗性能的操作,但是它带来的好处也是显而易见的,比如现在我们通过反射来注入布局等,可以消除很多findViewById的模板代码,如果界面View较多的情况下,大量的findViewById显得并不优雅。而APT技术不消耗性能,但是会在buid中生成很多补充代码,使我们的包体积变大。当前使用反射技术实现注入的目的,是为了更好的理解Java反射、自定义注解、动态代理等知识点。


常用API

方法 说明
  obj.getClass
Class.forName(“类路径”)
Object.class
获取类的Class对象
class getMethods 获取当前类及父类的公共方法
  getDeclaredMethods 获取当前类的所有方法
  getFeilds 获取所有public属性
  getDeclaredFeilds 获取所有属性
method getAnnotations 获取注解
  invoke() 调用method,一个参数是方法所在的类实例,一个是方法参数
feild setAccessible(true) 获取访问权限(针对受保护的属性如private)


引入布局

Activity中常规引入布局如下:

public class MainActivity extends AppCompactActivity { 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

注解方式引入:

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

为了实现它,我们首先需要自定义ContentView注解,代码很简单

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {   
    //对应括号里R.layout.xxx
    int value();
}

主要是理解@Target和@Retention这两个注解, 他们用来修饰注解,是注解的注解,称为元注解。

@Target表示ContentView注解作用的位置,比如类、方法、属性、注解类型等;

@Retention表示ContentView注解的运行周期,

source:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;被编译器忽略

class:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期

runtime:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在

因为我们需要在App运行的时候动态获取注解信息注入布局,所以选择Runtime周期

现在需要一个InjectManager容器去解析ContentView注解,然后给Activity类注入layout布局,

public class InjectManager {   
	//......
    private static void injectLayout(Activity activity) {      
    	//获取类
    	Class<? extends Activity> clazz = activity.getClass();      
    	//获取ContentView注解
    	ContentView contentView = clazz.getAnnotation(ContentView.class); 
        //有不用注解注入布局的activity
    	if (contentView != null) {      
        	//这里的方法可以拿到R.layout.xxx
    		int layoutId = contentView.value();         
            try {            
            	//反射获取activity的setContentView方法,并调用
                Method method = clazz.getMethod("setContentView", int.class);         
                method.invoke(activity, layoutId);         
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {            
                e.printStackTrace();         
            }      
    	}   
	}
    //......
}

这样就实现了布局注入的功能,是不是没那么复杂,主要还是对注解和反射的使用。需要注意的是我们要在Activity的onCreate方法中调用InjectManager.injectLayout(this),一般我们可以把它放在BaseActivity中,这点和ButterKnife相似。

</br>

控件的注入

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {   
	@InjectView(R.id.tv_sub)   
	private TextView tvSubject;
	//......
}

和上面一样我们来定义injectView注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {   
	//对应的R.id.XXX   
	int value();
}

然后在InjectManager里面实现功能。控件的注入一定是在布局注入之后完成的,还有和布局注入不同的是它有个赋值过程。这个下面继续,先看下代码:

private static void injectViews(Activity activity) {   
    Class<? extends Activity> clazz = activity.getClass();   
    //获取当前类的所有属性,和getFields不同 getFields获得某个类的所有的公共(public)的字段,包括父类中的公共字段
    Field[] fields = clazz.getDeclaredFields();   
    for (Field field : fields) {  
        //获取属性的InjectView注解
        InjectView bindView = field.getAnnotation(InjectView.class);      
        //不管没有InjectView注解的属性
        if (bindView == null) continue;      
        try {         
            //获取Activity的findViewById方法,因为该方法在父类中,所以用getMethod才行
            Method findViewById = clazz.getMethod("findViewById", int.class); 
            //调用findViewById
            Object view = findViewById.invoke(activity, bindView.value());
            //!!!拿到控件之后我们还需要给tvSubject赋值,但是它是个private属性,所以需要用setAccessible打开访问权限再赋值
            field.setAccessible(true);         
            field.set(activity, view);      
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {         
            e.printStackTrace();      
        }   
    }
}

这样控件的注入就完成了。和布局的注入类似,但是关键点就在于要给我们自己在Activity中定义的属性赋值,以及怎么给私有的属性赋值问题,在注释中已经写的很清楚了。

</br>

注入事件

注入事件相对来说比较复杂,我们先看下一般我们怎么写事件代码的:

tvSubject.setOnClickListener(
    new View.OnClickListener() {   
        @Override   
        public void onClick(View v) {   
            //do your self
        }
    }
);

我们给控件写点击事件,其实传入的是一个接口,然后执行回调方法,和上面两种很不同。目标事件注入方式:

@OnClick(R.id.tv_sub)
public void onTvSubClick(View view) {   
    Toast.makeText(this, ((TextView) view).getText(), Toast.LENGTH_SHORT).show();
}

我们怎么来实现动态执行回调接口,把它变成我们定义的onTvSubClick呢?这里就需要:动态代理。例如:

SayHello sayHello = (SayHello)Proxy.newProxyInstance(SayHello.class.getClassLoader(),
				new Class[]{SayHello.class},
				new InvocationHandler() {
					@Override
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						Toast.makeText(getBaseContext(), (proxy instanceof SayHello) + "", Toast.LENGTH_LONG).show();
						return null;
					}
				});
sayHello.say();

我们先实现自定义OnClick注解,因为考虑到后面还有OnLongClick的注入,所以我们在定义@OnClick和@OnLongClick的时候需要区分类型,这里就需要用到上面说的注解的注解,注解类型它作用于注解之上,我们定义一个EventBase

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {

	//监听方法
	String listenerSetter();

	//监听的类型
	Class listenerType();

	//回调方法
	String callbackListener();
}

然后定义Onclick

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

这样就能通过EventBase注解来定义事件的名称、类型、回调方法,那再通过反射就很容易实现了。View.OnClickListener是View中定义的接口,我们通过java动态代理这个接口能够在运行时执行我们想要的操作method,

private static void injectEvents(Activity activity) {
		Class<? extends Activity> clazz = activity.getClass();
		//获取该activity中所有方法
		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 eventBase = annotationType.getAnnotation(EventBase.class);
					if (eventBase != null) {
						//获取EventBase注解的 监听setter方法 监听类型 回调方法
						String listenerSetter = eventBase.listenerSetter();
						Class listenerType = eventBase.listenerType();
						String callbackListener = eventBase.callbackListener();
						try {
							OnClick click = (OnClick) annotation;
							int[] values = click.value();
                            assert values != null;
							//获取方法注解的value方法 拿到控件id数组
							InjectEventProxyHandler handler = new InjectEventProxyHandler(activity);
                            //给原始回调 替换成 method
							handler.add(callbackListener, method);
							//通过动态代理EventBase要代理的接口
							Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, handler);
							for (int value : values) {
                                //获取view
								View view = activity.findViewById(value);
                                //因为监听方法在View这个系统类中
								Method setXXX = View.class.getMethod(listenerSetter, listenerType);
								setXXX.invoke(view, listener);
							}
						} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
							e.printStackTrace();
						}
					}
				}
			}
		}
	}

下面就是实现InvocationHandler的invoke方法

class InjectEventProxyHandler implements InvocationHandler {   
    //防连点时间间隔   
    private static final int QUICK_EVENT_TIME_SPAN = 500;   
    // 目标对象,这里是Activity,因为我们的onTvSubClick定义在Activity中 
    private Object targetObject;   
    private long lastClickTime;   
    //替换的方法对应 
    private ArrayMap<String, Method> map = new ArrayMap<>();   
    
    InjectEventProxyHandler(Object targetObject) {      
        this.targetObject = targetObject;   
    }   
    
    //关联的这个实现类的方法被调用时将被执行   
    /*InvocationHandler接口的方法,proxy表示代理,method表示原对象被调用的方法,args表示方法的参数*/   
    @Override   
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {      
        if (targetObject != null) {     
            //获取被代理接口的方法名
            String name = method.getName();         
            //阻塞事件 1S n次点击         
            long timeSpan = System.currentTimeMillis() - lastClickTime;         
            if (timeSpan < QUICK_EVENT_TIME_SPAN) {            
                return null;         
            }         
            lastClickTime = System.currentTimeMillis();   
            //获取对应替换的方法
            method = map.get(name);         
            assert method != null;      
            //是否有参数
            if (method.getGenericParameterTypes().length == 0) {            
                return method.invoke(targetObject);         
            } else {            
                return method.invoke(targetObject, args);        
            }      
        }     
        return null;  
    }   
    /*
    将各种callback 方法替换成我们自己定义的方法
    */
    public void add(String callback, Method method) {      
        map.put(callback, method);   
    }
}

虽然贴了很多代码,但在自己的实践过程中对反射,注解和动态代理有了更好的理解。这里代码的地址: https://github.com/jarrod-chen/IoCApplication