什么是IOC注入框架
ButterKnife大家都應(yīng)該使用過(guò),對(duì)于view的注入減少了大量篇幅的findViewById操作日矫,而注解注入的方式也顯得更加優(yōu)雅。這里介紹一下我的IOC簡(jiǎn)單注入框架,項(xiàng)目地址移步這里
IOC使用簡(jiǎn)單介紹
添加依賴
項(xiàng)目根目錄下的build.gradle文件添加如下內(nèi)容
allprojects {
repositories {
...
maven { url "https://raw.githubusercontent.com/demoless/ioc/master/repo" }
}
}
然后在app模塊的build.gradle文件添加如下內(nèi)容
implementation 'com.demoless:ioc:1.0.0'
這里我貼出demo的調(diào)用示例代碼看看如何使用:
要實(shí)現(xiàn)這樣一套IOC框架我們還要先注冊(cè)一下挣柬,看BaseActiivty的代碼:
可以看到相比于傳統(tǒng)的Activity的寫(xiě)法惕虑,IOC注入框架頗具誘惑坟冲,下面我就帶大家了解一下我的IOC實(shí)現(xiàn)思路。
如何實(shí)現(xiàn)IOC
IOC是一套注解注入框架溃蔫,所以主要是通過(guò)Java的反射與注解來(lái)實(shí)現(xiàn)的樱衷,這里就不介紹了,不了解的可以看看這篇文章酒唉。
布局注入
@ContentView(R.layout.activity_main)
首先創(chuàng)建ContentView這個(gè)注解
創(chuàng)建過(guò)程跟創(chuàng)建類(lèi)的過(guò)程是一樣的矩桂,只需要將Kind選擇為Annotation即可:
注解的編寫(xiě)
package com.demo.iocinject.ioc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Create by Zhf on 2019/7/13
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
int value();
}
我們可以看到在這個(gè)注解的上面還有兩個(gè)注解,這是兩個(gè)元注解痪伦,首先@Target(ElementType.TYPE)代表這個(gè)ContentView注解作用在類(lèi)上面侄榴,然后@Retention(RetentionPolicy.RUNTIME)表示注解在運(yùn)行時(shí)執(zhí)行,因?yàn)橐粋€(gè)類(lèi)只會(huì)有一個(gè)布局文件网沾,所以這里value方法的返回值為int癞蚕,而不是數(shù)組。
Java實(shí)現(xiàn)注解的執(zhí)行邏輯
定義好了這個(gè)注解之后辉哥,我們就要考慮如何將ContentView里傳入的布局文件設(shè)置給Activity桦山,我們知道傳統(tǒng)的activity是通過(guò)在onCreate方法里的setContentView來(lái)將布局文件設(shè)置給activity的,那我們也只需要通過(guò)反射將傳入注解的布局再傳入setContentView并且讓它自動(dòng)執(zhí)行不就可以實(shí)現(xiàn)了嘛醋旦,思路好像沒(méi)錯(cuò)恒水,我們來(lái)實(shí)現(xiàn)以下:
//布局注入
private static void injectLayout(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//獲取類(lèi)之上的注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView != null){
//獲取注解的返回值
int layoutId = contentView.value();
//第一種方法
//activity.setContentView(layoutId);
//第二種方法
try {
Method setContentView = clazz.getMethod("setContentView", int.class);
setContentView.invoke(activity,layoutId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
這段代碼首先通過(guò)activity.getClass()拿到這個(gè)Class對(duì)象,再通過(guò)clazz.getAnnotation(ContentView.class)獲取類(lèi)上的注解饲齐,拿到注解之后自然要獲取他的返回值钉凌,所以再調(diào)用他的value方法,這樣我們就拿到了對(duì)應(yīng)的布局文件捂人,最好要完成的是傳入setContentView這個(gè)布局方法并執(zhí)行御雕。這里我給出了兩種方法矢沿,第一種很簡(jiǎn)單直接調(diào)用activity的setContentView方法,第二種通過(guò)反射拿到setContentView這個(gè)Method對(duì)象酸纲,在調(diào)用invoke方法自動(dòng)執(zhí)行捣鲸。這樣一個(gè)簡(jiǎn)易布局注入就實(shí)現(xiàn)了。
控件注入
@InjectView
注解文件
與ContentView一樣這里就不在贅述了:
Java執(zhí)行邏輯
//控件的注入
private static void injectViews(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//獲取類(lèi)的全部屬性
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//獲取屬性上的注解
InjectView injectView = field.getAnnotation(InjectView.class);
if (injectView != null){
//獲取注解的值
int viewId = injectView.value();
//View view = activity.findViewById(viewId);
try {
//獲取findViewById方法
Method findViewById = clazz.getMethod("findViewById", int.class);
//執(zhí)行findViewById方法
Object view = findViewById.invoke(activity, viewId);
//設(shè)置訪問(wèn)權(quán)限 private
field.setAccessible(true);
//為屬性賦值
field.set(activity,view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
這段代碼也跟布局注入的實(shí)現(xiàn)很相似闽坡,這是回去類(lèi)的全部屬性的時(shí)候摄狱,需要調(diào)用的是clazz.getDeclaredFields()方法,如果用getFields方法程序會(huì)崩潰无午,這個(gè)很容易想到媒役,因?yàn)樵诟割?lèi)和子類(lèi)中可能會(huì)有相同命名的一個(gè)控件,就行這樣:private Button mButton宪迟;酣衷,所以就造成崩潰,這只能獲取類(lèi)自身的屬性次泽;第二個(gè)需要注意的地方是穿仪,通常我們聲明一個(gè)控件,都是用的private關(guān)鍵字意荤,所以這里還需要設(shè)置一下訪問(wèn)權(quán)限啊片,調(diào)用 field.setAccessible(true),這樣對(duì)稀有屬性進(jìn)行操作;另一個(gè)與布局注入實(shí)現(xiàn)不同的是,setContentView方法沒(méi)有返回值玖像,而findViewById則相反紫谷,所以我們需要為屬性(這里就是一些View)賦值,調(diào)用的是field.set(activity捐寥,view)笤昨。
事件的注入
@InjectEvent
Android事件監(jiān)聽(tīng)規(guī)律
事件的注入相比之前的布局和控件注入,難度和復(fù)雜度大大提高了握恳。通過(guò)對(duì)Android中的事件監(jiān)聽(tīng)代碼的觀察瞒窒,我們得出如下三部曲:
- setListener
- new Listener
- doCallback
就像View的點(diǎn)擊事件和長(zhǎng)按時(shí)間監(jiān)聽(tīng)那樣,首先setListener:View.setOnClickListener()乡洼,然后new 一個(gè)Listener傳入崇裁,View.setOnClickListener(new OnClickListener(View v){}),最后執(zhí)行回調(diào)方法:
onClick(View v){...}
定義事件監(jiān)聽(tīng)規(guī)律的注解
@EventBase
通過(guò)上述規(guī)律總結(jié),我們要先定義這個(gè)注解:
/**
* Create by Zhf on 2019/7/13
**/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
//setListener
Class<?> listenerType();
//new View.OnxxxListener
String listenerSetter();
//回調(diào) 最終執(zhí)行方法
String callBackListener();
}
我們看到這個(gè)注解是放在注解類(lèi)之上的束昵,那么這個(gè)注解怎么使用呢拔稳,就以View的長(zhǎng)按事件監(jiān)聽(tīng)為例:
/**
* Create by Zhf on 2019/7/13
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter = "setOnLongClickListener",
listenerType = View.OnLongClickListener.class,
callBackListener = "onLongClick")
public @interface OnLongClick {
int[] value();
}
在這個(gè)注解上面調(diào)用了剛才定義的EventBase注解,根據(jù)傳入的值大家似乎就什么都看明白了吧妻怎,沒(méi)錯(cuò)這里傳入了View.OnLongClickListener事件監(jiān)聽(tīng)三部曲壳炎,因?yàn)樵谝粋€(gè)類(lèi)中可能不止一個(gè)控件會(huì)設(shè)置長(zhǎng)按事件監(jiān)聽(tīng),所以這里的返回值是數(shù)組逼侦。
事件注入的邏輯
//事件的注入
private static void injectEvents(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//獲取一個(gè)類(lèi)的所有方法
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 = annotationType.getAnnotation(EventBase.class);
if (eventBase != null) {
String listenerSetter = eventBase.listenerSetter();
Class<?> listenerType = eventBase.listenerType();
String callBackListener = eventBase.callBackListener();
try {
Method valueMethod = annotationType.getDeclaredMethod("value");
int[] viewIds = (int[]) valueMethod.invoke(annotation);
//設(shè)置private權(quán)限可見(jiàn)
method.setAccessible(true);
//AOP切面
ListenerInvocationHandler handler = new ListenerInvocationHandler(activity);
handler.addMethods(callBackListener, method);
//代理模式
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(),
new Class[]{listenerType}, handler);
for (int viewId : viewIds) {
View view = activity.findViewById(viewId);
Method setter = view.getClass().getMethod(listenerSetter, listenerType);
setter.invoke(view, listener);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
這里其他的不多介紹了匿辩,主要不同的就是這里使用了動(dòng)態(tài)代理和AOP切面技術(shù):
import android.util.Log;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
/**
* Create by Zhf on 2019/7/13
**/
public class ListenerInvocationHandler implements InvocationHandler {
private final static long QUICK_EVENT_TIME_SPAN = 300;
private long lastClickTime;
private Object target;//需要攔截的對(duì)象
private HashMap<String, Method> map = 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 methodName = method.getName();
method = map.get(methodName);
long timeSpan = System.currentTimeMillis() - lastClickTime;
if (timeSpan < QUICK_EVENT_TIME_SPAN){
Log.e("點(diǎn)擊阻塞,防止誤點(diǎn)", String.valueOf(timeSpan));
return null;
}
lastClickTime = System.currentTimeMillis();
if (method != null){
if (method.getGenericParameterTypes().length == 0) return method.invoke(target);
return method.invoke(target,args);
}
}
return null;
}
public void addMethods(String methodName, Method method){
map.put(methodName, method);
}
}
這個(gè)類(lèi)實(shí)現(xiàn)了InvocationHandler接口榛丢,可以實(shí)現(xiàn)點(diǎn)擊事件不傳參數(shù)以及點(diǎn)擊阻塞铲球,防誤點(diǎn),具體的邏輯比較簡(jiǎn)單晰赞,可以看看代碼以及注釋稼病。
站在巨人的肩膀上
該IOC實(shí)現(xiàn)參考網(wǎng)易云課堂,github地址本文開(kāi)篇已經(jīng)給出掖鱼,歡迎大家star與fork然走。