iOS 模塊化和組件化的那點(diǎn)事

吃瓜

看了Casa和Limboy's關(guān)于組件化的討論,有種神仙打架,小鬼吃瓜的既視感,在這談?wù)勎覍τ诮M件化的理解。

組件與模塊

首先,咱們先聊聊組件。組件分為兩種:

  1. 一種是具有某一功能的基礎(chǔ)組件(a.弱業(yè)務(wù)層/封裝層 b.功能組件層)。
  2. 一種是具有完整業(yè)務(wù)單元的業(yè)務(wù)組件(模塊!之后我會以模塊來命名)

雖然本質(zhì)上都是組件,但組件強(qiáng)調(diào)的是功能性和可復(fù)用性,模塊強(qiáng)調(diào)的是完整性和業(yè)務(wù)性。于是組件化也可以被分為"組件化"和"模塊化"。

我是圖1

模塊化

有幾個問題需要考慮:
什么是模塊化?為什么要模塊化?怎么進(jìn)行模塊化?

  1. 在我的理解里模塊化就是要解除業(yè)務(wù)模塊之間的耦合,能讓各個模塊彼此獨(dú)立存在。
  2. 那么模塊化究竟有什么好處呢?迭代!
    在開發(fā)中我們的項目的業(yè)務(wù)邏輯可能如下圖:
我是圖2

隨著項目的迭代功能模塊和功能模塊間的交互會越來越多,越往后越有一種維護(hù)不動的感覺,因?yàn)楦鱾€模塊已經(jīng)攪合在一起了。我們想要的只是模塊間的關(guān)系變得簡單一些,使各個模塊能夠高內(nèi)聚低耦合就是我們模塊化的唯一理由。

模塊化的方案

在學(xué)習(xí)一個新東西的時候,我習(xí)慣是先使用,并記錄下來使用過程中的疑惑,再從源碼層面去解答疑惑,下面我也會按照這個節(jié)奏來。

一. CTMediator

使用篇

首先咱們先分析其中一個業(yè)務(wù)場景:
模塊A-1跳轉(zhuǎn)至模塊C-2,并將name和age兩個字段傳向C-2中,當(dāng)點(diǎn)擊C-2時C-2會向A-1回調(diào)一個處理好的字符串msg。
實(shí)現(xiàn)步驟如下:

  1. 首先創(chuàng)建C-2:BusinessC_2ViewController
typedef void(^TouchBlock)(NSString *msg);

@interface BusinessC_2ViewController : BaseViewController

@property (nonatomic, copy) TouchBlock touchBlock;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@implementation BusinessC_2ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    NSString *msg = [[NSString alloc] initWithFormat:@"%@%ld歲啦",self.name,self.age];
    if (self.touchBlock) {
        self.touchBlock(msg);
    }
}
  1. 創(chuàng)建Target_+C模塊名”的 Target_BusinessC(命名方式之后會解釋)
    • 聲明并實(shí)現(xiàn)Action_+方法名的方法Action_getViewControllerC_2:
    • params里為A-1模塊傳來的數(shù)據(jù)
@interface Target_BusinessC : NSObject
- (UIViewController *)Action_getViewControllerC_2:(NSDictionary *)params

#import "BusinessC_2ViewController.h"
@implementation Target_BusinessC
- (UIViewController *)Action_getViewControllerC_2:(NSDictionary *)params {
    BusinessC_2ViewController *businessC2 = [[BusinessC_2ViewController alloc] init];
    businessC2.name = params[@"name"];
    businessC2.age = [params[@"age"] integerValue];
    businessC2.touchBlock = params[@"touchBlock"];
    return businessC2;
}
  1. 基于CTMediator創(chuàng)建分類CTMediator+BusinessA
    1. 聲明并實(shí)現(xiàn)方法getBusinessC2WithName:age:touchBlock
    2. 將要傳遞的數(shù)據(jù)放在字典dict中。
    3. 調(diào)用performTarget:action:params:方法,
      • performTarget的參數(shù)為Target_BusinessB中模塊名BusinessB。
      • action的參數(shù)為Target_BusinessBAction_getViewControllerC_2:方法中去除前綴的方法名getViewControllerC_2。
      • params為參數(shù)字典。
