iOS 模块化和组件化的那点事

吃瓜

看了Casa和Limboy's关于组件化的讨论,有种神仙打架,小鬼吃瓜的既视感,在这谈谈我对于组件化的理解。

组件与模块

首先,咱们先聊聊组件。组件分为两种:

  1. 一种是具有某一功能的基础组件(a.弱业务层/封装层 b.功能组件层)。
  2. 一种是具有完整业务单元的业务组件(模块!之后我会以模块来命名)

虽然本质上都是组件,但组件强调的是功能性和可复用性,模块强调的是完整性和业务性。于是组件化也可以被分为"组件化"和"模块化"。

我是图1

模块化

有几个问题需要考虑:
什么是模块化?为什么要模块化?怎么进行模块化?

  1. 在我的理解里模块化就是要解除业务模块之间的耦合,能让各个模块彼此独立存在。
  2. 那么模块化究竟有什么好处呢?迭代!
    在开发中我们的项目的业务逻辑可能如下图:
我是图2

随着项目的迭代功能模块和功能模块间的交互会越来越多,越往后越有一种维护不动的感觉,因为各个模块已经搅合在一起了。我们想要的只是模块间的关系变得简单一些,使各个模块能够高内聚低耦合就是我们模块化的唯一理由。

模块化的方案

在学习一个新东西的时候,我习惯是先使用,并记录下来使用过程中的疑惑,再从源码层面去解答疑惑,下面我也会按照这个节奏来。

一. CTMediator

使用篇

首先咱们先分析其中一个业务场景:
模块A-1跳转至模块C-2,并将name和age两个字段传向C-2中,当点击C-2时C-2会向A-1回调一个处理好的字符串msg。
实现步骤如下:

  1. 首先创建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. 创建Target_+C模块名”的 Target_BusinessC(命名方式之后会解释)
    • 声明并实现Action_+方法名的方法Action_getViewControllerC_2:
    • params里为A-1模块传来的数据
@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创建分类CTMediator+BusinessA
    1. 声明并实现方法getBusinessC2WithName:age:touchBlock
    2. 将要传递的数据放在字典dict中。
    3. 调用performTarget:action:params:方法,
      • performTarget的参数为Target_BusinessB中模块名BusinessB
      • action的参数为Target_BusinessBAction_getViewControllerC_2:方法中去除前缀的方法名getViewControllerC_2
      • params为参数字典。
@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. 创建A-1BusinessA_1ViewController并调用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中点击按钮跳转到C-2中,点击C-2控制台会打印:

C2传来的:BJHL5岁啦

原理篇

在实现过程中可能会有一些疑问,希望下面能将这些疑惑解答。
首先我们先跳出实现代码,在结构上分析各类的关系:


我是图3
  • Target_BusinessC

    1. 创建BusinessC_1ViewController实例。
    2. 解析params,获取A模块传来的参数。
  • CTMediator+BusinessA

    1. 适配器:将A-1传来的参数转包装成字典。
    2. 调用CTMediator封装的performTarget:action:params:shouldCacheTarget:方法。
  • CTMediator
    其实大家所有的疑惑可能都在CTMediator中:

    1. Target_BusinessC为什么要添加Target_前缀?
    2. 方法为什么要添加Action_前缀?
    3. performTarget:action:params:shouldCacheTarget为什么能够返回C-2的实例?为什么没有类引用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. 将传进来的targetName拼接为类名字符串Target_targetName
  2. 判断类名为Target_targetName是否有缓存。
  3. 没有缓存则将字符串Target_targetName映射为对应类的实例(Target_BusinessC)。
  4. 通过shouldCacheTarget来控制是否使用缓存,内部是通过类名来进行缓存相应类的实例,
  5. 添加Target_的前缀是为了标记Target_targetName是负责跳转的类。
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
  1. 将传进来的actionName拼接为方法名Action_actionName。到这里大家应该就能明白为什么在创建Target_BusinessCAction_actionName方法时要加前缀了,这样设计的初衷是为了与普通类/普通方法做区分。
  2. Action_actionName映射为对应的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];
}

