iOS控制反轉(zhuǎn)(IoC)與依賴注入(DI)的實(shí)現(xiàn)

背景

最近接觸了一段時(shí)間的SpringMVC吼驶,對(duì)其控制反轉(zhuǎn)(IoC)和依賴注入(DI)印象深刻污抬,此后便一直在思考如何使用OC語言較好的實(shí)現(xiàn)這兩個(gè)功能检痰。Java語言自帶的注解特性為IoC和DI帶來了極大的方便敦第,要在OC上較好的實(shí)現(xiàn)這兩個(gè)功能,需要一些小小的技巧理卑。

控制反轉(zhuǎn)和依賴注入

控制反轉(zhuǎn)

簡(jiǎn)單來說,將一個(gè)類對(duì)象的創(chuàng)建由手動(dòng)new方式改為從IOC容器內(nèi)獲取蔽氨,就是一種控制反轉(zhuǎn)藐唠,例如我們現(xiàn)在要?jiǎng)?chuàng)建一個(gè)ClassA類,則常規(guī)方法為

ClassA *a = [ClassA new];

如果使用控制反轉(zhuǎn)孵滞,則從容器內(nèi)獲取ClassA對(duì)象中捆,對(duì)象由容器負(fù)責(zé)創(chuàng)建。

ApplicationContext *context = [ApplicationContext sharedContext];
ClassA *a = [context getInstanceByClassName:@"ClassA"];

依賴注入

所謂依賴注入坊饶,是指一個(gè)類對(duì)象不應(yīng)負(fù)責(zé)去查找其依賴屬性泄伪,而是交由容器去處理。例如ClassA類有一個(gè)ClassB類對(duì)象匿级,如下所示蟋滴。

@interface ClassA : NSObject

@property(nonatomic, strong) ClassB *b;

@end

常規(guī)情況下,要將ClassB的對(duì)象創(chuàng)建后傳遞給ClassA痘绎,如下所示津函。

ClassA *a = [ClassA new];
ClassB *b = [ClassB new];
a.b = b;

如果交由容器處理,則容器會(huì)自動(dòng)創(chuàng)建ClassA孤页、ClassB尔苦,并且根據(jù)ClassA的依賴屬性類型ClassB自動(dòng)的將ClassB的實(shí)例注入到ClassA對(duì)象中。

優(yōu)點(diǎn)

使用控制反轉(zhuǎn)和依賴注入將對(duì)象的創(chuàng)建與依賴對(duì)象的注入交由IoC容器來完成行施,這樣做不僅能夠降低代碼的耦合度允坚,更可以減少代碼量。

類與屬性的修飾

這兩個(gè)功能都是在運(yùn)行時(shí)通過反射來實(shí)現(xiàn)的蛾号,具體的容器技術(shù)細(xì)節(jié)在下文討論稠项,這里主要討論的是如何修飾交由容器創(chuàng)建的對(duì)象以及依賴注入的屬性。

Java中的注解

在Java中鲜结,那些要交由Spring容器創(chuàng)建的類通過注解來修飾展运,例如JavaWeb中的Controller和Service分別由@Controller和@Service修飾活逆,如下所示。

// 控制器
@Controller
public class SomeController {...}
// 服務(wù)
@Service
public class SomeService {...}

而控制器要通過服務(wù)來處理業(yè)務(wù)邏輯拗胜,因此Controller依賴了Service蔗候,通過@Autowired修飾這一屬性,即可完成自動(dòng)注入挤土,如下所示琴庵。

@Controller
public class AdministratorController {
    @Autowired
    private SomeService serv;
...
}

通過@Autowired注解,IoC容器會(huì)根據(jù)屬性類型(SomeService)找到依賴對(duì)象的實(shí)例仰美,并且注入到Controller的serv屬性迷殿。這一過程中,Controller咖杂、Service以及注入均不需要寫額外的代碼庆寺,只需要通過注解修飾即可。

OC中的實(shí)現(xiàn)

由于OC沒有注解特性诉字,因此要標(biāo)記類和屬性就需要其他的方法懦尝,我經(jīng)過思考發(fā)現(xiàn)了一種較好的方式,那就是通過協(xié)議來標(biāo)記類壤圃,通過額外的屬性來標(biāo)記屬性陵霉。

  • 為了模仿@Controller、@Service等修飾類的注解伍绳,我們使用IOCComponents協(xié)議來標(biāo)記類踊挠,來表示這些類交由容器處理。