@interface CTMediator (BusinessA)
- (UIViewController *)getBusinessC2WithName:(NSString *)name
                                        age:(NSInteger)age
                                 touchBlock:(void(^)(NSString *msg))touchBlock;

@implementation CTMediator (BusinessA)
- (UIViewController *)getBusinessC2WithName:(NSString *)name
                                        age:(NSInteger)age
                                 touchBlock:(void(^)(NSString *msg))touchBlock {
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
    dict[@"name"] = name;
    dict[@"age"] = @(age);
    dict[@"touchBlock"] = touchBlock;
    return [self performTarget:@"BusinessC" action:@"getViewControllerC_2" params:dict shouldCacheTarget:NO];
}
  1. 創(chuàng)建A-1BusinessA_1ViewController并調(diào)用Target_BusinessAgetBusinessC2WithName方法。
- (void)button3Click {
    UIViewController *businessC2 = [[[CTMediator alloc] init] getBusinessC2WithName:@"BJHL" age:5 touchBlock:^(NSString * _Nonnull msg) {
        NSLog(@"C2傳來的:%@",msg);
    }];
    [self.navigationController pushViewController:businessC2 animated:YES];
}

在A-1中點(diǎn)擊按鈕跳轉(zhuǎn)到C-2中,點(diǎn)擊C-2控制臺會打印:

C2傳來的:BJHL5歲啦

原理篇

在實(shí)現(xiàn)過程中可能會有一些疑問,希望下面能將這些疑惑解答。
首先我們先跳出實(shí)現(xiàn)代碼,在結(jié)構(gòu)上分析各類的關(guān)系:


我是圖3
  • Target_BusinessC

    1. 創(chuàng)建BusinessC_1ViewController實(shí)例。
    2. 解析params,獲取A模塊傳來的參數(shù)。
  • CTMediator+BusinessA

    1. 適配器:將A-1傳來的參數(shù)轉(zhuǎn)包裝成字典。
    2. 調(diào)用CTMediator封裝的performTarget:action:params:shouldCacheTarget:方法。
  • CTMediator
    其實(shí)大家所有的疑惑可能都在CTMediator中:

    1. Target_BusinessC為什么要添加Target_前綴?
    2. 方法為什么要添加Action_前綴?
    3. performTarget:action:params:shouldCacheTarget為什么能夠返回C-2的實(shí)例?為什么沒有類引用Target_BusinessC

咱們就從 performTarget:action:params:shouldCacheTarget開始看看CTMediator究竟做了什么。

NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
    targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
    targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
    Class targetClass = NSClassFromString(targetClassString);
    target = [[targetClass alloc] init];
}
if (shouldCacheTarget) {
    self.cachedTarget[targetClassString] = target;
}
  1. 將傳進(jìn)來的targetName拼接為類名字符串Target_targetName。
  2. 判斷類名為Target_targetName是否有緩存。
  3. 沒有緩存則將字符串Target_targetName映射為對應(yīng)類的實(shí)例(Target_BusinessC)。
  4. 通過shouldCacheTarget來控制是否使用緩存,內(nèi)部是通過類名來進(jìn)行緩存相應(yīng)類的實(shí)例,
  5. 添加Target_的前綴是為了標(biāo)記Target_targetName是負(fù)責(zé)跳轉(zhuǎn)的類。
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
  1. 將傳進(jìn)來的actionName拼接為方法名Action_actionName。到這里大家應(yīng)該就能明白為什么在創(chuàng)建Target_BusinessCAction_actionName方法時要加前綴了,這樣設(shè)計的初衷是為了與普通類/普通方法做區(qū)分。
  2. Action_actionName映射為對應(yīng)的SEL。
if ([target respondsToSelector:action]) {
     return [self safePerformAction:action target:target params:params];
}
- (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;
    }
    ...
    return [target performSelector:action withObject:params];
}

