iOS元件化的分層思路
元件化是一種軟體設計方法,它將一個大型應用程式拆分成多個獨立且可重用的模組。這些模組可以分別開發、測試和維護,從而提高程式碼的複用性和可維護性。通常,元件化的分層思想大致分為以下三層:
基礎模組:封裝一些不與業務相關的模組,如工具類和分類。這一層的程式碼應當是完全獨立於專案,可以在其他專案中直接使用而無需修改。
通用模組:實際上是“通用業務模組”,既包含通用程式碼,也包含與業務邏輯相關的通用部分,如通用UIButton元件等。
業務模組:具體業務邏輯的實現模組,需要結合實際專案進行劃分。
模組的開發整合順序為從下至上,即從基礎模組到通用模組再到業務模組,而依賴順序則是從上到下。
業務模組的設計
業務模組的設計是元件化過程中最難的一部分,需要綜合考慮當前的合理性和未來的擴充套件性。在元件化之前,專案中的各個模組可能會有複雜的耦合關係。在進行元件化之後,需要透過建立“通訊中間層”來降低這些模組之間的耦合度。
CTMediator 通訊中間框架介紹
CTMediator是一個用於模組間通訊的中間層框架,透過runtime機制實現完全杜絕模組間的耦合。它的基本思路包括: • 每個模組隔離出一個獨立的Target層,這個層是模組的宣告檔案。該層提供了外部呼叫該模組功能的介面,並對傳入引數進行驗證和錯誤處理。 • 使用字串形式的類名和方法名,透過runtime機制在不匯入某模組的情況下呼叫其方法,從而避免模組間的硬性依賴。
增加分類 - 解決中間層與模組間的耦合
為防止CTMediator框架發生意外變化及避免專案或模組對框架的強耦合,可以透過增加分類(Category)進行拓展。其好處包括:
防止框架變化:透過建立分類進行拓展,不直接修改CTMediator原始碼,避免因框架升級導致的問題。
減少強耦合:為每個模組獨立出一個類,分類中的方法與Target層宣告的方法對應,方便外部呼叫。這使一個完整模組包括 分類 --> Target層 ---> 模組原始碼層。
框架使用示例
以present image操作為例,模組A暴露了一個imageView��於傳值。模組A的Target層實現了所需方法,如Action_nativePresentImage。最終,透過CTMediator框架的performSelector方法實現對目標方法的呼叫。 總結 • CTMediator透過runtime機制進行模組解耦。 • 增加分類進一步減少中間層與模組間的耦合。 • 模組化設計提高程式碼的可複用性和維護性。
基礎模組與通用模組的分層方式
這部分可依需求調整: • 基礎模組:封裝不與業務相關的模組,如分類、工具類等。理想狀態下,這一層不需修改即可在其他專案中使用。 • 通用模組:主要是通用業務模組,如公用元件、通用UIButton、瀑布流、時間計算NSDate等。此層應體現與業務掛鉤的通用邏輯;如果完全獨立於業務邏輯,則放入基礎模組。
業務模組
元件化除了技術層面,更難的是業務模組設計,需要考慮整個專案的劃分、當前分層的合理性及未來擴充套件性。頻繁改動的模組需要合理設計前端介面及與其他模組的互動。
通常專案模組間的關係大致如下圖,就是我們在進行元件化之前的專案,給個層次中間的關係,這裏寫的模組可以理解是類之間的關係。
各個模組都或多或少有關係,模組間進行通訊(即類之間的方法呼叫),需要進行#import匯入標頭檔案,是一種比較強的耦合關係。
模組化的元件間耦合問題
爲了實現模組間的低耦合,首先需要解決模組間的耦合關係。模組越獨立,系統的可維護性和靈活性就越高。基本思路是建立一個通訊中間層,各模組透過該中間層進行通訊,避免直接聯絡。
通訊中間層的問題
與中間層的強耦合:中間層需要匯入所有模組,才能允許模組間通訊。
中間層過於龐大:如果A模組只想與C模組通訊,中間層卻包含ABCD所有程式碼,這不合理。
排列組合問題:中間層設計為AB、AC、AD、BC、BD、CD等組合,會使設計複雜化。
CTMediator 通訊中間框架介紹 框架地址:github.com/casatwy/CTM… 程式碼結構圖:
框架使用前提:已經對專案劃分好合理的業務模組,單純是對專案的業務進行分層,不考模組之間的通訊。
基本思路
專案劃分:先對專案進行合理的業務模組劃分,不考慮模組間通訊。
獨立Target層:每個模組有一個獨立的Target層,作為模組宣告和入口,類似於.h檔案。
入口封裝:Target層封裝模組提供給外部的功能,如登入、註冊等,並進行業務判斷或容錯處理。
程式碼實現思路 • 杜絕耦合:透過runtime機制將方法呼叫轉換為字串形式,以類名和方法名(字串)進行呼叫,避免模組間直接匯入。 • 錯誤處理:框架內部對引數如target和action進行容錯處理,以減少手誤風險。
增加分類 - 減少中間層與模組間的耦合
設計目的
防止框架意外變化:建立分類進行拓展,不直接修改CTMediator原始碼,類似於使用AFN等三方框架時,透過獨立類管理和封裝。
減少強耦合:為每個模組獨立出一個類,分類中的方法與Target層宣告的方法一一對應。
實現方式 • 舉例:在登入模組中,將登入、註冊、忘記密碼等功能方法分別在Target層寫好宣告和實現,然後複製到分類。 • 外部呼叫:外部只需匯入這個分類即可使用相應功能,類似於常規分類的使用效果。 • 完整模組結構:一個完整的模組包括 分類 --> Target層 ---> 模組原始碼層。
CTMediator程式碼和demo介紹
下面我們針對在github給出的demo進行介紹
專案整體介紹 專案程式碼結構
程式碼執行起來,是一個tableView
我們以 present image 這個操作
這個功能是將 DemoModuleADetailViewController modal出來,並需要往這個控制器裡傳遞一個UIImage,DemoModuleADetailViewController 以下簡稱為模組A 模組A 宣告暴露了一個imageView,用於傳值
@interface DemoModuleADetailViewController : UIViewController @property (nonatomic, strong, readonly) UILabel *valueLabel; @property (nonatomic, strong, readonly) UIImageView *imageView; @end
模組A 的target 層 Target_A類對應的方法及其實現為如下,主要是建立模組A控制器,解析傳進來的引數解,並進行賦值控制器,並實現modal。
- (id)Action_nativePresentImage:(NSDictionary *)params { DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init]; viewController.valueLabel.text = @"this is image"; viewController.imageView.image = params[@"image"]; [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil]; return nil; }
方法名 Action_nativePresentImage根據是根據目前CTM的規則拼接出來,方法名的規則是Action_ 拼上方法名,nativePresentImage是我們可以定義的方法的名字,這名字在分類的層面進行宣告拼接,在demo是定義成了一個static string.我的理解起名和如何定義字串,只要專案內部約定好就行,大家都根據這規則就好。
第二步解決了方法名的問題,然後我們整體看下方法的呼叫,從cell的點選一步一步。 cell 點選,呼叫CTM分類方法
分類方法的實現 呼叫到CTM內部方法,進行類 和 方法名的轉換,建立物件,內部應做了相應的註釋,下一步對 performSelector: 進一步封裝
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget { // tagrt判空 if (targetName == nil || actionName == nil) { return nil; } // 對swift的特殊標記 NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName]; // 拼接target-action的tagert,就是方法呼叫的類名的字串,類名規則為Target_方法名 NSString *targetClassString = nil; if (swiftModuleName.length > 0) { targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName]; } else { // 類名規則為Target_方法名 targetClassString = [NSString stringWithFormat:@"Target_%@", targetName]; } // 做了類物件的快取,避免多次建立物件 NSObject *target = self.cachedTarget[targetClassString]; // 類物件 if (target == nil) { Class targetClass = NSClassFromString(targetClassString); target = [[targetClass alloc] init]; } // 處理方法名字 拼接規則為 Action_方法名 NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName]; // 方法名轉 SEL SEL action = NSSelectorFromString(actionString); // 容錯處理 if (target == nil) { // 這裏是處理無響應請求的地方之一,這個demo做得比較簡單,如果沒有可以響應的target,就直接return了。實際開發過程中是可以事先給一個固定的target專門用於在這個時候頂上,然後處理這種請求的 [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params]; return nil; } // 快取物件 if (shouldCacheTarget) { self.cachedTarget[targetClassString] = target; } if ([target respondsToSelector:action]) { // 底層方法呼叫 return [self safePerformAction:action target:target params:params]; } else { // 這裏是處理無響應請求的地方,如果無響應,則嘗試呼叫對應target的notFound方法統一處理 SEL action = NSSelectorFromString(@"notFound:"); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else { // 這裏也是處理無響應請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發過程中,可以用前面提到的固定的target頂上的。 [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params]; [self.cachedTarget removeObjectForKey:targetClassString]; return nil; } } }
此部分主要是對引數進行容錯,對performSelector進一步封裝,並返回方法呼叫方的物件。
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params { NSMethodSignature* methodSig = [target methodSignatureForSelector:action]; if(methodSig == nil) { return nil; } const char* retType = [methodSig methodReturnType]; if (strcmp(retType, @encode(void)) == 0) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setArgument:¶ms atIndex:2]; [invocation setSelector:action]; [invocation setTarget:target]; [invocation invoke]; return nil; } if (strcmp(retType, @encode(NSInteger)) == 0) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setArgument:¶ms atIndex:2]; [invocation setSelector:action]; [invocation setTarget:target]; [invocation invoke]; NSInteger result = 0; [invocation getReturnValue:&result]; return @(result); } if (strcmp(retType, @encode(BOOL)) == 0) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setArgument:¶ms atIndex:2]; [invocation setSelector:action]; [invocation setTarget:target]; [invocation invoke]; BOOL result = 0; [invocation getReturnValue:&result]; return @(result); } if (strcmp(retType, @encode(CGFloat)) == 0) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setArgument:¶ms atIndex:2]; [invocation setSelector:action]; [invocation setTarget:target]; [invocation invoke]; CGFloat result = 0; [invocation getReturnValue:&result]; return @(result); } if (strcmp(retType, @encode(NSUInteger)) == 0) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setArgument:¶ms atIndex:2]; [invocation setSelector:action]; [invocation setTarget:target]; [invocation invoke]; NSUInteger result = 0; [invocation getReturnValue:&result]; return @(result); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" return [target performSelector:action withObject:params]; #pragma clang diagnostic pop }
至此一個完整CTM呼叫就結束了,大致的流程為