@interface SGService : NSObject <IOCComponents>
...
@end
  • 為了模仿@Autowired實(shí)現(xiàn)的按類型注入冲杀,我們?cè)谝⑷氲膶傩郧岸嗉右粭l屬性效床,該條屬性的類型為TypeAnnotation,我們稱之為注解屬性权谁,這個(gè)類通過@Class聲明而并不存在剩檀,只是為了標(biāo)記,在反射時(shí)得到的所有屬性都是按照順序排列的旺芽,因此每一條注解屬性后的屬性都是需要進(jìn)行依賴注入的沪猴,示例如下。
@interface SGService : NSObject <IOCComponents>

@property (nonatomic, weak, readonly) TypeAnnotation *autowired_0;
@property (nonatomic, strong) SGMapper *mapper;

@end

通過上文這種方式采章,在反射時(shí)autowired_0和mapper是相鄰的运嗜,因此可以判斷出mapper需要注入,只需要反射出該property的信息共缕,并且從容器中查找,并注入即可士复,為了方便定義這樣的注解屬性图谷,我們使用一個(gè)宏如下所示翩活。

#define Autowired(num) @property (nonatomic, weak, readonly) TypeAnnotation *autowired_##num;

那么上面的代碼可以進(jìn)行如下的簡(jiǎn)化。

@interface SGService : NSObject <IOCComponents>

Autowired(0)
@property (nonatomic, strong) SGMapper *mapper;

@end

這里的數(shù)字是為了多個(gè)注解造成屬性的重復(fù)定義便贵,可以從0開始編號(hào)菠镇。

OC的一個(gè)實(shí)現(xiàn)示例

這里假設(shè)IoC容器已經(jīng)完成,主要演示流程承璃,容器的技術(shù)細(xì)節(jié)在下文討論利耍。
有一個(gè)Service類和一個(gè)Mapper類,Service依賴了Mapper盔粹,具體代碼如下隘梨。

類的定義與修飾

  • IOCComponents用于標(biāo)識(shí)該類由容器負(fù)責(zé)創(chuàng)建
  • Autowired(x)表示該屬性按類型進(jìn)行依賴注入
@interface SGService : NSObject <IOCComponents>

Autowired(0)
@property (nonatomic, strong) SGMapper *mapper;

@end
@interface SGMapper : NSObject <IOCComponents>

@end

掃描配置

類似于Spring的applicationContext.xml,這里通過ApplicationContext.plist來配置要掃描的類的特征舷嗡,目前提供了前綴和類列表轴猎,對(duì)于有公共前綴的類可以配置一個(gè)前綴來實(shí)現(xiàn)掃描,對(duì)于沒有前綴的類进萄,單獨(dú)配置到類列表中捻脖,如下圖所示。

ApplicationContext.plist

驗(yàn)證與使用

經(jīng)過上面的配置中鼠,IoC容器便可完成Service與Mapper的創(chuàng)建可婶,以及將Mapper注入到Service,驗(yàn)證如下援雇。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    SGApplicationContext *context = [SGApplicationContext sharedContext];
    SGService *serv = [context getInstanceByClassName:@"SGService"];
    printf("%s -> %s\n",serv.description.UTF8String, serv.mapper.description.UTF8String);
    return YES;
}

打印如下

<SGService: 0x7ff34bc749f0> -> <SGMapper: 0x7ff34bc74920>

IoC容器的實(shí)現(xiàn)

IoC容器的核心功能是對(duì)象創(chuàng)建矛渴、依賴查找和依賴注入,這些功能都需要借助運(yùn)行時(shí)的反射實(shí)現(xiàn)熊杨,下面將按照容器初始化的過程來逐一介紹用到的OC運(yùn)行時(shí)函數(shù)曙旭,這些函數(shù)均可以在包含objc/runtime.h后使用。

1.IoC容器

容器通過SGApplicationContext單例來管理需要容器創(chuàng)建的類對(duì)象晶府,通過一個(gè)Dictionary類型的屬性instanceMap存儲(chǔ)對(duì)象是否已經(jīng)創(chuàng)建過桂躏,容器中的對(duì)象目前還只是單例的,邏輯很簡(jiǎn)單川陆,每次根據(jù)類型取出對(duì)象時(shí)剂习,先檢查instanceMap該對(duì)象是否已經(jīng)創(chuàng)建過,如果創(chuàng)建過則直接返回较沪,否則創(chuàng)建后返回鳞绕,這里會(huì)通過運(yùn)行時(shí)函數(shù)objc_getClass檢查該類是否已經(jīng)裝載,具體實(shí)現(xiàn)如下尸曼。