這里做了個區(qū)分:

  • 如果action的返回值是void和值引用類型的會用NSInvocation進(jìn)行方法調(diào)用。
  • [invocation setArgument:&params atIndex:2]; 這里atIndex為2是因?yàn)榈谝粋€參數(shù)代表接受者,第二個參數(shù)代表選擇子,后續(xù)參數(shù)就是消息中的那些參數(shù)。
  • 如果action的返回值是指針引用類型的話使用performSelector:withObject:方法來進(jìn)行方法調(diào)用。

小拓展

這部分與模塊化沒什么直接聯(lián)系,只是我對NSInvocationperformSelector的討論,不感興趣的話可以跳過。
CTMediator為什么不直接用performSelector:withObject:NSInvocation來進(jìn)行方法調(diào)用呢?

  1. 假設(shè)action的返回值是指針引用類型用NSInvocation的話,代碼應(yīng)該會寫成這樣。
NSString *type = [NSString stringWithFormat:@"%s",retType];
if ([type isEqualToString:@"@"]) {
     NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
     [invocation setArgument:&params atIndex:2];
     [invocation setSelector:action];
     [invocation setTarget:target];
     [invocation invoke];
     id result = nil;
     [invocation getReturnValue:&result];
     return result;
}

當(dāng)我們添加這塊代碼后,點(diǎn)擊跳轉(zhuǎn)按鈕能夠正常跳轉(zhuǎn),但是點(diǎn)擊屏幕時會發(fā)生crash!為什么?

在ARC模式下getReturnValue:是從invocation的返回值拷貝到指定的內(nèi)存地址,如果返回值是一個NSObject對象的話,是沒有進(jìn)行內(nèi)存管里的。因?yàn)橄到y(tǒng)默認(rèn)會給指針引用類型進(jìn)行隱式聲明__strong,所以 ARC 假定放入的變量已被retain了,在超出作用范圍時會釋放它。雖然界面已經(jīng)跳轉(zhuǎn)了,但其實(shí)頁面已經(jīng)被釋放了,然后會造成崩潰。
這種情況我們可以將result添加__autoreleasing修飾符解決:

__autoreleasing id result = nil;
  1. 假設(shè)只使用performSelector:action withObject:,我們可以先將所有有關(guān)NSInvocation的代碼進(jìn)行注釋,再次啟動程序點(diǎn)擊跳轉(zhuǎn)按鈕,程序正常運(yùn)行。那么為什么前半部分還要過濾返回值為void值引用類型呢?
    因?yàn)榇藭r在使用performSelector:action withObject:時action是動態(tài)方法,編譯器無法確認(rèn)action方法的返回值類型,導(dǎo)致無法進(jìn)行相應(yīng)的內(nèi)存管理,當(dāng)返回值為void或值引用類型時會發(fā)生崩潰。

CTMediator的其他功能

CTMediator還提供了遠(yuǎn)程跳轉(zhuǎn)的入口performActionWithUrl:completion:
我們可以將targetName actionName params拼入URL中,然后解析URL并調(diào)用performTarget:action:params:shouldCacheTarget:來實(shí)現(xiàn)遠(yuǎn)程調(diào)用的功能。

CTMediator 內(nèi)部提供了緩存的小功能releaseCachedTargetWithTargetName方法可以清除對應(yīng)類的緩存。

使用討論

所有頁面之間都需要使用CTMediator來實(shí)現(xiàn)跳轉(zhuǎn)嗎?
我的看法是同模塊間的頁面是不需要這樣實(shí)現(xiàn)跳轉(zhuǎn)的,因?yàn)樵谕K下,不同的頁面之間的耦合,其實(shí)就是業(yè)務(wù)體現(xiàn),換句話說,此時的耦合就是業(yè)務(wù)。 但是不同模頁面之間的交互是需要使用這種方式來進(jìn)行交互,將各模塊之間隔離起來。于是模塊之間的聯(lián)系就發(fā)生了變化。

我是圖3

每個模塊都會有對應(yīng)的Target_模塊名CTMediator+模塊名,但是每個模塊之間都是沒有直接進(jìn)行引用,完成了模塊化。
簡化下就變成了這樣:

MGJRouter

使用篇

還是剛才的例子我們使用MGJRouter再實(shí)現(xiàn)一下, BusinessC_2ViewController的代碼與之前相同。

  1. 創(chuàng)建RouteConfig.h文件存放路由路徑。
