iOS 模块化和组件化的那点事
吃瓜
看了Casa和Limboy's关于组件化的讨论,有种神仙打架,小鬼吃瓜的既视感,在这谈谈我对于组件化的理解。
组件与模块
首先,咱们先聊聊组件。组件分为两种:
- 一种是具有某一功能的基础组件(a.弱业务层/封装层 b.功能组件层)。
- 一种是具有完整业务单元的业务组件(模块!之后我会以模块来命名)
虽然本质上都是组件,但组件强调的是功能性和可复用性,模块强调的是完整性和业务性。于是组件化也可以被分为"组件化"和"模块化"。
我是图1模块化
有几个问题需要考虑:
什么是模块化?为什么要模块化?怎么进行模块化?
- 在我的理解里模块化就是要解除业务模块之间的耦合,能让各个模块彼此独立存在。
- 那么模块化究竟有什么好处呢?迭代!
在开发中我们的项目的业务逻辑可能如下图:
随着项目的迭代功能模块和功能模块间的交互会越来越多,越往后越有一种维护不动的感觉,因为各个模块已经搅合在一起了。我们想要的只是模块间的关系变得简单一些,使各个模块能够高内聚低耦合就是我们模块化的唯一理由。
模块化的方案
在学习一个新东西的时候,我习惯是先使用,并记录下来使用过程中的疑惑,再从源码层面去解答疑惑,下面我也会按照这个节奏来。
一. CTMediator
使用篇
首先咱们先分析其中一个业务场景:
模块A-1跳转至模块C-2,并将name和age两个字段传向C-2中,当点击C-2时C-2会向A-1回调一个处理好的字符串msg。
实现步骤如下:
- 首先创建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);
}
}
- 创建
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;
}
- 基于CTMediator创建分类
CTMediator+BusinessA
:- 声明并实现方法
getBusinessC2WithName:age:touchBlock
。 - 将要传递的数据放在字典dict中。
- 调用
performTarget:action:params:
方法,-
performTarget
的参数为Target_BusinessB
中模块名BusinessB
。 -
action
的参数为Target_BusinessB
中Action_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];
}
- 创建A-1
BusinessA_1ViewController
并调用Target_BusinessA
的getBusinessC2WithName
方法。
- (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
- 创建
BusinessC_1ViewController
实例。 - 解析
params
,获取A模块传来的参数。
- 创建
-
CTMediator+BusinessA
- 适配器:将A-1传来的参数转包装成字典。
- 调用
CTMediator
封装的performTarget:action:params:shouldCacheTarget:
方法。
-
CTMediator
其实大家所有的疑惑可能都在CTMediator
中:-
Target_BusinessC
为什么要添加Target_
前缀? - 方法为什么要添加
Action_
前缀? -
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;
}
- 将传进来的
targetName
拼接为类名字符串Target_targetName
。 - 判断类名为
Target_targetName
是否有缓存。 - 没有缓存则将字符串
Target_targetName
映射为对应类的实例(Target_BusinessC
)。 - 通过
shouldCacheTarget
来控制是否使用缓存,内部是通过类名来进行缓存相应类的实例, - 添加
Target_
的前缀是为了标记Target_targetName
是负责跳转的类。
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
- 将传进来的
actionName
拼接为方法名Action_actionName
。到这里大家应该就能明白为什么在创建Target_BusinessC
和Action_actionName
方法时要加前缀了,这样设计的初衷是为了与普通类/普通方法做区分。 - 将
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:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil;
}
...
return [target performSelector:action withObject:params];
}
这里做了个区分:
- 如果action的返回值是void和值引用类型的会用NSInvocation进行方法调用。
-
[invocation setArgument:¶ms atIndex:2];
这里atIndex为2是因为第一个参数代表接受者,第二个参数代表选择子,后续参数就是消息中的那些参数。 - 如果action的返回值是指针引用类型的话使用
performSelector:withObject:
方法来进行方法调用。
小拓展
这部分与模块化没什么直接联系,只是我对NSInvocation
和performSelector
的讨论,不感兴趣的话可以跳过。
CTMediator
为什么不直接用performSelector:withObject:
或NSInvocation
来进行方法调用呢?
- 假设
action
的返回值是指针引用类型用NSInvocation
的话,代码应该会写成这样。
NSString *type = [NSString stringWithFormat:@"%s",retType];
if ([type isEqualToString:@"@"]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms 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;
- 假设只使用
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来实现跳转吗?
我的看法是同模块间的页面是不需要这样实现跳转的,因为在同模块下,不同的页面之间的耦合,其实就是业务体现,换句话说,此时的耦合就是业务。 但是不同模页面之间的交互是需要使用这种方式来进行交互,将各模块之间隔离起来。于是模块之间的联系就发生了变化。
每个模块都会有对应的
Target_模块名
和 CTMediator+模块名
,但是每个模块之间都是没有直接进行引用,完成了模块化。简化下就变成了这样:
MGJRouter
使用篇
还是刚才的例子我们使用MGJRouter再实现一下, BusinessC_2ViewController
的代码与之前相同。
- 创建RouteConfig.h文件存放路由路径。
#ifndef RouteConfig_h
#define RouteConfig_h
#define ROUTEURL_BUSIBESSA1_c2_push @"BJHL://BusinessA/A1_c2VC_Push"
#endif /* RouteConfig_h */
- 创建注册路由类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];
}];
}
- 在
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
的原理是在业务模块无感知情况下进行URL
与registerBlock
的注册,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
。我们可以将RouteRegistered
和BusinessA_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;
}
-
pathComponentsFromURL:
会以://
和/
为分界符将注册的URLPattern分解为一个字符串数组pathComponents。 -
遍历pathComponents生成如下结构:
routes结构图 - 将最后一级的字典返回,在
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:
做了两件事:
- 通过url找到注册的
registerBlock
并将其放入返回的字典中。 -
将url中拼接的参数提取出来放在返回的字典中。
parameters - 将
completion
的block
放入parameters
中。 - 将参数字典
userInfo
以key为MGJRouterParameterUserInfo
放入parameters
中。(这就是为什么注册时,在registerBlock
里需要通过keyMGJRouterParameterUserInfo
获取userInfo的原因) - 触发
registerBlock
。
结构
解析完代码后我们再看看工程的的结构图:
我是图4
模块内部是不知道对其他模块的依赖的,因为借助CTMediator把原本因为模块间交互产生的依赖转移到了RouteRegistered中,消除了模块间的横向依赖,将RouteRegistered进行结构上升,使RouteRegistered依赖所有的业务模块。
WeChat2e097177ed8ebd778bb592e92515b3b0.png
方案间的比较
CTMediator与MGJRouter孰优孰劣我在这里不能下定论,我个人偏倾向于CTMediator。
原因如下:
- RouteRegistered需要依赖所有的业务模块,那么相当于RouteRegistered要知道所有业务跳转时的部分业务细节。
- 每有一个业务跳转,就需要注册一个URL-配置Block,随着业务量的增加,要保存的Block实例会越来越多。
- 无论用户使用多少功能,在App启动时需要将所有的配置Block进行注册 那么对开机时的性能会有一定的影响。
- 使用两种方式去传递参数,维护起来会有一定的成本。
但是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中进行检索。
- 在Github上创建Repo仓库。
- 将远程Repo添加到本地。
pod repo add XXXCocoaPodsRepo https://github.com/CodeisSunShine/MagicalAoRepo.git
- 进入本地repos文件,查看是否添加成功。
cd ~/.cocoapods/repos
创建功能组件库
- 在github上创建功能组件仓库
- 在本地创建Pod项目
pod lib create xxxName
- 然后依次会有一下几个问题:
- 组件化应用在哪个平台上
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'
-
本地验证:
- 进入xxx.podspec同一级文件,执行pod lib lint xxx.podspec
- 如果你的库无法保证一条 Warning 都没有,那么当你按照上面的这行命令进行执行后,将会收到来自 CocoasPod 的第一条验证警告.可以执行pod lib lint --allow-warnings解决
-
git提交 建议提交时要备注版本号并与tag和podspec中的version保持一致的版本信息
git add --all
git commit -m "0.1.0"
- 与远程代码库建立连接:
git remote add origin https://xxxxxx/CodeisSunShine/design_Network.git
- 将本地代码推到远程仓库
git push origin master
- 打上标签
git tag 0.1.0
- 上传本地tag
git push --tags #上传本地所有tag
- 组件库发版: 组件库发版也就是将本地的.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或者上层组件的编译速度。组件二进制化在组件化过程中不是必须的,但我认为觉着是必要的。
组件二进制化能大大减少工程的编译速度,那么对于平时开发调试和打包效率都有很大的提升。
怎么进行二进制化?
在做二进制化之前我们要保证以下几点:
- 不影响未接入二进制化方案的功能组件。
- 组件级别源码/二进制依赖切换功能。
为了满足以上要求我的方案是,在组件仓库中同时存放源码和二进制文件,通过一个变量进行标记,当需要二进制文件时返回二进制,当需要源码时返回源码。
修改 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
制作二进制包
- 安装插件
sudo gem install cocoapods-packager -n /usr/local/bin
- cd 对应组件 xx.podspec文件所在路径
- 使用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
- 在xx.podspec平级创建Framework和0.1.1文件夹,并移动xxx.framework使其结构为:
├── Framework
│ └── 0.1.1
│ └── xxx.framework
二进制使用
使用 use_lib=1 pod install
为安装二进制文件,直接pod install则安装的是源码。