- (id)getInstanceByClassName:(NSString *)className {
    if (self.instanceMap[className] == nil) {
        Class clazz = NSClassFromString(className);
        // 檢查類是否已經(jīng)裝載们何,防止未定義的類實(shí)例化時(shí)出錯(cuò)
        if (!objc_getClass(className.UTF8String)) {
            return nil;
        }
        id instance = [clazz new];
        self.instanceMap[className] = instance;
    }
    return self.instanceMap[className];
}

2.掃描類列表

通過函數(shù)objc_getClassList可以獲取類列表,該函數(shù)的具體信息如下控轿。

int objc_getClassList(Class *buffer, int bufferCount);

buffer是所有類的數(shù)組冤竹,bufferCount為數(shù)組大小拂封,返回值也為數(shù)組大小,由于第一次調(diào)用時(shí)并不知道bufferCount鹦蠕,因此可以兩次調(diào)用冒签,第一次拿到bufferCount,第二次再初始化類列表數(shù)組钟病,具體代碼如下萧恕,詳細(xì)請(qǐng)見注釋。

- (void)scanClasses {
    int classCount = objc_getClassList(NULL, 0);
    Class *classList = (Class *)malloc(classCount * sizeof(Class));
    classCount = objc_getClassList(classList, classCount);
    // 用于存放需要IoC容器處理的類的OC數(shù)組
    NSMutableArray *temp = @[].mutableCopy;
    // 獲得IOCComponents協(xié)議肠阱,用于判斷標(biāo)記
    Protocol *protocol = objc_getProtocol("IOCComponents");
    for (int i = 0; i < classCount; i++) {
        Class clazz = classList[i];
        NSString *className = NSStringFromClass(clazz);
        // 第一個(gè)判斷條件對(duì)應(yīng)于ApplicationContext.plist中的掃描類特征配置票唆,只有符合條件的類才能被添加
        // 第二個(gè)條件檢查IOCComponents標(biāo)記,有標(biāo)記的類才被IoC容器處理
        if ([self isValidIOCClassNamed:className] && [clazz conformsToProtocol:protocol]) {
            [temp addObject:className];
        }
    }
    // 將IoC需要處理的類存儲(chǔ)起來
    self.DIClasses = temp;
    // 由于類列表是malloc創(chuàng)建的辖所,需要手動(dòng)釋放
    free(classList);
    // 根據(jù)注解屬性處理依賴注入
    [self scanAnnotation];
}

3.掃描類屬性與屬性注入

要掃描一個(gè)類的所有私有和公有屬性惰说,使用下面的函數(shù)。

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);

該方法可以列出所有屬性缘回,先私有后公有吆视,按照定義的順序從上到下列出,其返回值是一個(gè)objc_property_t結(jié)構(gòu)體數(shù)組酥宴,從objc_property_t結(jié)構(gòu)體中可以得到屬性的各種信息啦吧,對(duì)于名稱可以通過property_getName函數(shù)直接獲得,而其他信息需要先通過property_getAttributes方法獲得描述屬性的字符串拙寡,再進(jìn)行分割授滓。通過這樣的步驟,即可獲得所有注解屬性的位置肆糕,并由此得到所有需要注入的屬性般堆,根據(jù)這些屬性的類型進(jìn)行注入即可。
要完成注入诚啃,需要先得到代表屬性的Ivar淮摔,然后對(duì)實(shí)例進(jìn)行注入,得到Ivar和注入屬性的函數(shù)如下始赎。

// 注意得到Ivar時(shí)的屬性名為實(shí)例變量名而不是property名和橙,例如@property定義的xx,則這里應(yīng)該寫作_xx
Ivar class_getInstanceVariable(Class cls, const char *name);
// 將value注入到obj實(shí)例的 ivar實(shí)例變量上
void object_setIvar(id obj, Ivar ivar, id value);

具體代碼如下造垛,詳細(xì)請(qǐng)看注釋魔招。