这里做了个区分:

  • 如果action的返回值是void和值引用类型的会用NSInvocation进行方法调用。
  • [invocation setArgument:&params atIndex:2]; 这里atIndex为2是因为第一个参数代表接受者,第二个参数代表选择子,后续参数就是消息中的那些参数。
  • 如果action的返回值是指针引用类型的话使用performSelector:withObject:方法来进行方法调用。

小拓展

这部分与模块化没什么直接联系,只是我对NSInvocationperformSelector的讨论,不感兴趣的话可以跳过。
CTMediator为什么不直接用performSelector:withObject:NSInvocation来进行方法调用呢?

  1. 假设action的返回值是指针引用类型用NSInvocation的话,代码应该会写成这样。
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;
}

当我们添加这块代码后,点击跳转按钮能够正常跳转,但是点击屏幕时会发生crash!为什么?

在ARC模式下getReturnValue:是从invocation的返回值拷贝到指定的内存地址,如果返回值是一个NSObject对象的话,是没有进行内存管里的。因为系统默认会给指针引用类型进行隐式声明__strong,所以 ARC 假定放入的变量已被retain了,在超出作用范围时会释放它。虽然界面已经跳转了,但其实页面已经被释放了,然后会造成崩溃。
这种情况我们可以将result添加__autoreleasing修饰符解决:

__autoreleasing id result = nil;
  1. 假设只使用performSelector:action withObject:,我们可以先将所有有关NSInvocation的代码进行注释,再次启动程序点击跳转按钮,程序正常运行。那么为什么前半部分还要过滤返回值为void值引用类型呢?
    因为此时在使用performSelector:action withObject:时action是动态方法,编译器无法确认action方法的返回值类型,导致无法进行相应的内存管理,当返回值为void或值引用类型时会发生崩溃。

CTMediator的其他功能

CTMediator还提供了远程跳转的入口performActionWithUrl:completion:
我们可以将targetName actionName params拼入URL中,然后解析URL并调用performTarget:action:params:shouldCacheTarget:来实现远程调用的功能。

CTMediator 内部提供了缓存的小功能releaseCachedTargetWithTargetName方法可以清除对应类的缓存。

使用讨论

所有页面之间都需要使用CTMediator来实现跳转吗?
我的看法是同模块间的页面是不需要这样实现跳转的,因为在同模块下,不同的页面之间的耦合,其实就是业务体现,换句话说,此时的耦合就是业务。 但是不同模页面之间的交互是需要使用这种方式来进行交互,将各模块之间隔离起来。于是模块之间的联系就发生了变化。

我是图3

每个模块都会有对应的Target_模块名CTMediator+模块名,但是每个模块之间都是没有直接进行引用,完成了模块化。
简化下就变成了这样:

MGJRouter

使用篇

还是刚才的例子我们使用MGJRouter再实现一下, BusinessC_2ViewController的代码与之前相同。

  1. 创建RouteConfig.h文件存放路由路径。
#ifndef RouteConfig_h
#define RouteConfig_h

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

#endif /* RouteConfig_h */
  1. 创建注册路由类RouteRegistered
    一般路由的注册会放在两个地方,第一是管理注册类的+load方法里或者在didFinishLaunchingWithOptions代理中,这两处没有什么区别,只要保证在openURL之前已经被注册过就可以了。
    我们需要在registerBlock中组织跳转的逻辑,获取参数等等,registerBlock会储存在MGJRouter的字典中。
    但需要注意的是拼接在url中的参数直接在routerParameters字典中通过传递的key就可以获取了,但是通过userInfo传来的参数需要先通过key值MGJRouterParameterUserInfo获取userInfo,然后在userInfo中解析传来的数据,这点后面会解释。
@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中需要跳转页面的地方执行代码:
    将我们需要传递的非对象类型的参数以url?key=value&key=value的格式进行拼接,key值和注册时从参数字典获取的key是一一对应的。如果要传递对象类型的数据,可以将其包装在字典中,作为withUserInfo的参数进行传递,如可以将当前的VC和一个block作为参数传递。
