都說依賴注入,我就從實現(xiàn)的角度來一發(fā),以android作為引子..

用過諸多的view注入的框架,例如xutils,butterknife,KJLibraray,Guice等,你了解過如何實現(xiàn)嗎?
從零來一發(fā), 今天老司機為新來者帶帶路~其他老司機略過
從demo上,我只實現(xiàn)兩個功能@InjectView,@OnClick。前者注入view,后者注入點擊事件。其他的實現(xiàn)角度上是一樣的,大家可以一起探討一下!


還是老套路,先分析:
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, //加在參數(shù)上的注解,可以是方法內(nèi)的參數(shù)
    
    CONSTRUCTOR, //加在構(gòu)造函數(shù)上的注解
    
    LOCAL_VARIABLE, //加在方法內(nèi)變量上的注解
    
    ANNOTATION_TYPE,
 //加在注解上的注解
    PACKAGE
        //加在包名上的注解
    }

@Retention中:

public enum RetentionPolicy {
    
    SOURCE, //只保留在源碼中
    
    CLASS, //保留在類字節(jié)碼中
    
    RUNTIME //保留在運行時(我們自定義注解一般都是在運行時檢測的)
}

ok,繼續(xù),
@InjectView中只接收一個int類型的值,用于表示view的id,
@OnClick中接收一個int[],表示可以接收多個view的id,綁定到同一個click執(zhí)行方法上


既然有了注解,就少不了注解的解釋者,
先分析一個view賦值的過程:

1. 首先要有一個rootView,用于findView
2. 還需要有目標(biāo)view的id,這個就是@InjectView中的值
3. 需要目標(biāo)對象
4. 把從rootView找到的view賦值給目標(biāo)對象的目標(biāo)變量

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);
        }
    }
}

怎么還實現(xiàn)了一個接口呢?

public interface ProcessorIntf<T extends AnnotatedElement> {
    public boolean accept(AnnotatedElement t);

    public void process(Object object, View view, T t);
}

這個泛型接口接收一個AnnotatedElement的子類,它是什么呢?
在java中,F(xiàn)ield,Method,Constructor...一切可注解的對象都實現(xiàn)了AnnotatedElement接口。ProcessorIntf用于給解析器提供一系列通用行為:

/*
 * 每個不同的處理器都會通過這個方法來告訴最終的調(diào)度者,這個注解是否由我來
 * 處理 
 */
public boolean accept(AnnotatedElement t); 

process方法換個角度一想,無論是@InjectView,@InjectString,@OnClick等等任何注入的操作,是不是應(yīng)該都需要這幾個條件呢?所以:

/*這樣看來,可以把處理行為抽象成這幾個參數(shù)?
 *第一個object是目標(biāo)對象,
 *第二個view是根view
 *第三個是加上注解的那個東西
 */
public void process(Object object, View view, T e);

所以在InjectViewProcessor中是這樣實現(xiàn)的:

@Override
public boolean accept(AnnotatedElement e) {
//如果當(dāng)前這個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.為防止目標(biāo)屬性是private的,將可訪問設(shè)置為true,并賦值
-.ok,這樣一個view就被注入到目標(biāo)屬性中了

接著再分析@OnClick的實現(xiàn):

1.我需要知道要綁定點擊事件的view的id,這個在@OnClick中的value指定
2.和之前的一樣,也需要一個根view來拿到具體的view
3.要注入的對象,這個是必須的,
4.要把某個方法綁定為點擊事件的回調(diào),我還要知道是哪個方法
5.ok上述的條件都滿足后,就可以拿到find來的view并設(shè)置setOnClickListener,在收到回調(diào)的時候,去調(diào)用指定方法,來實現(xiàn)間接的綁定

好了,分析就到這里面,看代碼:

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個或一個參數(shù),且只接收View", m.getName()));
            } else {
                hasParam = true;
            }
        }

        @Override
        public void onClick(View v) {
            //點擊事件觸發(fā)了
            Object o = obj.get();
            if (o != null) {
                try {
                    if (hasParam) {
                        method.invoke(o, v);
                    } else {
                        method.invoke(o);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

再來分析代碼:

//這個很簡單,就是告訴管理器我響應(yīng)OnClick注解
 @Override
    public boolean accept(AnnotatedElement e) {
        return e.isAnnotationPresent(OnClick.class);
    }
/*
* 這個也還是一樣,
* 1.先拿到具體的注解對象 ,并拿到里面的值
* 2.因為存在多個id綁定到一個方法上的情況,所以一個循環(huán)不可少
* 3.就是拿到view,設(shè)置監(jiān)聽事件
* 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是一個中間件,注冊給系統(tǒng),系統(tǒng)在得到點擊事件后,通知給InvokeOnClickListener,在這個里面再調(diào)用你所指定的方法。

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個或一個參數(shù),且只接收View", m.getName()));
            } else {
                hasParam = true;
            }
        }

        @Override
        public void onClick(View v) {
            //點擊事件觸發(fā)了
            Object o = obj.get();
            if (o != null) {
                try {
                    if (hasParam) {
                        method.invoke(o, v);
                    } else {
                        method.invoke(o);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

ok,構(gòu)造函數(shù)中,拿到了要回調(diào)的方法,和目標(biāo)對象。這是必須的。
然后就是幾個簡單的判斷 :
1.先拿到方法的參數(shù),看看有沒有參數(shù) , 沒有就紀(jì)錄下hasParam為false,
2.有參數(shù)的話,判斷是幾個參數(shù),超過兩個了直接就報錯吧,那多的參數(shù)我從哪里給呢。
3.ok很聽話的只接收一個View,hasParam為true


        @Override
        public void onClick(View v) {
            //點擊事件觸發(fā)了
            Object o = obj.get(); //為什么要用一個WeakReference,其實沒有必要,因為activity消亡了,view也就消亡了,這個循環(huán)引用似乎不存在,但是我還是寫下,假如有假如呢。
            if (o != null) {
                try {
                    if (hasParam) { //有參數(shù),就把view傳過去
                        method.invoke(o, v);
                    } else { //沒有參數(shù)就直接調(diào)
                        method.invoke(o);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

ok,InjectView和OnClick到這里就已經(jīng)完工了, 但是直接拿來用,卻是有點不方便的,于是加一個管理者:

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);
            }
    }
}

管理者很簡單,提供了兩個靜態(tài)方法,一個給activity用, 一個可以給fragment,viewholder等任何對象用。其實最終用的也是同一個方法。
這里我用了一個處理器鏈的方式,假如后面我還要實現(xiàn)注入@string/xx,@color/xxx , @Service private WindowManager wm;等等等,把實現(xiàn)好的處理器加入到chain鏈中即可。

//這個就是前面已經(jīng)說過的,把每個遍歷到的方法或者屬性,甚至是構(gòu)造方法,類等等通過處理器鏈來詢問這個注解你accept嗎?接受則交給它來處理,
private static void doChain(Object obj,AccessibleObject ao, View rootView) {
            for (ProcessorIntf p : chain) {
                if(p.accept(ao)) p.process(obj,rootView,ao);
            }
    }
}

好了,長篇大論結(jié)束,最后貼下最終的調(diào)用:

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,如果我要實現(xiàn)一個注入contentView怎么辦呢?


留給讀者。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容