iOS 的組件化開(kāi)發(fā)

在一個(gè)APP開(kāi)發(fā)過(guò)程中豪嗽,如果項(xiàng)目較小且團(tuán)隊(duì)人數(shù)較少,使用最基本的MVC豌骏、MVVM開(kāi)發(fā)就已經(jīng)足夠了龟梦,因?yàn)榫S護(hù)成本比較低。

但是當(dāng)一個(gè)項(xiàng)目開(kāi)發(fā)團(tuán)隊(duì)人數(shù)較多時(shí)窃躲,因?yàn)槊總€(gè)人都會(huì)負(fù)責(zé)相應(yīng)組件的開(kāi)發(fā)计贰,常規(guī)開(kāi)發(fā)模式耦合會(huì)越來(lái)越嚴(yán)重,而且導(dǎo)致大量代碼沖突蒂窒,會(huì)使后期維護(hù)和升級(jí)過(guò)程中代碼“牽一發(fā)而動(dòng)全身”躁倒,額外帶來(lái)很大的工作量荞怒,并且會(huì)導(dǎo)致一些潛在的BUG。

在這時(shí)秧秉,組件化開(kāi)發(fā)就派上很大用場(chǎng)了褐桌,所謂的組件化開(kāi)發(fā),就是把APP根據(jù)業(yè)務(wù)拆分為各獨(dú)立的組件象迎,各個(gè)組件相互寫(xiě)作荧嵌,組成完整的APP。

一砾淌、各組件的引入

關(guān)于組件的拆分啦撮,就根據(jù)具體項(xiàng)目進(jìn)行拆分,假如APP被拆分了AModule拇舀、BModule逻族、CModule,那么骄崩,應(yīng)該如何引入這些組件呢聘鳞?你可能會(huì)想到APP的入口AppDelegate。在平時(shí)開(kāi)發(fā)中要拂,AppDelegate中往往初始化了好多組件抠璃,比如推送、統(tǒng)計(jì)等組件脱惰,這樣就會(huì)導(dǎo)致AppDelegate的臃腫搏嗡。

所以,我們可以增加一個(gè)ModuleManager拉一,專門(mén)用來(lái)初始化各組件采盒。
首先增加一個(gè) ModuleProtocol

#import <Foundation/Foundation.h>
@import UIKit;
@import UserNotifications;

@protocol ModuleProtocol <UIApplicationDelegate, UNUserNotificationCenterDelegate>

@end

我們?cè)?code>ModuleManager中hook住UIApplicationDelegateUNUserNotificationCenterDelegate中的方法,使相應(yīng)的組件中實(shí)現(xiàn)了對(duì)應(yīng)方法蔚润,在相應(yīng)時(shí)機(jī)就會(huì)調(diào)用組建里的對(duì)應(yīng)方法:

#import "ModuleManager.h"
#import "AppDelegate.h"
#import <objc/runtime.h>

#define ALL_MODULE [[ModuleManager sharedInstance] allModules]
#define SWIZZLE_METHOD(m) swizzleMethod(class, @selector(m),@selector(module_##m));

@interface ModuleManager ()

@property (nonatomic, strong) NSMutableArray<id<ModuleProtocol>> *modules;

@end

@implementation ModuleManager

+ (instancetype)sharedInstance { ...... }

- (NSMutableArray<id<ModuleProtocol>> *)modules { ...... }

- (void)addModule:(id<ModuleProtocol>) module { ...... }

- (void)loadModulesWithPlistFile:(NSString *)plistFile { ...... }

- (NSArray<id<ModuleProtocol>> *)allModules { ...... }

@end

@implementation AppDelegate (Module)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SWIZZLE_METHOD(application:willFinishLaunchingWithOptions:);
        SWIZZLE_METHOD(application:didFinishLaunchingWithOptions:);
        ......
    });
}

static inline void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) { ...... }


- (BOOL)module_application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    BOOL result = [self module_application:application willFinishLaunchingWithOptions:launchOptions];
    for (id<ModuleProtocol> module in ALL_MODULE) {
        if ([module respondsToSelector:_cmd]) {
            [module application:application willFinishLaunchingWithOptions:launchOptions];
        }
    }
    return result;
}

- (BOOL)module_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    BOOL result = [self module_application:application didFinishLaunchingWithOptions:launchOptions];
    for (id<ModuleProtocol> module in ALL_MODULE) {
        if ([module respondsToSelector:_cmd]) {
            [module application:application didFinishLaunchingWithOptions:launchOptions];
        }
    }
    return result;
}
......

@end

ModuleManager.h:

#import <Foundation/Foundation.h>
#import "ModuleProtocol.h"

@interface ModuleManager : NSObject

+ (instancetype)sharedInstance;

- (void)loadModulesWithPlistFile:(NSString *)plistFile;

- (NSArray<id<ModuleProtocol>> *)allModules;

@end

之后我們通過(guò)一個(gè) ModulesRegister.plist文件管理需要引入的組件:

ModulesRegister.plist