#ifndef RouteConfig_h
#define RouteConfig_h

#define ROUTEURL_BUSIBESSA1_c2_push @"BJHL://BusinessA/A1_c2VC_Push"

#endif /* RouteConfig_h */
  1. 創(chuàng)建注冊路由類RouteRegistered
    一般路由的注冊會放在兩個地方,第一是管理注冊類的+load方法里或者在didFinishLaunchingWithOptions代理中,這兩處沒有什么區(qū)別,只要保證在openURL之前已經(jīng)被注冊過就可以了。
    我們需要在registerBlock中組織跳轉(zhuǎn)的邏輯,獲取參數(shù)等等,registerBlock會儲存在MGJRouter的字典中。
    但需要注意的是拼接在url中的參數(shù)直接在routerParameters字典中通過傳遞的key就可以獲取了,但是通過userInfo傳來的參數(shù)需要先通過key值MGJRouterParameterUserInfo獲取userInfo,然后在userInfo中解析傳來的數(shù)據(jù),這點(diǎn)后面會解釋。
@interface RouteRegistered : NSObject

@end

#import "RouteRegistered.h"
#import "MGJRouter.h"
#import "BusinessC_2ViewController.h"

@implementation RouteRegistered

+ (void)load {
    [MGJRouter registerURLPattern:ROUTEURL_BUSIBESSA1_c2_push toHandler:^(NSDictionary *routerParameters) {
        BusinessC_2ViewController *businessC_2 = [[BusinessC_2ViewController alloc] init];
        UINavigationController *currentVC = routerParameters[MGJRouterParameterUserInfo][@"currentVC"];
        businessC_2.name = routerParameters[@"name"];
        businessC_2.age = [routerParameters[@"age"] integerValue];
        businessC_2.touchBlock = routerParameters[MGJRouterParameterUserInfo][@"touchBlock"];
        [currentVC pushViewController:businessC_2 animated:YES];
    }];
}
  1. BusinessA_1ViewController中需要跳轉(zhuǎn)頁面的地方執(zhí)行代碼:
    將我們需要傳遞的非對象類型的參數(shù)以url?key=value&key=value的格式進(jìn)行拼接,key值和注冊時從參數(shù)字典獲取的key是一一對應(yīng)的。如果要傳遞對象類型的數(shù)據(jù),可以將其包裝在字典中,作為withUserInfo的參數(shù)進(jìn)行傳遞,如可以將當(dāng)前的VC和一個block作為參數(shù)傳遞。
- (void)button3Click {
    void(^touchBlock)(NSString *msg) = ^ (NSString *msg) {
        NSLog(@"%@",msg);
    };
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
    dict[@"currentVC"] = self.navigationController;
    dict[@"touchBlock"] = touchBlock;
    NSString *url = [[NSString alloc] initWithFormat:@"%@?name=%@&age=%d",ROUTEURL_BUSIBESSA1_c2_push,@"BJHL",5];
    [MGJRouter openURL:url withUserInfo:dict completion:nil];
}

在A-1中點(diǎn)擊按鈕跳轉(zhuǎn)到C-2中,點(diǎn)擊C-2控制臺會打印:

C2傳來的:BJHL5歲啦

原理篇

CTMediator的原理是在業(yè)務(wù)模塊無感知情況下進(jìn)行URLregisterBlock的注冊,CTMediator以單例的形式存在,其內(nèi)部有一個字典routes以以URL為key把registerBlock保存起來,當(dāng)用戶調(diào)用openURL:方法進(jìn)行頁面的跳轉(zhuǎn),方法內(nèi)部通過URL來找到對應(yīng)的registerBlock,并在MGJRouter內(nèi)部會觸發(fā)此registerBlock

一. 注冊路由:

