都說依賴注入,我就從實現的角度來一發,以android作為引子..
用過諸多的view注入的框架,例如xutils,butterknife,KJLibraray,Guice等,你瞭解過如何實現嗎?
從零來一發, 今天老司機為新來者帶帶路~其他老司機略過
從demo上,我只實現兩個功能@InjectView,@OnClick。前者注入view,後者注入點選事件。其他的實現角度上是一樣的,大家可以一起探討一下!
還是老套路,先分析:
1.我需要兩個註解類@InjectView和@OnClick
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectView {
public @IdRes int value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
public @IdRes int[] value();
}
簡單提一下:
@Target的ElementType中
public enum ElementType {
TYPE, //加在類上的註解
FIELD, //加在類中屬性上的註解
METHOD, //加在類中方法上的註解
PARAMETER, //加在引數上的註解,可以是方法內的引數
CONSTRUCTOR, //加在建構函式上的註解
LOCAL_VARIABLE, //加在方法內變數上的註解
ANNOTATION_TYPE,
//加在註解上的註解
PACKAGE
//加在包名上的註解
}
@Retention中:
public enum RetentionPolicy {
SOURCE, //只保留在原始碼中
CLASS, //保留在類位元組碼中
RUNTIME //保留在執行時(我們自定義註解一般都是在執行時檢測的)
}
ok,繼續,
@InjectView中只接收一個int型別的值,用於表示view的id,
@OnClick中接收一個int[],表示可以接收多個view的id,繫結到同一個click執行方法上
既然有了註解,就少不了註解的解釋者,
先分析一個view賦值的過程:
1. 首先要有一個rootView,用於findView
2. 還需要有目標view的id,這個就是@InjectView中的值
3. 需要目標物件
4. 把從rootView找到的view賦值給目標物件的目標變數
ok,看程式碼
public class InjectViewProcessor implements ProcessorIntf<Field>{
@Override
public boolean accept(AnnotatedElement e) {
return e.isAnnotationPresent(InjectView.class);
}
@Override
public void process(Object object, View view, Field field) {
InjectView iv = field.getAnnotation(InjectView.class);
final int viewId = iv.value();
final View v = view.findViewById(viewId);
field.setAccessible(true);
try {
field.set(object, v);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
怎麼還實現了一個介面呢?
public interface ProcessorIntf<T extends AnnotatedElement> {
public boolean accept(AnnotatedElement t);
public void process(Object object, View view, T t);
}
這個泛型介面接收一個AnnotatedElement的子類,它是什麼呢?
在java中,Field,Method,Constructor…一切可註解的物件都實現了AnnotatedElement介面。ProcessorIntf用於給解析器提供一系列通用行為:
/*
* 每個不同的處理器都會通過這個方法來告訴最終的排程者,這個註解是否由我來
* 處理
*/
public boolean accept(AnnotatedElement t);
process方法換個角度一想,無論是@InjectView,@InjectString,@OnClick等等任何注入的操作,是不是應該都需要這幾個條件呢?所以:
/*這樣看來,可以把處理行為抽象成這幾個引數?
*第一個object是目標物件,
*第二個view是根view
*第三個是加上註解的那個東西
*/
public void process(Object object, View view, T e);
所以在InjectViewProcessor中是這樣實現的:
@Override
public boolean accept(AnnotatedElement e) {
//如果當前這個AnnotatedElement例項加有InjectView註解,則返回true
return e.isAnnotationPresent(InjectView.class);
}
如果是返回true,說明這個它可以處理,則走到
@Override
public void process(Object object, View view, Field field) {
InjectView iv = field.getAnnotation(InjectView.class);
final int viewId = iv.value();
final View v = view.findViewById(viewId);
field.setAccessible(true);
try {
field.set(object, v);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
程式碼很簡單,就簡單說明下:
1.先拿到具體的註解類,並拿到裡面的值比如R.id.txt等
2.從跟view中findViewById拿到指定的view
3.為防止目標屬性是private的,將可訪問設定為true,並賦值
-.ok,這樣一個view就被注入到目標屬性中了
接著再分析@OnClick的實現:
1.我需要知道要繫結點選事件的view的id,這個在@OnClick中的value指定
2.和之前的一樣,也需要一個根view來拿到具體的view
3.要注入的物件,這個是必須的,
4.要把某個方法繫結為點選事件的回撥,我還要知道是哪個方法
5.ok上述的條件都滿足後,就可以拿到find來的view並設定setOnClickListener,在收到回撥的時候,去呼叫指定方法,來實現間接的繫結
好了,分析就到這裡面,看程式碼:
public class OnClickProcessor implements ProcessorIntf<Method> {
@Override
public boolean accept(AnnotatedElement e) {
return e.isAnnotationPresent(OnClick.class);
}
@Override
public void process(Object object,View view, Method method) {
final OnClick oc = method.getAnnotation(OnClick.class);
final int[] value = oc.value();
for (int id : value) {
view.findViewById(id).setOnClickListener(new InvokeOnClickListener(method,object));
}
}
private static class InvokeOnClickListener implements View.OnClickListener {
public Method method;
public WeakReference<Object> obj;
private boolean hasParam;
InvokeOnClickListener(Method m, Object object) {
this.method = m;
this.obj = new WeakReference<Object>(object);
final Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes == null || parameterTypes.length == 0) {
hasParam = false;
} else if (parameterTypes.length > 1 || !View.class.isAssignableFrom(parameterTypes[0])) {
throw new IllegalArgumentException(String.format("%s方法只能擁有0個或一個引數,且只接收View", m.getName()));
} else {
hasParam = true;
}
}
@Override
public void onClick(View v) {
//點選事件觸發了
Object o = obj.get();
if (o != null) {
try {
if (hasParam) {
method.invoke(o, v);
} else {
method.invoke(o);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
再來分析程式碼:
//這個很簡單,就是告訴管理器我響應OnClick註解
@Override
public boolean accept(AnnotatedElement e) {
return e.isAnnotationPresent(OnClick.class);
}
/*
* 這個也還是一樣,
* 1.先拿到具體的註解物件 ,並拿到裡面的值
* 2.因為存在多個id繫結到一個方法上的情況,所以一個迴圈不可少
* 3.就是拿到view,設定監聽事件
* 4.但是,這個InvokeOnClickListener是個什麼東西呢?
*/
@Override
public void process(Object object,View view, Method method) {
final OnClick oc = method.getAnnotation(OnClick.class);
final int[] value = oc.value();
for (int id : value) {
view.findViewById(id).setOnClickListener(new InvokeOnClickListener(method,object));
}
}
//先說下,這裡面的InvokeOnClickListener是一箇中間件,註冊給系統,系統在得到點選事件後,通知給InvokeOnClickListener,在這個裡面再呼叫你所指定的方法。
private static class InvokeOnClickListener implements View.OnClickListener {
public Method method;
public WeakReference<Object> obj;
private boolean hasParam;
InvokeOnClickListener(Method m, Object object) {
this.method = m;
this.obj = new WeakReference<Object>(object);
final Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes == null || parameterTypes.length == 0) {
hasParam = false;
} else if (parameterTypes.length > 1 || !View.class.isAssignableFrom(parameterTypes[0])) {
throw new IllegalArgumentException(String.format("%s方法只能擁有0個或一個引數,且只接收View", m.getName()));
} else {
hasParam = true;
}
}
@Override
public void onClick(View v) {
//點選事件觸發了
Object o = obj.get();
if (o != null) {
try {
if (hasParam) {
method.invoke(o, v);
} else {
method.invoke(o);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
ok,建構函式中,拿到了要回調的方法,和目標物件。這是必須的。
然後就是幾個簡單的判斷 :
1.先拿到方法的引數,看看有沒有引數 , 沒有就紀錄下hasParam為false,
2.有引數的話,判斷是幾個引數,超過兩個了直接就報錯吧,那多的引數我從哪裡給呢。
3.ok很聽話的只接收一個View,hasParam為true
@Override
public void onClick(View v) {
//點選事件觸發了
Object o = obj.get(); //為什麼要用一個WeakReference,其實沒有必要,因為activity消亡了,view也就消亡了,這個迴圈引用似乎不存在,但是我還是寫下,假如有假如呢。
if (o != null) {
try {
if (hasParam) { //有引數,就把view傳過去
method.invoke(o, v);
} else { //沒有引數就直接調
method.invoke(o);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
ok,InjectView和OnClick到這裡就已經完工了, 但是直接拿來用,卻是有點不方便的,於是加一個管理者:
public class Injector {
private static List<? extends ProcessorIntf<? extends AccessibleObject>> chain = Arrays.asList(new InjectViewProcessor(), new OnClickProcessor());
public static void inject(Activity act) {
inject(act,act.getWindow().getDecorView());
}
public static void inject(Object obj, View rootView) {
final Class<?> aClass = obj.getClass();
final Field[] declaredFields = aClass.getDeclaredFields();
for (Field f : declaredFields) {
doChain(obj,f,rootView);
}
final Method[] declaredMethods = aClass.getDeclaredMethods();
for (Method m : declaredMethods) {
doChain(obj, m, rootView);
}
}
private static void doChain(Object obj,AccessibleObject ao, View rootView) {
for (ProcessorIntf p : chain) {
if(p.accept(ao)) p.process(obj,rootView,ao);
}
}
}
管理者很簡單,提供了兩個靜態方法,一個給activity用, 一個可以給fragment,viewholder等任何物件用。其實最終用的也是同一個方法。
這裡我用了一個處理器鏈的方式,假如後面我還要實現注入@string/xx,@color/xxx , @Service private WindowManager wm;等等等,把實現好的處理器加入到chain鏈中即可。
//這個就是前面已經說過的,把每個遍歷到的方法或者屬性,甚至是構造方法,類等等通過處理器鏈來詢問這個註解你accept嗎?接受則交給它來處理,
private static void doChain(Object obj,AccessibleObject ao, View rootView) {
for (ProcessorIntf p : chain) {
if(p.accept(ao)) p.process(obj,rootView,ao);
}
}
}
好了,長篇大論結束,最後貼下最終的呼叫:
public class TestActivity extends AppCompatActivity {
@InjectView(R.id.txt)
private TextView txt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_activity);
Injector.inject(this);
}
@OnClick({R.id.btnTestOne,R.id.btnTestTwo})
public void btnTestOne(Button view) {
final int id = view.getId();
if (id == R.id.btnTestOne) {
txt.setText("按鈕一被點選");
}else{
txt.setText("按鈕二被點選");
}
}
}
ok,如果我要實現一個注入contentView怎麼辦呢?
留給讀者。