如上圖磅氨,假如我們要引入AModule、BModule嫡纠、CModule烦租,那么這三個(gè)Module只需要實(shí)現(xiàn)協(xié)議ModuleProtocol,然后實(shí)現(xiàn)AppDelegate中對(duì)應(yīng)的方法除盏,在對(duì)應(yīng)方法中初始化自身即可:
AModule.h:

#import <Foundation/Foundation.h>
#import "ModuleProtocol.h"

@interface AModule : NSObject<ModuleProtocol>

@end

AModule.m:

#import "AModule.h"

@implementation AModule

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    //初始化AModule
    return YES;
}

@end

之后在AppDelegateload方法中通過(guò)ModulesRegister.plist引入各組件即可:

@implementation AppDelegate

+ (void)load {
    //load modules
    NSString* plistPath = [[NSBundle mainBundle] pathForResource:@"ModulesRegister" ofType:@"plist"];
    [[ModuleManager sharedInstance] loadModulesWithPlistFile:plistPath];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    ......
}

@end

這樣叉橱,各組件的開(kāi)發(fā)者在自己的組件中初始化自己,其他人需要使用時(shí)只需要加入ModulesRegister.plist文件中即可者蠕。

二窃祝、組件間協(xié)作

簡(jiǎn)單來(lái)看,假設(shè)APP的每個(gè)頁(yè)面就是一個(gè)組件踱侣,假如我們的APP有AViewController锌杀、BViewController甩栈、CViewController、DViewController糕再、EViewController,各ViewController必然設(shè)置各種相互跳轉(zhuǎn)玉转。那么突想,我們APP的跳轉(zhuǎn)邏輯可能是下面這個(gè)樣子:


跳轉(zhuǎn)邏輯

為了解決這種復(fù)雜的耦合關(guān)系,我們可以增加一個(gè)Router中間層去管理各ViewController之間的跳轉(zhuǎn)關(guān)系(也就是實(shí)際開(kāi)發(fā)中組件間相互調(diào)用的關(guān)系)究抓。

所以猾担,根據(jù)需要,我開(kāi)發(fā)并開(kāi)源了一個(gè)支持URL Rewrite的iOS路由庫(kù)— FFRouter刺下,通過(guò)FFRouter去管理各ViewController之間的跳轉(zhuǎn)關(guān)系:

FFRouter

這樣绑嘹,各ViewController之間的跳轉(zhuǎn)關(guān)系就變的清晰了許多。

FFRouter通過(guò)提前注冊(cè)對(duì)應(yīng)的URL橘茉,之后就直接通過(guò)打開(kāi)URL去控制各ViewController之間的跳轉(zhuǎn)(或各組件間的調(diào)用)工腋。
FFRouter支持組件間傳遞非常規(guī)對(duì)象,如UIImage等畅卓,并支持獲取組件返回值擅腰。
基本使用如下:

/**
 注冊(cè) url

 @param routeURL 要注冊(cè)的 URL
 @param handlerBlock URL 被 Route 后的回調(diào)
 */
+ (void)registerRouteURL:(NSString *)routeURL handler:(FFRouterHandler)handlerBlock;

/**
 注冊(cè) URL,通過(guò)該方式注冊(cè)的 URL 被 Route 后可返回一個(gè) Object

 @param routeURL 要注冊(cè)的 URL
 @param handlerBlock URL 被 Route 后的回調(diào),可在回調(diào)中返回一個(gè) Object
 */
+ (void)registerObjectRouteURL:(NSString *)routeURL handler:(FFObjectRouterHandler)handlerBlock;



/**
 判斷 URL 是否可被 Route(是否已經(jīng)注冊(cè))

 @param URL 要判斷的 URL
 @return 是否可被 Route
 */
+ (BOOL)canRouteURL:(NSString *)URL;



/**
 Route 一個(gè) URL

 @param URL 要 Router 的 URL
 */
+ (void)routeURL:(NSString *)URL;

/**
 Route 一個(gè) URL,并帶上額外參數(shù)

 @param URL 要 Router 的 URL
 @param parameters 額外參數(shù)
 */
+ (void)routeURL:(NSString *)URL withParameters:(NSDictionary<NSString *, id> *)parameters;

/**
 Route 一個(gè) URL翁潘,可獲得返回的 Object

 @param URL 要 Router 的 URL
 @return 返回的 Object
 */
+ (id)routeObjectURL:(NSString *)URL;

/**
 Route 一個(gè) URL趁冈,并帶上額外參數(shù),可獲得返回的 Object

 @param URL 要 Router 的 URL
 @param parameters 額外參數(shù)
 @return 返回的 Object
 */
+ (id)routeObjectURL:(NSString *)URL withParameters:(NSDictionary<NSString *, id> *)parameters;



/**
 Route 一個(gè)未注冊(cè) URL 時(shí)回調(diào)

 @param handler 回調(diào)
 */
+ (void)routeUnregisterURLHandler:(FFRouterUnregisterURLHandler)handler;



/**
 取消注冊(cè)某個(gè) URL

 @param URL 要被取消注冊(cè)的 URL
 */
+ (void)unregisterRouteURL:(NSString *)URL;

/**
 取消注冊(cè)所有 URL
 */
+ (void)unregisterAllRoutes;