- (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中点击按钮跳转到C-2中,点击C-2控制台会打印:

C2传来的:BJHL5岁啦

原理篇

CTMediator的原理是在业务模块无感知情况下进行URLregisterBlock的注册,CTMediator以单例的形式存在,其内部有一个字典routes以以URL为key把registerBlock保存起来,当用户调用openURL:方法进行页面的跳转,方法内部通过URL来找到对应的registerBlock,并在MGJRouter内部会触发此registerBlock

一. 注册路由:

这两个方法差别不大,内部都是需要调用- (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern,他们的区别在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的内容用以下代码替换。

[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分解为一个字符串数组pathComponents。
  2. 遍历pathComponents生成如下结构:


    routes结构图
  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中拼接的参数提取出来放在返回的字典中。


    parameters
  3. completionblock放入parameters中。
  4. 将参数字典userInfo以key为MGJRouterParameterUserInfo放入parameters中。(这就是为什么注册时,在registerBlock里需要通过key MGJRouterParameterUserInfo获取userInfo的原因)
  5. 触发registerBlock

结构

解析完代码后我们再看看工程的的结构图:


我是图4

模块内部是不知道对其他模块的依赖的,因为借助CTMediator把原本因为模块间交互产生的依赖转移到了RouteRegistered中,消除了模块间的横向依赖,将RouteRegistered进行结构上升,使RouteRegistered依赖所有的业务模块。


WeChat2e097177ed8ebd778bb592e92515b3b0.png

方案间的比较

CTMediator与MGJRouter孰优孰劣我在这里不能下定论,我个人偏倾向于CTMediator。
原因如下:

  1. RouteRegistered需要依赖所有的业务模块,那么相当于RouteRegistered要知道所有业务跳转时的部分业务细节。
  2. 每有一个业务跳转,就需要注册一个URL-配置Block,随着业务量的增加,要保存的Block实例会越来越多。
  3. 无论用户使用多少功能,在App启动时需要将所有的配置Block进行注册 那么对开机时的性能会有一定的影响。
  4. 使用两种方式去传递参数,维护起来会有一定的成本。

但是MGJRouter的一大优势在于,更容易进行多端统一的路由设计。

组件化

什么是组件化

我倾向于把它理解为,将程序内部的代码根据功能的不同封装成各个组件,并且以一种更加便利和系统的方式去迭代这些组件。

组件化的方案

组件化重点在于组件的功能独立和版本迭代,对于功能的颗粒度大家都有自己的见解,而且还需要分析具体的情况,在这我先不多说。我接下来讲的重心在版本迭代。

CocoaPod的宗旨是Define once, update easily,不得不说CocoaPod对第三方源码的非常优秀。所以比较流行方案是将工程里的各个功能组件独立出来,然后用CocoaPod去维护这些私库。

CocoaPod原理篇

我首先简单讲一下CocoaPod的原理,这样大家在后续的过程会少一些麻烦。
CocoaPod的大致结构如下:


CocoaPod

首先创建组件的远程代码库,然后将xxx.podspec推给Repo源进行管理,xxx.podspec中记录着组件的远程代码库的版本和地址等信息。本地工程通过编写Podfile文件确定关联的远程Repo源和Repo源管理的组件。执行pod install就可以将相应组件的远程代码拉下来。

CocoaPod的的结构如下:

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

每个版本号都会对应一个xxx..podspec文件。

使用篇

创建私有Spec Repo

Pods的索引,一旦在Podfile中设置source为某个私有repo的git地址,在进行pod update的时候就会去repo中进行检索。

  1. 在Github上创建Repo仓库。
  2. 将远程Repo添加到本地。
pod repo add XXXCocoaPodsRepo https://github.com/CodeisSunShine/MagicalAoRepo.git
  1. 进入本地repos文件,查看是否添加成功。
cd  ~/.cocoapods/repos 

创建功能组件库

  1. 在github上创建功能组件仓库
  2. 在本地创建Pod项目
pod lib create xxxName
  1. 然后依次会有一下几个问题:
  • 组件化应用在哪个平台上
What platform do you want to use?? [ iOS / macOS ]
  • 使用何种语言
What language do you want to use?? [ Swift / ObjC ]
  • 问是否需要一个Demo工程,方便调试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|
  #组件名称,也是执行 pod search 时输入的名称
  s.name             = 'testModule'
  #版本号,通常和tag一致 
  s.version          = '0.1.0'
  #概要,一句话介绍
  s.summary          = '这是一个业务组件'
  #描述,比概要字多就可以
  s.description      = <<-DESC 
                         这是一个详细的描述,比上面的字多就可以了
                        DESC
  #B pod私有库的地址
  s.homepage         = 'http://xxxxxx/testModule.git'
  #遵循的开源协议类型,默认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库可以像这样用一个头文件包含需要引用的本组件其他代码的头文件,便于拆分成各个独立的文件夹管理,参考AFNetworking的目录,swift库就不用了
  s.source_files = 'testModule/Classes/testModule.h'
  #模块名称,在工程中调用时 #import <TModule/xxxx.h>
  s.module_name  = 'TModule'
  #私有头文件路径,如果有不希望暴露在组件外的私有头文件路径可以设置
  s.private_header_files = 'testModule/Classes/*.h'
  #公共头文件路径
  s.public_header_files = 'testModule/Classes/testModule.h'
  #是否使用ARC,默认true
  s.requires_arc = true
  #如果有需要单独使用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. 本地验证:

    1. 进入xxx.podspec同一级文件,执行pod lib lint xxx.podspec
    2. 如果你的库无法保证一条 Warning 都没有,那么当你按照上面的这行命令进行执行后,将会收到来自 CocoasPod 的第一条验证警告.可以执行pod lib lint --allow-warnings解决
  2. git提交 建议提交时要备注版本号并与tag和podspec中的version保持一致的版本信息

git add --all
git commit -m "0.1.0"
  1. 与远程代码库建立连接:
git remote add origin https://xxxxxx/CodeisSunShine/design_Network.git
  1. 将本地代码推到远程仓库
git push origin master
  1. 打上标签
git tag 0.1.0
  1. 上传本地tag
git push --tags #上传本地所有tag
  1. 组件库发版: 组件库发版也就是将本地的.podspec推到远程源仓库,也就是spec源仓库。(如果在本地/远程验证时加入了 --no-clean 参数,在发版时需要去掉该参数,否则会报错。)
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

执行pod install 就可以了

二进制化

什么是组件二进制化?

通过将非开发中的组件预先编译打包成静态/动态库并存放在某处,待集成此组件时,直接使用二进制包,从而提升集成此组件的App或者上层组件的编译速度。组件二进制化在组件化过程中不是必须的,但我认为觉着是必要的。
组件二进制化能大大减少工程的编译速度,那么对于平时开发调试和打包效率都有很大的提升。

怎么进行二进制化?

在做二进制化之前我们要保证以下几点:

  1. 不影响未接入二进制化方案的功能组件。
  2. 组件级别源码/二进制依赖切换功能。

为了满足以上要求我的方案是,在组件仓库中同时存放源码和二进制文件,通过一个变量进行标记,当需要二进制文件时返回二进制,当需要源码时返回源码。

修改 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}"]
    }
    #这种是你已经打包好了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

制作二进制包

  1. 安装插件
sudo gem install cocoapods-packager -n /usr/local/bin
  1. cd 对应组件 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 不包含依赖的符号表,生成动态库的时候不能包含这个命令,动态库一定需要包含依赖的符号表。
  • --force 强制覆盖之前已经生成的二进制库。
  • --no-mangle 如果你的pod库没有其他依赖的话,不使用这个命令也不会报错,但如果有其他依赖,不使用--no-mangle这个命令的话,那么你在工程里使用生成的二进制库时就会报错 Undefined symbols for architecture x86_64
  • --spec-sources 一些依赖的source 如果有依赖是来自于私有库的,那么就需要加上那个私有库的source
  1. 在xx.podspec平级创建Framework和0.1.1文件夹,并移动xxx.framework使其结构为:
├── Framework
│   └── 0.1.1
│       └── xxx.framework

二进制使用

使用 use_lib=1 pod install 为安装二进制文件,直接pod install则安装的是源码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345