- (void)scanAnnotation {
    // 對(duì)scanClasses中得到的需要IoC容器處理的類進(jìn)行遍歷
    for (NSUInteger i = 0; i < self.DIClasses.count; i++) {
        NSString *className = self.DIClasses[i];
        Class class = NSClassFromString(className);
        unsigned int outCount;
        // 反射出所有屬性
        objc_property_t *props = class_copyPropertyList(class, &outCount);
        // 保存所有注解屬性,注解屬性包含了位置索引(index)五辽、名稱(name)和類型(type)办斑,通過一個(gè)模型類SGAnnotation來存儲(chǔ)
        NSMutableArray *annotations = @[].mutableCopy;
        // 保存所有的屬性信息,每個(gè)屬性包含了名稱(name)和類型(type)杆逗,通過一個(gè)模型類SGProperty來存儲(chǔ)
        NSMutableArray *properties = @[].mutableCopy;
        // 遍歷所有屬性
        for (NSUInteger i = 0; i < outCount; i++) {
            objc_property_t prop = props[i];
            NSString *propName = [[NSString alloc] initWithCString:property_getName(prop) encoding:NSUTF8StringEncoding];
            // 這一段代碼用于從描述屬性的字符串中獲取到類型乡翅,用到了正則和字串處理
            NSString *propAttrs = [[NSString alloc] initWithCString:property_getAttributes(prop) encoding:NSUTF8StringEncoding];
            NSRange range = [propAttrs rangeOfString:@"@\".*\"" options:NSRegularExpressionSearch];
            if (range.location != NSNotFound) {
                range.location += 2;
                range.length -= 3;
                NSString *typeName = [propAttrs substringWithRange:range];
                // 如果當(dāng)前屬性為注解屬性吁讨,則記錄進(jìn)annotaions
                if ([typeName isEqualToString:@"TypeAnnotation"]) {
                    SGAnnotation *anno = [SGAnnotation new];
                    anno.index = i;
                    anno.name = propName;
                    anno.type = typeName;
                    [annotations addObject:anno];
                }
                // 記錄每一條屬性
                SGProperty *sp = [SGProperty new];
                sp.name = propName;
                sp.type = typeName;
                [properties addObject:sp];
            }
        } // scan class properties end
        // 從容器中得到類的實(shí)例
        id diInstance = [self getInstanceByClassName:className];
        // 遍歷注解,得到所有被修飾的屬性
        for (NSUInteger i = 0; i < annotions.count; i++){
            SGAnnotation *annotation = annotations[i];
            SGProperty *prop = properties[annotation.index + 1];
            NSString *typeName = prop.type;
            NSString *varName = prop.name;
            // 得到依賴對(duì)象峦朗,并注入到注解修飾的屬性
            id destInstance = [self getInstanceByClassName:typeName];
            Ivar ivar = class_getInstanceVariable([diInstance class], [NSString stringWithFormat:@"_%@",varName].UTF8String);
            object_setIvar(diInstance, ivar, destInstance);
        }
    } // scan classes end
}

總結(jié)

通過這樣的處理,完成了OC語言對(duì)IoC和DI的基本實(shí)現(xiàn)排龄,這只是一次探索波势,還有很多地方需要優(yōu)化和完善。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末橄维,一起剝皮案震驚了整個(gè)濱河市尺铣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌争舞,老刑警劉巖凛忿,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異竞川,居然都是意外死亡店溢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門委乌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來床牧,“玉大人,你說我怎么就攤上這事遭贸「昕龋” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵壕吹,是天一觀的道長著蛙。 經(jīng)常有香客問我,道長耳贬,這世上最難降的妖魔是什么踏堡? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮效拭,結(jié)果婚禮上暂吉,老公的妹妹穿的比我還像新娘。我一直安慰自己缎患,他們只是感情好慕的,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著挤渔,像睡著了一般肮街。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上判导,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天嫉父,我揣著相機(jī)與錄音沛硅,去河邊找鬼。 笑死绕辖,一個(gè)胖子當(dāng)著我的面吹牛摇肌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播仪际,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼围小,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了树碱?” 一聲冷哼從身側(cè)響起肯适,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎成榜,沒想到半個(gè)月后框舔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赎婚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年刘绣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挣输。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡额港,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出歧焦,到底是詐尸還是另有隱情移斩,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布绢馍,位于F島的核電站向瓷,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏舰涌。R本人自食惡果不足惜猖任,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瓷耙。 院中可真熱鬧朱躺,春花似錦、人聲如沸搁痛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸡典。三九已至源请,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谁尸。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國打工舅踪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人良蛮。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓抽碌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親决瞳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咬展,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

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