/**
 是否顯示 Log拜马,用于調(diào)試

 @param enable YES or NO渗勘,默認(rèn)為 NO
 */
+ (void)setLogEnabled:(BOOL)enable;

而且參考天貓的方案增加了URL Rewrite功能:

可以使用正則添加一條 Rewrite 規(guī)則,例如:
要實(shí)現(xiàn)打開(kāi) URL:https://www.taobao.com/search/原子彈時(shí)俩莽,將其攔截旺坠,改用本地已注冊(cè)的 URL:protocol://page/routerDetails?product=原子彈打開(kāi)。
首先添加一條 Rewrite 規(guī)則:

[FFRouterRewrite addRewriteMatchRule:@"(?:https://)?www.taobao.com/search/(.*)" targetRule:@"protocol://page/routerDetails?product=$1"];

之后在打開(kāi)URL:https://www.taobao.com/search/原子彈時(shí)豹绪,將會(huì) Rewrite 到URL:protocol://page/routerDetails?product=原子彈价淌。

[FFRouter routeURL:@"https://www.taobao.com/search/原子彈"];


可以通過(guò)以下方法同時(shí)增加多個(gè)規(guī)則:

+ (void)addRewriteRules:(NSArray<NSDictionary *> *)rules;

其中 rules 格式必須為以下格式:

@[@{@"matchRule":@"YourMatchRule1",@"targetRule":@"YourTargetRule1"},
  @{@"matchRule":@"YourMatchRule2",@"targetRule":@"YourTargetRule2"},
  @{@"matchRule":@"YourMatchRule3",@"targetRule":@"YourTargetRule3"},]


Rewrite 規(guī)則中的保留字:

  • 通過(guò) $scheme$host瞒津、$port蝉衣、$path$query巷蚪、$fragment 獲取標(biāo)準(zhǔn) URL 中的相應(yīng)部分病毡。通過(guò)$url獲取完整 URL
  • 通過(guò) $1$2屁柏、$3...獲取matchRule的正則中使用圓括號(hào)取出的參數(shù)
  • $:原變量的值啦膜、$$:原變量URL Encode后的值有送、$#:原變量URL Decode后的值


例如:
https://www.taobao.com/search/原子彈對(duì)于Rewrite 規(guī)則(?:https://)?www.taobao.com/search/(.*)

$1=原子彈
$$1=%e5%8e%9f%e5%ad%90%e5%bc%b9

同樣,https://www.taobao.com/search/%e5%8e%9f%e5%ad%90%e5%bc%b9對(duì)于Rewrite 規(guī)則(?:https://)?www.taobao.com/search/(.*)

$1=%e5%8e%9f%e5%ad%90%e5%bc%b9
$#1=原子彈

考慮到經(jīng)常用路由配置UIViewController之間的跳轉(zhuǎn)僧家,所以增加了額外的工具FFRouterNavigation來(lái)更方便地控制UIViewController之間的跳轉(zhuǎn)雀摘。

三、其他組件化方案

目前這種組件化方案參考了蘑菇街八拱、天貓阵赠、京東的的實(shí)現(xiàn)方案。除這種方案外肌稻,Casa(查看文章)之前提出了解耦程度更高的方案清蚀,這種方案組件仍然使用中間件通信,但中間件通過(guò) runtime 接口解耦爹谭,然后使用 target-action 簡(jiǎn)化寫(xiě)法枷邪,通過(guò) category 分離組件接口代碼。
但是诺凡,這種方案雖然解耦程度更高东揣,但是也增加了組件化的成本,綜合考慮绑洛,直接使用中間件通信的方式更好一點(diǎn)救斑。具體哪種方案好,也就仁者見(jiàn)仁真屯、智者見(jiàn)智了~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末脸候,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子绑蔫,更是在濱河造成了極大的恐慌运沦,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件配深,死亡現(xiàn)場(chǎng)離奇詭異携添,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)篓叶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)烈掠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人缸托,你說(shuō)我怎么就攤上這事左敌。” “怎么了俐镐?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵矫限,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)叼风,這世上最難降的妖魔是什么取董? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮无宿,結(jié)果婚禮上茵汰,老公的妹妹穿的比我還像新娘。我一直安慰自己懈贺,他們只是感情好经窖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著梭灿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪冰悠。 梳的紋絲不亂的頭發(fā)上堡妒,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音溉卓,去河邊找鬼皮迟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛桑寨,可吹牛的內(nèi)容都是我干的伏尼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼尉尾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼爆阶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起沙咏,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辨图,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后肢藐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體故河,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年吆豹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鱼的。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痘煤,死狀恐怖凑阶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情速勇,我是刑警寧澤晌砾,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站烦磁,受9級(jí)特大地震影響养匈,放射性物質(zhì)發(fā)生泄漏哼勇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一呕乎、第九天 我趴在偏房一處隱蔽的房頂上張望积担。 院中可真熱鬧,春花似錦猬仁、人聲如沸帝璧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)的烁。三九已至,卻和暖如春诈闺,著一層夾襖步出監(jiān)牢的瞬間渴庆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工雅镊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留襟雷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓仁烹,卻偏偏與公主長(zhǎng)得像耸弄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子卓缰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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