Android IOC注入框架

什么是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的代碼:

IOC注冊(cè)

可以看到相比于傳統(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即可:

注解的創(chuàng)建

注解的編寫(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一樣這里就不在贅述了:

InjectView

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然走。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市戏挡,隨后出現(xiàn)的幾起案子芍瑞,更是在濱河造成了極大的恐慌,老刑警劉巖褐墅,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拆檬,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡妥凳,警方通過(guò)查閱死者的電腦和手機(jī)竟贯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)逝钥,“玉大人屑那,你說(shuō)我怎么就攤上這事∷铱睿” “怎么了齐莲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)磷箕。 經(jīng)常有香客問(wèn)我选酗,道長(zhǎng),這世上最難降的妖魔是什么岳枷? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任芒填,我火速辦了婚禮,結(jié)果婚禮上空繁,老公的妹妹穿的比我還像新娘殿衰。我一直安慰自己,他們只是感情好盛泡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布闷祥。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凯砍。 梳的紋絲不亂的頭發(fā)上箱硕,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音悟衩,去河邊找鬼剧罩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛座泳,可吹牛的內(nèi)容都是我干的惠昔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼挑势,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼镇防!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起潮饱,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤来氧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后饼齿,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體饲漾,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年缕溉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了考传。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡证鸥,死狀恐怖僚楞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情枉层,我是刑警寧澤泉褐,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站鸟蜡,受9級(jí)特大地震影響膜赃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜揉忘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一跳座、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泣矛,春花似錦疲眷、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春几颜,著一層夾襖步出監(jiān)牢的瞬間倍试,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工菠剩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留易猫,地道東北人耻煤。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓具壮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親哈蝇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子棺妓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容