前言
應(yīng)用在逐步迭代中,代碼體積越來越大?,F(xiàn)有的工程代碼雖然已經(jīng)按照模塊分門別類存放,但是其引用文件還是很隨意,代碼間的耦合性高,使得代碼“牽一發(fā)而動(dòng)全身”,使開發(fā)人員對(duì)源代碼的維護(hù)帶來極大的不便。因此,為了今后更好的增刪和重用代碼,需要將各個(gè)業(yè)務(wù)邏輯拆分,實(shí)現(xiàn)組件化。
demo地址:demo地址
組件化簡(jiǎn)介
組件化就是將各個(gè)業(yè)務(wù)邏輯代碼或者功能代碼拆分出來,形成獨(dú)立的一部分。組件化的最初目的是代碼重用,比如一個(gè)彈窗提示功能會(huì)在很多項(xiàng)目很多頁(yè)面調(diào)用,它的樣式相對(duì)固定,使用頻率高,因此可以提取出來作為一個(gè)組件,其他項(xiàng)目只要引用該組件就能在自己的頁(yè)面中調(diào)用,這樣可以減少大量的重復(fù)代碼。其特點(diǎn)是高內(nèi)聚,即脫離掉主工程代碼也能編譯通過,低耦合,從主工程中剝離簡(jiǎn)單容易。
組件化方案
基本概念
組件化應(yīng)當(dāng)是可以完全剝離出主程序,并可以獨(dú)立編譯通過并且運(yùn)行,但是實(shí)際項(xiàng)目中,特別是沒有組件化架構(gòu)的項(xiàng)目想要中途將業(yè)務(wù)邏輯拆分出來還是有一定困難。因此在方案上我進(jìn)行了調(diào)整,允許初期組件化各個(gè)組件對(duì)主程序有依賴,如下圖所示:

總結(jié)下方案設(shè)計(jì)大致有如下要求:
- 各個(gè)組件可以脫離主程序獨(dú)立成塊并編譯,但允許存在少量依賴。
- 主程序脫離各個(gè)組件可以獨(dú)立運(yùn)行,對(duì)組件沒有依賴。
組件化實(shí)現(xiàn)
采用靜態(tài)庫(kù)脫離主程序
網(wǎng)絡(luò)上有很多組件化的方法,經(jīng)過多次嘗試,發(fā)現(xiàn)都有一定弊端,而且適合新項(xiàng)目,對(duì)已有項(xiàng)目不友好。最后,我采取靜態(tài)庫(kù)加工程組的方式進(jìn)行組件化。
所謂工程組,就是將多個(gè)項(xiàng)目工程關(guān)聯(lián)到一起,由.xcworkspace文件管理。常用的cocoaPods就是用這種形式。

建多個(gè)項(xiàng)目文件的好處是可以將組建的代碼和主程序代碼完全剝離開來,做到高內(nèi)聚的特點(diǎn)。
下面我將演示如何創(chuàng)建一個(gè)靜態(tài)庫(kù)工程。
1、在文件夾中創(chuàng)建新項(xiàng)目

2、創(chuàng)建項(xiàng)目的時(shí)候選取項(xiàng)目組

3、靜態(tài)庫(kù)項(xiàng)目中將編譯格式切換成static Library

4、創(chuàng)建資源文件target




5、將靜態(tài)包引入主工程,并設(shè)置先后編譯順序


按照以上方法配置,一個(gè)組件化就完成,后續(xù)可以在NMModelA.xcodeproj中編寫組件代碼。NMModelAResource targets主要是為了存放一些資源文件,比如圖片資源、xib編譯后的nib文件。每次改動(dòng)圖片或者xib都需要把新新生成的資源文件替換掉主程序中的資源文件 過程如下圖所示:

每次添加新資源文件或者新建xib記得從NMModelA target刪除,添加到 NMModelAResource中,否則實(shí)際包會(huì)生成2份或多份圖片,造成包大小變大


使用靜態(tài)庫(kù)加工程組的方式的好處:
- 高內(nèi)聚,低耦合
由于工程的限制,主程序的代碼無法直接引用各個(gè)組件的代碼,組件中的代碼也無法直接引用主程序,真正實(shí)現(xiàn)了組件化的概念。
- 編譯調(diào)試方便
在驗(yàn)證組件化功能的時(shí)候可以直接編譯主程序,在組件工程里下斷點(diǎn),就像編譯一個(gè)程序一樣,簡(jiǎn)單方便。
- 靜態(tài)庫(kù)只需要.h不需要.m就能編譯
前面我們說到,一些沒有組件化架構(gòu)的項(xiàng)目想要完全去除依賴非常困難,因此組件化程序往往需要引用主程序的頭文件。使用靜態(tài)庫(kù)可以很好解決該問題,只要在靜態(tài)庫(kù)中添加需要引用的.h文件,而不用添加.m文件就能編譯通過,并打成靜態(tài)包,可以作為臨時(shí)過渡期的方法。該方案同樣適用于解決庫(kù)沖突問題。
- 圖片資源、本地化文件與主程序共用
將圖片資源、本地化文件添加到主程序中,可以實(shí)現(xiàn)本地化、圖片資源共用。
- 完成組件代碼測(cè)試后可以打成.framework包引入工程
靜態(tài)庫(kù)文件不會(huì)暴露源代碼,因此可以大大減少編程人員誤修改組件代碼,導(dǎo)致出錯(cuò)的問題。
組件與主程序之間的交互
既然組件代碼與主程序不能直接交互,那么怎么做到一些常用操作,比如界面跳轉(zhuǎn)呢?
答案是RunTime。object使用runTime可以很好解決耦合問題,甚至做到不變動(dòng)代碼,刪除組件,主程序成功編譯并且不崩潰的神奇效果。以下是總體設(shè)計(jì)思想:

主程序和組件代碼只能通過各自的接口文件獲取數(shù)據(jù)
接口程序間通過runtime調(diào)用方法
由于runtime performSelector方法的參數(shù)個(gè)數(shù)限制和回調(diào)類型限制,這里規(guī)定接口最終回調(diào)類型必須是對(duì)象,參數(shù)傳id,多個(gè)參數(shù)用NSDictionary封裝傳遞
這里我創(chuàng)建了一個(gè)接口基本類HSLinkClass,所有的接口都是繼承自該類,主要提供了對(duì)象方法和類方法的調(diào)用。
.h文件
/**
回調(diào)block
@param retunValue 該方法return 的值
@param performSuccess 該方法是否執(zhí)行成功
@param classIsLoad 該類是否加載
*/
typedef void (^plugInCallBackblock)(id retunValue, BOOL performSuccess,BOOL classIsLoad);
@interface HSLinkClass : NSObject
/**
調(diào)用對(duì)象方法
@param className 類名
@param selectStr 方法名
@param params 參數(shù)字典
@param block 回調(diào)函數(shù)
*/
- (void)hsObjectMethodClassName:(NSString *)className performSelectorStr:(NSString *)selectStr param:(id)params block:(plugInCallBackblock)block;
/**
調(diào)用類方法
@param className 類名
@param selectStr 方法名
@param params 參數(shù)字典
@param block 回調(diào)函數(shù)
*/
- (void)hsClassMethodClassName:(NSString *)className performSelectorStr:(NSString *)selectStr param:(id)params block:(plugInCallBackblock)block;
/**
初始方法
*/
+ (instancetype)shareObject;
@end
.m文件
@implementation HSLinkClass
/**
初始方法
*/
+ (instancetype)shareObject{
return [[self alloc] init];
}
/**
調(diào)用對(duì)象方法
@param className 類名
@param selectStr 方法名
@param params 參數(shù)字典
@param block 回調(diào)函數(shù)
*/
- (void)hsObjectMethodClassName:(NSString *)className performSelectorStr:(NSString *)selectStr param:(id)params block:(plugInCallBackblock)block{
Class classObj = NSClassFromString(className);
if (classObj == nil) {
if (block) {
block(nil,NO,NO);
}
}
id object = [[classObj alloc] init];
if ([object respondsToSelector:NSSelectorFromString(selectStr)]) {
id returnValue = nil;
if (params != nil) {
returnValue = [object performSelector:NSSelectorFromString(selectStr) withObject:params];
if (block) {
block(returnValue,YES,YES);
}
}
else {
returnValue = [object performSelector:NSSelectorFromString(selectStr)];
if (block) {
block(returnValue,YES,YES);
}
}
}
else {
if (block) {
block(nil,NO,YES);
}
}
}
/**
調(diào)用類方法
@param className 類名
@param selectStr 方法名
@param params 參數(shù)字典
@param block 回調(diào)函數(shù)
*/
- (void)hsClassMethodClassName:(NSString *)className performSelectorStr:(NSString *)selectStr param:(NSDictionary *)params block:(plugInCallBackblock)block{
Class classObj = NSClassFromString(className);
if (classObj == nil) {
if (block) {
block(nil,NO,NO);
}
}
if ([classObj respondsToSelector:NSSelectorFromString(selectStr)]) {
id returnValue = nil;
if (params != nil) {
returnValue = [classObj performSelector:NSSelectorFromString(selectStr) withObject:params];
if (block) {
block(returnValue,YES,YES);
}
}
else {
returnValue = [classObj performSelector:NSSelectorFromString(selectStr)];
if (block) {
block(returnValue,YES,YES);
}
}
}
else {
if (block) {
block(nil,NO,YES);
}
}
}
@end
通過Runtime可以判斷該模塊是否加載,在基本類中做了多種情況保護(hù)防止其崩潰。
主程序和組件程序都需要新建一個(gè)HSLinkClass的子類作為接口,也稱之為中間層,分別提供索取方法和供應(yīng)方法。組件的索取方法就是主程序的供應(yīng)方法,反之也一樣。
下面是主程序跳轉(zhuǎn)到視頻扥類頁(yè)面的例子:
主程序接口代碼:

組件接口代碼:

主程序代碼:

建議以及優(yōu)化
每次xib變動(dòng)或者添加圖片都得手動(dòng)復(fù)制到主程序中去十分麻煩,這里可以學(xué)習(xí)cocoapods的做法,寫一個(gè)腳本放到Build Phases中,可以減少不少操作
接口基類目前只是實(shí)現(xiàn)了基本方法用runtime調(diào)用,還有很大的提升空間,比如增加屬性判斷組件是否加載
總結(jié)
組件化的確會(huì)增加一定工作量,原來#import一個(gè)文件就能解決的事,現(xiàn)在需要多好幾步操作。不過,也正是因?yàn)橹暗碾S意,導(dǎo)致后續(xù)代碼功能變更,牽一發(fā)而動(dòng)全身,痛苦萬分。吃苦在前,享樂在后才是正確的工作態(tài)度。