切換語言為:簡體
高階 iOS 工程師必備知識之程式碼元件化思路及實踐

高階 iOS 工程師必備知識之程式碼元件化思路及實踐

  • 爱糖宝
  • 2024-07-27
  • 2080
  • 0
  • 0

iOS元件化的分層思路

元件化是一種軟體設計方法,它將一個大型應用程式拆分成多個獨立且可重用的模組。這些模組可以分別開發、測試和維護,從而提高程式碼的複用性和可維護性。通常,元件化的分層思想大致分為以下三層:

  1. 基礎模組:封裝一些不與業務相關的模組,如工具類和分類。這一層的程式碼應當是完全獨立於專案,可以在其他專案中直接使用而無需修改。

  2. 通用模組:實際上是“通用業務模組”,既包含通用程式碼,也包含與業務邏輯相關的通用部分,如通用UIButton元件等。

  3. 業務模組:具體業務邏輯的實現模組,需要結合實際專案進行劃分。

模組的開發整合順序為從下至上,即從基礎模組到通用模組再到業務模組,而依賴順序則是從上到下。

業務模組的設計

業務模組的設計是元件化過程中最難的一部分,需要綜合考慮當前的合理性和未來的擴充套件性。在元件化之前,專案中的各個模組可能會有複雜的耦合關係。在進行元件化之後,需要透過建立“通訊中間層”來降低這些模組之間的耦合度。

CTMediator 通訊中間框架介紹

高階 iOS 工程師必備知識之程式碼元件化思路及實踐 CTMediator是一個用於模組間通訊的中間層框架,透過runtime機制實現完全杜絕模組間的耦合。它的基本思路包括: • 每個模組隔離出一個獨立的Target層,這個層是模組的宣告檔案。該層提供了外部呼叫該模組功能的介面,並對傳入引數進行驗證和錯誤處理。 • 使用字串形式的類名和方法名,透過runtime機制在不匯入某模組的情況下呼叫其方法,從而避免模組間的硬性依賴。

增加分類 - 解決中間層與模組間的耦合

為防止CTMediator框架發生意外變化及避免專案或模組對框架的強耦合,可以透過增加分類(Category)進行拓展。其好處包括:

  1. 防止框架變化:透過建立分類進行拓展,不直接修改CTMediator原始碼,避免因框架升級導致的問題。

  2. 減少強耦合:為每個模組獨立出一個類,分類中的方法與Target層宣告的方法對應,方便外部呼叫。這使一個完整模組包括 分類 --> Target層 ---> 模組原始碼層。

框架使用示例

以present image操作為例,模組A暴露了一個imageView��於傳值。模組A的Target層實現了所需方法,如Action_nativePresentImage。最終,透過CTMediator框架的performSelector方法實現對目標方法的呼叫。 總結 • CTMediator透過runtime機制進行模組解耦。 • 增加分類進一步減少中間層與模組間的耦合。 • 模組化設計提高程式碼的可複用性和維護性。

基礎模組與通用模組的分層方式

這部分可依需求調整: • 基礎模組:封裝不與業務相關的模組,如分類、工具類等。理想狀態下,這一層不需修改即可在其他專案中使用。 • 通用模組:主要是通用業務模組,如公用元件、通用UIButton、瀑布流、時間計算NSDate等。此層應體現與業務掛鉤的通用邏輯;如果完全獨立於業務邏輯,則放入基礎模組。

業務模組

元件化除了技術層面,更難的是業務模組設計,需要考慮整個專案的劃分、當前分層的合理性及未來擴充套件性。頻繁改動的模組需要合理設計前端介面及與其他模組的互動。

通常專案模組間的關係大致如下圖,就是我們在進行元件化之前的專案,給個層次中間的關係,這裏寫的模組可以理解是類之間的關係。

高階 iOS 工程師必備知識之程式碼元件化思路及實踐

各個模組都或多或少有關係,模組間進行通訊(即類之間的方法呼叫),需要進行#import匯入標頭檔案,是一種比較強的耦合關係。

  高階 iOS 工程師必備知識之程式碼元件化思路及實踐

模組化的元件間耦合問題

爲了實現模組間的低耦合,首先需要解決模組間的耦合關係。模組越獨立,系統的可維護性和靈活性就越高。基本思路是建立一個通訊中間層,各模組透過該中間層進行通訊,避免直接聯絡。

通訊中間層的問題

  1. 與中間層的強耦合:中間層需要匯入所有模組,才能允許模組間通訊。

  2. 中間層過於龐大:如果A模組只想與C模組通訊,中間層卻包含ABCD所有程式碼,這不合理。

  3. 排列組合問題:中間層設計為AB、AC、AD、BC、BD、CD等組合,會使設計複雜化。

CTMediator 通訊中間框架介紹 框架地址:github.com/casatwy/CTM… 程式碼結構圖: 高階 iOS 工程師必備知識之程式碼元件化思路及實踐

框架使用前提:已經對專案劃分好合理的業務模組,單純是對專案的業務進行分層,不考模組之間的通訊。

基本思路

  1. 專案劃分:先對專案進行合理的業務模組劃分,不考慮模組間通訊。

  2. 獨立Target層:每個模組有一個獨立的Target層,作為模組宣告和入口,類似於.h檔案。

  3. 入口封裝:Target層封裝模組提供給外部的功能,如登入、註冊等,並進行業務判斷或容錯處理。

程式碼實現思路 • 杜絕耦合:透過runtime機制將方法呼叫轉換為字串形式,以類名和方法名(字串)進行呼叫,避免模組間直接匯入。 • 錯誤處理:框架內部對引數如target和action進行容錯處理,以減少手誤風險。

增加分類 - 減少中間層與模組間的耦合

設計目的

  1. 防止框架意外變化:建立分類進行拓展,不直接修改CTMediator原始碼,類似於使用AFN等三方框架時,透過獨立類管理和封裝。

  2. 減少強耦合:為每個模組獨立出一個類,分類中的方法與Target層宣告的方法一一對應。

實現方式 • 舉例:在登入模組中,將登入、註冊、忘記密碼等功能方法分別在Target層寫好宣告和實現,然後複製到分類。 • 外部呼叫:外部只需匯入這個分類即可使用相應功能,類似於常規分類的使用效果。 • 完整模組結構:一個完整的模組包括 分類 --> Target層 ---> 模組原始碼層。

CTMediator程式碼和demo介紹

下面我們針對在github給出的demo進行介紹

  1. 專案整體介紹 專案程式碼結構

    高階 iOS 工程師必備知識之程式碼元件化思路及實踐

    程式碼執行起來,是一個tableView

    高階 iOS 工程師必備知識之程式碼元件化思路及實踐

    我們以 present image 這個操作

  2. 這個功能是將 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.我的理解起名和如何定義字串,只要專案內部約定好就行,大家都根據這規則就好。 高階 iOS 工程師必備知識之程式碼元件化思路及實踐

  1. 第二步解決了方法名的問題,然後我們整體看下方法的呼叫,從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:&params 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:&params 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:&params 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:&params 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:&params 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呼叫就結束了,大致的流程為

  高階 iOS 工程師必備知識之程式碼元件化思路及實踐


0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.