這兩個方法差別不大,內(nèi)部都是需要調(diào)用- (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern,他們的區(qū)別在toObjectHandler需要返回一個object 。

+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(MGJRouterHandler)handler;
+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler;

1.1 registerURLPattern:toObjectHandler:的使用需要配合+ (id)objectForURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo。我們可以將RouteRegisteredBusinessA_1ViewController的內(nèi)容用以下代碼替換。

[MGJRouter registerURLPattern:ROUTEURL_BUSIBESSA1_c2_push toObjectHandler:^id(NSDictionary *routerParameters) {
    BusinessC_2ViewController *businessC_2 = [[BusinessC_2ViewController alloc] init];
    businessC_2.name = routerParameters[@"name"];
    businessC_2.age = [routerParameters[@"age"] integerValue];
    businessC_2.touchBlock = routerParameters[MGJRouterParameterUserInfo][@"touchBlock"];
    return businessC_2;
}];

- (void)button3Click {
    void(^touchBlock)(NSString *msg) = ^ (NSString *msg) {
        NSLog(@"%@",msg);
    };
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
    dict[@"currentVC"] = self.navigationController;
    dict[@"touchBlock"] = touchBlock;
    NSString *url = [[NSString alloc] initWithFormat:@"%@?name=%@&age=%d",ROUTEURL_BUSIBESSA1_c2_push,@"BJHL",5];
//    [MGJRouter openURL:url withUserInfo:dict completion:nil];
    UIViewController *businessC_2 = [MGJRouter objectForURL:url withUserInfo:dict];
    [self.navigationController pushViewController:businessC_2 animated:YES];
}

1.2 - (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern解析

- (void)addURLPattern:(NSString *)URLPattern andHandler:(MGJRouterHandler)handler {
    NSMutableDictionary *subRoutes = [self addURLPattern:URLPattern];
    if (handler && subRoutes) {
        subRoutes[@"_"] = [handler copy];
    }
}

- (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern {
    NSArray *pathComponents = [self pathComponentsFromURL:URLPattern];
    NSMutableDictionary* subRoutes = self.routes;
    for (NSString* pathComponent in pathComponents) {
        if (![subRoutes objectForKey:pathComponent]) {
            subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
        }
        subRoutes = subRoutes[pathComponent];
    }
    return subRoutes;
}
  1. pathComponentsFromURL:會以:///為分界符將注冊的URLPattern分解為一個字符串?dāng)?shù)組pathComponents。
  2. 遍歷pathComponents生成如下結(jié)構(gòu):


    routes結(jié)構(gòu)圖
  3. 將最后一級的字典返回,在addURLPattern:方法里保存registerBlock。
二. openURL:及其兄弟方法
+ (void)openURL:(NSString *)URL {
    [self openURL:URL completion:nil];
}

+ (void)openURL:(NSString *)URL completion:(void (^)(id result))completion {
    [self openURL:URL withUserInfo:nil completion:completion];
}

+ (void)openURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo completion:(void (^)(id result))completion {
    URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSMutableDictionary *parameters = [[self sharedInstance] extractParametersFromURL:URL matchExactly:NO];
    [parameters enumerateKeysAndObjectsUsingBlock:^(id key, NSString *obj, BOOL *stop) {
        if ([obj isKindOfClass:[NSString class]]) {
            parameters[key] = [obj stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        }
    }];
    if (parameters) {
        MGJRouterHandler handler = parameters[@"block"];
        if (completion) {
            parameters[MGJRouterParameterCompletion] = completion;
        }
        if (userInfo) {
            parameters[MGJRouterParameterUserInfo] = userInfo;
        }
        if (handler) {
            [parameters removeObjectForKey:@"block"];
            handler(parameters);
        }
    }
}

- (NSMutableDictionary *)extractParametersFromURL:matchExactly:做了兩件事:

  1. 通過url找到注冊的registerBlock并將其放入返回的字典中。
  2. 將url中拼接的參數(shù)提取出來放在返回的字典中。


    parameters
  3. completionblock放入parameters中。
  4. 將參數(shù)字典userInfo以key為MGJRouterParameterUserInfo放入parameters中。(這就是為什么注冊時,在registerBlock里需要通過key MGJRouterParameterUserInfo獲取userInfo的原因)
  5. 觸發(fā)registerBlock。

結(jié)構(gòu)

解析完代碼后我們再看看工程的的結(jié)構(gòu)圖:


我是圖4

模塊內(nèi)部是不知道對其他模塊的依賴的,因?yàn)榻柚鶦TMediator把原本因?yàn)槟K間交互產(chǎn)生的依賴轉(zhuǎn)移到了RouteRegistered中,消除了模塊間的橫向依賴,將RouteRegistered進(jìn)行結(jié)構(gòu)上升,使RouteRegistered依賴所有的業(yè)務(wù)模塊。


WeChat2e097177ed8ebd778bb592e92515b3b0.png

方案間的比較

CTMediator與MGJRouter孰優(yōu)孰劣我在這里不能下定論,我個人偏傾向于CTMediator。
原因如下:

  1. RouteRegistered需要依賴所有的業(yè)務(wù)模塊,那么相當(dāng)于RouteRegistered要知道所有業(yè)務(wù)跳轉(zhuǎn)時的部分業(yè)務(wù)細(xì)節(jié)。
  2. 每有一個業(yè)務(wù)跳轉(zhuǎn),就需要注冊一個URL-配置Block,隨著業(yè)務(wù)量的增加,要保存的Block實(shí)例會越來越多。
  3. 無論用戶使用多少功能,在App啟動時需要將所有的配置Block進(jìn)行注冊 那么對開機(jī)時的性能會有一定的影響。
  4. 使用兩種方式去傳遞參數(shù),維護(hù)起來會有一定的成本。

但是MGJRouter的一大優(yōu)勢在于,更容易進(jìn)行多端統(tǒng)一的路由設(shè)計。

組件化

什么是組件化

我傾向于把它理解為,將程序內(nèi)部的代碼根據(jù)功能的不同封裝成各個組件,并且以一種更加便利和系統(tǒng)的方式去迭代這些組件。

組件化的方案

組件化重點(diǎn)在于組件的功能獨(dú)立和版本迭代,對于功能的顆粒度大家都有自己的見解,而且還需要分析具體的情況,在這我先不多說。我接下來講的重心在版本迭代。

CocoaPod的宗旨是Define once, update easily,不得不說CocoaPod對第三方源碼的非常優(yōu)秀。所以比較流行方案是將工程里的各個功能組件獨(dú)立出來,然后用CocoaPod去維護(hù)這些私庫。

CocoaPod原理篇

我首先簡單講一下CocoaPod的原理,這樣大家在后續(xù)的過程會少一些麻煩。
CocoaPod的大致結(jié)構(gòu)如下:


CocoaPod

首先創(chuàng)建組件的遠(yuǎn)程代碼庫,然后將xxx.podspec推給Repo源進(jìn)行管理,xxx.podspec中記錄著組件的遠(yuǎn)程代碼庫的版本和地址等信息。本地工程通過編寫Podfile文件確定關(guān)聯(lián)的遠(yuǎn)程Repo源和Repo源管理的組件。執(zhí)行pod install就可以將相應(yīng)組件的遠(yuǎn)程代碼拉下來。

CocoaPod的的結(jié)構(gòu)如下:

├── MagicalAoRepo
│   ├── GADesignNetwork
│   │   ├── 0.1.0
│   │   │   └── GADesignNetwork.podspec
│   │   └── 0.1.1
│   │       └── GADesignNetwork.podspec
│   └── README.md

每個版本號都會對應(yīng)一個xxx..podspec文件。

使用篇

創(chuàng)建私有Spec Repo

Pods的索引,一旦在Podfile中設(shè)置source為某個私有repo的git地址,在進(jìn)行pod update的時候就會去repo中進(jìn)行檢索。

  1. 在Github上創(chuàng)建Repo倉庫。
  2. 將遠(yuǎn)程Repo添加到本地。
pod repo add XXXCocoaPodsRepo https://github.com/CodeisSunShine/MagicalAoRepo.git
  1. 進(jìn)入本地repos文件,查看是否添加成功。
cd  ~/.cocoapods/repos 

創(chuàng)建功能組件庫

  1. 在github上創(chuàng)建功能組件倉庫
  2. 在本地創(chuàng)建Pod項目
pod lib create xxxName
  1. 然后依次會有一下幾個問題:
  • 組件化應(yīng)用在哪個平臺上
What platform do you want to use?? [ iOS / macOS ]
  • 使用何種語言
What language do you want to use?? [ Swift / ObjC ]
  • 問是否需要一個Demo工程,方便調(diào)試Pod。
Would you like to include a demo application with your library? [ Yes / No ]
  • 問是否需要UT測試框架,可選擇Specta和Kiwi,或者選擇不要。
Which testing frameworks will you use? [ Specta / Kiwi / None ]
  • Specta是OC的一個輕量級TDD/BDD框架
Possible answers are [ Specta / Kiwi / None ]
  • 如果上一步選擇了Specta ,這步會生成一部分有利于做自動化測試的邏輯和代碼
Would you like to do view based testing? [ Yes / No ]
  • 指定你的項目前綴
What is your class prefix?
  • 編寫podspec配置 打開 xxx.podspec
Pod::Spec.new do |s|
  #組件名稱,也是執(zhí)行 pod search 時輸入的名稱
  s.name             = 'testModule'
  #版本號,通常和tag一致 
  s.version          = '0.1.0'
  #概要,一句話介紹
  s.summary          = '這是一個業(yè)務(wù)組件'
  #描述,比概要字多就可以
  s.description      = <<-DESC 
                         這是一個詳細(xì)的描述,比上面的字多就可以了
                        DESC
  #B pod私有庫的地址
  s.homepage         = 'http://xxxxxx/testModule.git'
  #遵循的開源協(xié)議類型,默認(rèn)MIT
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  #作者及郵箱
  s.author           = { 'author name' => 'xxxxx@email.com' }
  #源碼地址,B pod私有庫的ssh地址,如果需要加入子模塊,就在后面加一個
  s.source           = { :git => 'git@xxxx/testModule.git', :tag => s.version.to_s}
  #與Xcode中主工程的最低支持版本號一直即可
  s.ios.deployment_target = '8.0'
  #源碼文件路徑,如果是oc庫可以像這樣用一個頭文件包含需要引用的本組件其他代碼的頭文件,便于拆分成各個獨(dú)立的文件夾管理,參考AFNetworking的目錄,swift庫就不用了
  s.source_files = 'testModule/Classes/testModule.h'
  #模塊名稱,在工程中調(diào)用時 #import <TModule/xxxx.h>
  s.module_name  = 'TModule'
  #私有頭文件路徑,如果有不希望暴露在組件外的私有頭文件路徑可以設(shè)置
  s.private_header_files = 'testModule/Classes/*.h'
  #公共頭文件路徑
  s.public_header_files = 'testModule/Classes/testModule.h'
  #是否使用ARC,默認(rèn)true
  s.requires_arc = true
  #如果有需要單獨(dú)使用MRC的文件,將文件路徑加入排除文件,并以,隔開
  s.exclude_files = 'testModule/Classes/Libraries/MRC/**/*.{h,m}','testModule/Classes/Categorys/MRC/**/*.{h,m}'
  #依賴的其他庫,包括公開Pod庫、私有Pod庫、subspec等
  s.dependency 'Masonry', '~> 1.0.1'
 
  1. 本地驗(yàn)證:

    1. 進(jìn)入xxx.podspec同一級文件,執(zhí)行pod lib lint xxx.podspec
    2. 如果你的庫無法保證一條 Warning 都沒有,那么當(dāng)你按照上面的這行命令進(jìn)行執(zhí)行后,將會收到來自 CocoasPod 的第一條驗(yàn)證警告.可以執(zhí)行pod lib lint --allow-warnings解決
  2. git提交 建議提交時要備注版本號并與tag和podspec中的version保持一致的版本信息

git add --all
git commit -m "0.1.0"
  1. 與遠(yuǎn)程代碼庫建立連接:
git remote add origin https://xxxxxx/CodeisSunShine/design_Network.git
  1. 將本地代碼推到遠(yuǎn)程倉庫
git push origin master
  1. 打上標(biāo)簽
git tag 0.1.0
  1. 上傳本地tag
git push --tags #上傳本地所有tag
  1. 組件庫發(fā)版: 組件庫發(fā)版也就是將本地的.podspec推到遠(yuǎn)程源倉庫,也就是spec源倉庫。(如果在本地/遠(yuǎn)程驗(yàn)證時加入了 --no-clean 參數(shù),在發(fā)版時需要去掉該參數(shù),否則會報錯。)
pod repo push testModule-spec(A倉庫名) testModule.podspec (.podspec文件名) --allow-warnings --verbose

工程項目使用:

修改Podfile

source 'https://github.com/CocoaPods/Specs.git' # 公有庫 repo
source 'https://github.com/CodeisSunShine/MagicalAoRepo.git'

platform :ios, '8.0'

target 'ArchitectureDemo' do
  # Pods for ArchitectureDemo
  pod 'GADesignNetwork', :git => 'https://github.com/CodeisSunShine/GADesignNetwork.git'
end

執(zhí)行pod install 就可以了

二進(jìn)制化

什么是組件二進(jìn)制化?

通過將非開發(fā)中的組件預(yù)先編譯打包成靜態(tài)/動態(tài)庫并存放在某處,待集成此組件時,直接使用二進(jìn)制包,從而提升集成此組件的App或者上層組件的編譯速度。組件二進(jìn)制化在組件化過程中不是必須的,但我認(rèn)為覺著是必要的。
組件二進(jìn)制化能大大減少工程的編譯速度,那么對于平時開發(fā)調(diào)試和打包效率都有很大的提升。

怎么進(jìn)行二進(jìn)制化?

在做二進(jìn)制化之前我們要保證以下幾點(diǎn):

  1. 不影響未接入二進(jìn)制化方案的功能組件。
  2. 組件級別源碼/二進(jìn)制依賴切換功能。

為了滿足以上要求我的方案是,在組件倉庫中同時存放源碼和二進(jìn)制文件,通過一個變量進(jìn)行標(biāo)記,當(dāng)需要二進(jìn)制文件時返回二進(jìn)制,當(dāng)需要源碼時返回源碼。

修改 xx.podspec

if ENV['use_lib'] || ENV["#{s.name}_use_lib"]
    puts '---------binary-------'
      s.ios.vendored_framework = "Framework/#{s.version}/#{s.name}.framework"
      #這種是幫你打包成bundle
      s.resource_bundles = {
      "#{s.name}" => ["#{s.name}/Assets/*.{png,xib,plist}"]
    }
    #這種是你已經(jīng)打包好了bundle,推薦這種,可以省去每次pod幫你生成bundle的時間
    s.resources = "#{s.name}/Assets/*.bundle"
else
    puts '---------source-------'
      s.source_files = 'GADesignNetwork/Classes/**/*'
    s.public_header_files = "#{s.name}/Classes/**/*.h"
end

制作二進(jìn)制包

  1. 安裝插件
sudo gem install cocoapods-packager -n /usr/local/bin
  1. cd 對應(yīng)組件 xx.podspec文件所在路徑
  2. 使用package
pod package xx.podspec --exclude-deps --force --no-mangle --spec-sources=http://git.xxxxx.net/ios/cocoapods-spec.git 然后包在GADesignNetwork-0.1.1/ios 
  • --exclude-deps 不包含依賴的符號表,生成動態(tài)庫的時候不能包含這個命令,動態(tài)庫一定需要包含依賴的符號表。
  • --force 強(qiáng)制覆蓋之前已經(jīng)生成的二進(jìn)制庫。
  • --no-mangle 如果你的pod庫沒有其他依賴的話,不使用這個命令也不會報錯,但如果有其他依賴,不使用--no-mangle這個命令的話,那么你在工程里使用生成的二進(jìn)制庫時就會報錯 Undefined symbols for architecture x86_64
  • --spec-sources 一些依賴的source 如果有依賴是來自于私有庫的,那么就需要加上那個私有庫的source
  1. 在xx.podspec平級創(chuàng)建Framework和0.1.1文件夾,并移動xxx.framework使其結(jié)構(gòu)為:
├── Framework
│   └── 0.1.1
│       └── xxx.framework

二進(jìn)制使用

使用 use_lib=1 pod install 為安裝二進(jìn)制文件,直接pod install則安裝的是源碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容