写点什么

iOS App 组件化开发实践

2016 年 9 月 25 日

前因

其实我们这个 7 人 iOS 开发团队并不适合组件化开发。原因是因为性价比低,需要花很多时间和经历去做这件事,带来的收益并不能彻底改变什么。但是因为有 2~3 个星期的空档期,并不是很忙;另外是可以用在一个全新的 App 上。所以决定想尝试下组件化开发。

所谓尝试也就是说:去尝试解决组件化开发当中的一些问题。如果能解决,并且有比较好的解决方案,那就继续下去,否则就放弃。

背景

脱离实际情况去谈方案的选型是不合理的。

所以先简单介绍下背景:我们是一家纳斯达克交易所上市的科技企业。我们公司还有好几款 App,由不同的几个团队去维护,我们是其中之一。我们这个团队是一个 7 人的 iOS 开发小团队。作者本人是小组长。

之前的 App 已经使用了模块化(CocoaPods)开发,并且已经使用了 **二进制化 ** 方案。App 已经在使用自动化集成。

虽然要开发一个新 App,但是很多业务和之前的 App 是一样的或者相似的。

为什么要写这篇博客?

想把整个过程记录下来,方便以后回顾。

我们的思路和解决方案不一定是对的或者是最好的。所以希望大家看了这篇博客之后,能给我们提供很多建议和别的解决方案,让我们可以优化使得这个组件化开发的方案能变得更加好。

技术栈

  • gitlab
  • gitlab-runner
  • CocoaPods
  • CocoaPods-Packager
  • fir
  • 二进制化
  • fastlane
  • deploymate
  • oclint
  • Kiwi

成果

使用组件化开发 App 之后:

  • 代码提交更规范,质量提高。体现在测试人员反馈的 bug 明显减少。
  • 编译加快。在都是源码的情况下:原 App 需要 150s 左右整个编译完毕,然后开发人员才可以开始调试。而现在组件化之后,某个业务组件只需要 10s~20s 左右。在依赖二进制化组件的情况下,业务组件编译速度一般低于 10s。
  • 分工更为明确,从而提升开发效率。
  • 灵活,耦合低。
  • 结合 MVVM。非常细致的单元测试,提高代码质量,保证 App 稳定性。体现在测试人员反馈的 bug 明显减少。
  • 回滚更方便。我们经常会发生业务或者 UI 变回之前版本的情况,以前我们都是 checkout 出之前的代码。而现在组件化了之后,我们只需要使用旧版本的业务组件 Pod 库,或者在旧版本的基础上再发一个 Pod 库。
  • 新人更容易上手。

对于我来说:

  • 更加容易地把控代码质量。
  • 更加容易地知道小组成员做了些什么。
  • 更加容易地分配工作。
  • 更加容易地安排新成员。

解耦

我们的想法是这样的,就算最后做不成组件化开发,把这些应该重用的代码抽出来做成 Pod 库也没有什么影响。所以优先做了这一步。

哪些东西需要抽成 Pod 库?

我们之前的 App 已经使用了模块化(CocoaPods 化)开发。我们已经把会在 App 之间重用的 Util、Category、网络层和本地存储等等这些东西抽成了 Pod 库。还有些一些和业务相关的,比如 YTXChart,YTXChartSocket;这些也是在各个 App 之间重用的。

所以得出一个很简单的结论:要在 App 之间共享的代码就应该抽成 Pod 库,把它们作为一个个组件。

我们去仔细查看了原 App 代码,发现很多东西都需要重用而我们却没有把它们组件化。

为什么没有把这些代码组件化?

因为当时没想好怎么解耦,举个例子。

有一个类叫做 YTXAnalytics。是依赖 UMengAnalytics 来做统计的。 它的耦合是在于一个方法。这个方法是用来收集信息的。它依赖了 User,还依赖了 currentServerId 这个东西。

复制代码
+ (NSDictionary*)collectEventInfo:(NSString*)event withData:(NSDictionary*)data
{
.......
return @{
@"event" : event,
@"eventType" : @"event",
@"time" : [[[NSDate date] timeIntervalSince1970InMillionSecond] stringValue],
@"os" : device.systemName,
@"osVersion" : device.systemVersion,
@"device" : device.model,
@"screen" : screenStr,
@"network" : [YTXAnalytics networkType],
@"appVersion" : [AppInfo appVersion],
@"channel" : [AppInfo marketId],
@"deviceId" : [ASIdentifierManager sharedManager].advertisingIdentifier.UUIDString,
@"username" : objectOrNull([YTXUserManager sharedManager].currentUser.username),
@"userType" : objectOrNull([[YTXUserManager sharedManager].currentUser.userType stringValue]),
@"company" : [[ServiceProvider sharedServiceProvider].currentServerId stringValue],
@"ip" : objectOrNull([SSNetworkInfo currentIPAddress]),
@"data" : jsonStr
};
}

解决方案是,搞了一个 block,把获取这些信息的责任丢出来。

复制代码
[YTXAnalytics sharedAnalytics].analyticsDataBlock = ^ NSDictionary *() {
return @{
@"appVersion" : objectOrNull([PBBasicProviderModule appVersion]),
@"channel" : objectOrNull([PBBasicProviderModule marketId]),
@"username" : objectOrNull([PBUserManager shared].currentUser.username),
@"userType" : objectOrNull([PBUserManager shared].currentUser.userType),
@"company" : objectOrNull([PBUserManager shared].currentUser.serverId),
@"ip" : objectOrNull([SSNetworkInfo currentIPAddress])
};
};

我们的耦合大多数都是这种。解决方案都是弄了一个 block,把获取信息的职责丢出来到外面。

我们解耦的方式就是以下几种:

  1. 把它依赖的代码先做成一个 Pod 库,然后转而依赖 Pod 库。有点像是“依赖下沉”。
  2. 使用 category 的方式把依赖改成组合的方式。
  3. 使用一个 block 或 delegate(协议)把这部分职责丢出去。
  4. 直接 copy 代码。copy 代码这个事情看起来很不优雅,但是它的好处就是快。对于一些不重要的工具方法,也可以直接 copy 到内部来用。

初始化

AppDelegate 充斥着各种初始化。 比如我们自己的代码。已经只是截取了部分!

复制代码
[self setupScreenShowManager];
//event start
[YTXAnalytics createYtxanalyticsTable];
[YTXAnalytics start];
[YTXAnalytics page:APP_OPEN];
[YTXAnalytics sharedAnalytics].analyticsDataBlock = ^ NSDictionary *() {
return @{
@"appVersion" : objectOrNull([AppInfo appVersion]),
.......
@"ip" : objectOrNull([SSNetworkInfo currentIPAddress]),
};
};
[self registerPreloadConfig];
//Migrate UserDefault 转移 standardUserDefault 到 group
[NSUserDefaults migrateOldUserDefaultToGroup];
[ServiceProvider sharedServiceProvider];
[YTXChatManager sharedYTXChatManager];
[ChartSocketManager sharedChartSocketController].delegate = [ChartProvider sharedChartProvider];
// 初始化最初的行情集合
[[ChartProvider sharedChartProvider] addMetalList:[ChartSocketManager sharedChartSocketController].quoteList];
// 初始化环信信息 Manager
[YTXEaseMobManager sharedManager];

比如第三方:

复制代码
// 注册环信
[self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions];
//Talking Data
[self setupTalkingData];
[self setupAdTalkingData];
[self setupShareSDK];
[self setupUmeng];
[self setupJSPatch];
[self setupAdhocSDK];
[YTXGdtAnalytics communicateWithGdt];// 广点通

首先这些初始化的东西是会被各个业务组件都用到的。

那我组件化开发的时候,每一个业务组件如何保证我使用这些东西的时候已经初始化过了呢?难道每一个业务组件都初始化一遍?有参数怎么办,能不能使用单例?

但问题是第三方库基本都需要注册一个 AppKey,我每一个业务组件里都写一份?那样肯定不好,那我配置在主 App 里的 info.plist 里面,每一个业务组件都初始化一下好了,也不会有什么副作用。但这样感觉不优雅,而且有很多重复代码。万一某个 AppKey 或重要参数改了,那每一个业务组件岂不是都得改了。这样肯定不行。另外一点,那我的业务组件必须依赖主 App 的内容了。无论是在主 App 里调试还是把主 App 的 info.plist 的相关内容拷贝过来使用。

更关键的是有一些第三方的库需要在 application: didFinishLaunchingWithOptions: 时初始化。

复制代码
// 初始化环信,shareSDK, 友盟, Talking Data 等
[self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions];

有没有更好的办法呢?

首先我写了一个 YTXModule 。它利用 runtime,不需要在 AppDelegate 中添加任何代码,就可以捕获 App 生命周期。

在某个想获得 App 生命周期的类中的.m 中这样使用:

复制代码
YTXMODULE_EXTERN()
{
// 相当于 load
isLoad = YES;
}
+ (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
// 实现一样的方法名,但是必须是静态方法。
return YES;
}

分层

因为在解决初始化问题的时候,要先设计好层级结构。所以这里突然跳转到分层。

上个图:

我们自己定了几个原则。

  • 业务组件之间不能有依赖关系。
  • 按照图示不能跨层依赖。
  • 所谓弱业务组件就是包含着少部分业务,并且可以在这个 App 内的各个业务组件之间重用的代码。
  • 要依赖 YTXModule 的组件一定要以 Module 结尾,而且它一定是个业务组件或是弱业务组件。
  • 弱业务组件以 App 代号开头(比如 PB),以 Module 结尾。例:PBBasicProviderModule。
  • 业务组件以 App 代号开头(比如 PB)BusinessModule 结尾。例:PBHomePageBusinessModule。

业务组件之间不能有依赖关系,这是公认的的原则。否则就失去了组件化开发的核心价值。

弱业务组件之间也不应当有依赖关系。如果有依赖关系说明你的功能划分不准确。

初始化

我们约定好了层级结构,明确了职责之后。我们就可以跳回初始化的设计了。

创建一个 PBBasicProviderModule 弱业务组件。

  • 它通过依赖 YTXModule 来捕捉 App 生命周期。
  • 它来负责初始化自己的和第三方的东西。
  • 所有业务组件都可以依赖这个弱业务组件。
  • 它来保证所有东西一定是是初始化完毕的。
  • 它来统一管理。
  • 它来暴露一些类和功能给业务组件使用。

反正就是业务组件中依赖 PBBasicProviderModule,它保证它里面的所有东西都是好用的。

因为有了 PBBasicProviderModule,所以才让我更明确了弱业务组件这个概念。

因为我们懒,如果把 PBBasicProvider 定义为业务组件。那它和其他业务组件之间的通信就必须通过 Bus、Notification 或协议等等。

但它又肯定是业务啊。因为那些 AppKey 肯定是和这个 App 有关系的,也就是 App 的相关配置和参数也可以说是业务;我需要初始化设置那些 Block 依赖 User 信息、CurrentServerId 等等肯定都是业务啊。

那只好搞个弱业务出来啊。因为我不能打破这个原则啊:业务组件之间不能互相依赖。

再进一步分清弱业务组件和业务组件。

业务组件里面基本都有:storyboard、nib、图片等等。弱业务组件里面一般没有。这不是绝对的,但一般情况是这样。

业务组件一般都是 App 上某一具体业务。比如首页、我、直播、行情详情、XX 交易大盘、YY 交易大盘、XX 交易中盘、资讯、发现等等。而弱业务组件是给这些业务组件提供功能的,自己不直接表现在 App 上展示。

我们还可以创建一些弱业务组件给业务组件提供功能。当然了,不能够滥用。需要准确划分职责。

最后,代码大概是这样的:

复制代码
@implementation PBBasicProviderModule
YTXMODULE_EXTERN()
{
}
+ (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
[self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions];
[self setupBasic:application didFinishLaunchingWithOptions:launchOptions];
return YES;
}
+ (void) setupThirdParty:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions];
[self setupTalkingData];
[self setupAdTalkingData];
[self setupShareSDK];
[self setupJSPatch];
[self setupUmeng];
// [self setupAdhoc];
});
}
+ (void) setupBasic:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self registerBasic];
[self autoIncrementOpenAppCount];
[self setupScreenShowManager];
[self setupYTXAnalytics];
[self setupRemoteHook];
}
+ (YTXAnalytics) sharedYTXAnalytics
{
return ......;
}
......

设想

这个 PBBasicProviderModule 简直就是个大杂烩啊,把很多以前写在 AppDelegate 里的东西都丢在里面了。毫无优雅可言。

的确是这样的,感觉没有更好的办法了。

既然已经这样了。我们可不可以大胆地设想一下:每个开发者开发自己负责的业务组件的时候不需要关心主 App。

因为我知道美团的组件化开发必须依赖主 App 的 AppDelegate 的一大堆设置和初始化。所以干脆他们就直接在主 App 中集成调试,他们通过二进制化和去 Pod 依赖化的方式让主 App 的构建非常快。

所以我们是不是可以继续污染这个 PBBasicProviderModule。不需要在主 App 项目里的 AppDelegate 写任何初始化代码?基本或者尽量不在主 App 里写任何代码?改依赖主 App 变为依赖这个弱业务组件?

按照这个思路我们搬空了 AppDelegate 里的所有代码。比如一些初始化 App 样式的东西、初始化 RootViewController 等等这些都可以搬到一个新的弱业务组件里。

而业务组件其实根本不需关心这个弱业务组件,开发人员只需要在业务组件中的 Example App 中的 AppDelegate 中初始化自己业务组件的 RootViewController 就好了。

其他的事情交给这个新的弱业务组件就好了。而主 App 和 Example App 只要在 Podfile 中依赖它就好了。

所以最后的设想就是:开发者不会去改主 App 项目,也不需要知道主 App 项目。对于开发者来说,主 App 和业务组件之间是隔绝的。

有一个更大的好处,我只要更换这个弱业务组件,这个业务组件就能马上适配一个新 App。这也是某种意义上的解耦。

Debug/Release

谁说不用在主 App 里的 AppDelegate 写任何代码的,打脸。。。

我们在对二进制 Pod 库跑测试的发现,源码能过,二进制 (.a) 不能过。百思不得其解,然后仔细查看代码,发现是这个宏的锅:

复制代码
#ifdef DEBUG
#endif

DEBUG 在编译阶段就已经决定了。二进制化的时候已经编译完成了。 而我们的代码中充满着#ifdef DEBUG 就这样这样。那怎么办,这是二进制化的锅。但是我们的二进制化已经形成了标准,大家都自觉会这么做,怎么解决这个问题呢。

解决方案是:

创建了一个 PBEnvironmentProvider。大家都去依赖它。

然后原来判断宏的代码改成这样:

复制代码
if([PBEnvironmentProvider testing])
{
//...
}

在主 App 的 AppDelegate 中这样:

复制代码
#if DEBUG && TESTING
//PBEnvironmentProvider 提供的宏
CONFIG_ENVIRONMENT_TESTING
#endif

原理是:如果 AppDelegate 有某个方法(CONFIG_ENVIRONMENT_TESTING 宏会提供这个方法),[PBEnvironmentProvider testing] 得到的结果就是 YES。

为什么要写在主 App 里呢?其实也可以丢在 PBBasicProviderModule 里面,提供一个方法啊。

因为主 App 的 AppDelegate.m 是源码,未经编译。另外注意 TESTING 这个宏。我们可以在 xcode 设置里加一个 macro 参数 TESTING,并且修改为 0 的情况下,能够生成一个实际是 DEBUG 的 App 但里面内容却是线上的内容。

这个需求是来自于我们经常需要紧急通过 xcode 直接 build 一个 app 到手机上以解决或确认线上的问题。

虽然打脸了,但是也还好,以后也不用改了。再说这个是特殊需求。除了这个之外,主 App 没有其他代码了。

业务组件间通信

我们解决了初始化和解耦的问题。接下来只要解决组件间通信的问题就好了。

然后我找了几个第三方库,选用了 MGJRouter 。本来直接依赖它就好了。

后来觉得都使用 Block 的方式会导致这样的代码,全部堆在了一个方法里:

复制代码
+ (void) setupRouter
{
......
[MGJRouter registerURLPattern:@"mgj://foo/a" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];
[MGJRouter registerURLPattern:@"mgj://foo/b" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];
......
}

这样感觉很不爽。那我干脆就把 MGJRouter 代码复制了下来,把 Block 改成了 @selector。并且把它直接加入了 YTXModule 里面。并且使用了宏,让结果看起来优雅些。代码看起来是这样的:

复制代码
// 在某个类的.m 里,其实并不需要继承 YTXModule 也可以使用该功能
YTXMODULE_EXTERN_ROUTER_OBJECT_METHOD(@"object1")
{
YTXMODULE_EXAPAND_PARAMETERS(parameters)
NSLog(@"%@ %@", userInfo, completion);
isCallRouterObjectMacro2 = YES;
return @" 我是个类型 ";
}
YTXMODULE_EXTERN_ROUTER_METHOD(@"YTX://QUERY/:query")
{
YTXMODULE_EXAPAND_PARAMETERS(parameters)
NSLog(@"%@ %@", userInfo, completion);
testQueryStringQueryValue = parameters[@"query"];;
testQueryStringNameValue = parameters[@"name"];
testQueryStringAgeValue = parameters[@"age"];
}

调用的时候看起来是这样的:

复制代码
[YTXModule openURL:@"YTX://QUERY/query?age=18&name=CJ" withUserInfo:@{@"Test":@1} completion:nil];
NSString * testObject2 = [YTXModule objectForURL:@"object1" withUserInfo:@{@"Test":@2}];

通信问题解决了。其实页面跳转问题也解决了。

页面跳转

页面跳转解决方案与业务组件之间通信问题是一样的。

但是需要注意的是,你一个业务组件内部的页面跳转也请使用 URL+Router 的方式跳转,而不要自己直接 pushViewController。

这样的好处是:如果将来某些内部跳转页面需要给其他业务组件调用,你就不需要再注册个 URL 了。因为本来就有。

是否去 Model 化

去 Model 化主要体现在业务组件间通信,要不要传一个 Model 过去 (传过去的 Dictionary 中的某个键是 Model)。

如果去 Model 化,这个业务组件的开发者如何确定 Dictionary 里面有哪些内容分别是什么类型呢?那需要有个地方传播这些信息,比如写在头文件,wiki 等等。

如果不去 Model 化的话,就需要把这个 Model 做成 Pod 库。两个业务组件都去依赖它。

最后决定不去 Model。因为实际上有一些 Model 就是在各个业务组件之间公用的(比如 User),所以肯定就会有 Model 做成 Pod 库。我们可以把它做成重 Model,Model 里可以带网络请求和本地存储的方法。唯一不能避免的问题是,两个业务组件的开发者都有可能去改这个 Model 的 Pod 库。

信息的披露

跳转的页面需要传哪些参数? 业务组件之间传递数据时候本质的载体是什么?

不同业务开发者如何知晓这些信息。

使用去 Model 化和不使用去 Model 化,我们都有各自的方案。

去 Model 化,则披露头文件,在头文件里面写详细的注释。

如果不去 Model 化,则就看 Model 就可以了。如有特殊情况,那也是文档写在头文件内。

总结的话:信息披露的方式就是把注释文档写在头文件内。

组件的生命周期

业务组件的生命周期和 App 一样。它本身就是个类,只暴露类方法,不存在需要实例,所以其实不存在生命周期这个概念。而它可以使用类方法创建很多 ViewController,ViewController 的生命周期由 App 管理。哪怕这些 ViewController 之间需要通信,你也可以使用 Bus/YTXModule/ 协议等等方式来做,而不应该让业务组件这个类来负责他们之间的通信;也不应该自己持有 ViewController;这样增加了耦合。

弱业务组件的生命周期由创建它的对象来管理。按需创建和 ARC 自动释放。

基础功能组件和第三方的生命周期由创建它的对象来管理。按需创建和 ARC 自动释放。

版本规范

我们自己定的规则。

所有 Pod 库都只依赖到 minor

复制代码
"~> 2.3"

主 App 中精确依赖到 patch

复制代码
"2.3.1"

主 App 中的业务组件版本号的 Main.Minor 要和主 App 版本保持一致。

参考: Semantic Versioning RubyGems Versioning Policies

二进制化

二进制化我认为是必须的,能够加快开发速度。

而我使用的这个二进制方案

有个坑就是在gitlab-runner 上在二进制和源码切换时,经常需要pod cache clean --all,test/lint/publish 才能成功。而每次pod cache clean --all 之后CocoaPods 会去重新下载相关的pod 库,增加了时间和不必要的开销。

我们现在通过podspec 中增加preserve_paths 和执行download_zip.sh 解决了cache 的问题。原理是让pod cache 既有源码又有二进制.a。具体可以看ytx-pod-template 项目中的 Name.podspec download_zip.sh

二进制化还得注意宏的问题。小心使用宏,尤其是#ifdef。避免源码和二进制代码运行的结果不一样。

集成调试

集成调试很简单。每一个业务组件在自己的 Example App 中调试。

这个业务组件的 podspec 只要写清楚自己依赖的库有哪些。剩下的其他业务组件应该写在 Example App 的 Podfile 里面。

依赖的 Pod 库都是二进制的。如有问题可以装源码 (IS_SOURCE=1 pod install) 来调试。

开发人员其实只需要关心自己的业务组件,这个业务组件是自洽的。

公共库谁来维护的问题

这个问题在我们这种小 Team 不存在。没有仔细地去想过。但是只要做好代码准入(Test/Lint/Code Review)和权限管理就应该不会存在大的问题。

单元测试

单元测试我们用的是 Kiwi 。 结合 MVVM 模式,对每一个业务组件的 ViewModel 都进行单元测试。每次 push 代码,gitlab-runner 都会自动跑测试。一旦开发人员发现测试挂了就能够及时找到问题。也可以很容易的追溯哪次提交把测试跑挂了。

这也是我们团队的强制要求。没有测试,测试写的不好,测试挂了,直接拒绝 merge request。

lint

对每一个组件进行 lint 再发布,保证了正确性。这也是一步强制要求。

lint 的时候能够发现很多问题。通常情况下不允许 warning 出现的。如果不能避免(比如第三方)请用–allow-warnings。

复制代码
pod lib lint --sources=$SOURCES --verbose --fail-fast --use-libraries

统一的网络服务和本地存储方式

这个就很简单。把这两个部分抽象成几个 Pod 库供所有业务组件使用就好了。 我们这边分别是三个 Pod 库:

  • YTXRequest
  • YTXRestfulModel
  • NSUserDefault+YTX

其他一些内容

ignore 了主 App 中的 Podfile.lock 尽量避免冲突。

主 App Archive 的时候要使用源码,而不是二进制。

后期可以使用 oclint 和 deploymate 检查代码。

使用 fastlane match 去维护开发证书。

一些需要从 plist 或者 json 读取配置的 Pod 库模块,要注意读出来的内容最好要加一个 namespace。namespace 可以是这个业务组件的名字。

业务组件读取资源文件的区别

复制代码
#从 main bundle 中取。如果图片希望在 storyboard 中被找到,使用这种方式。
s.resource = ["#{s.name}/Assets/**"]
#只是希望在我这个业务组件的 bundle 内使用的 plist。作为配置文件。这是官方推荐方式。
s.resource_bundles = {
"{s.name}/" => ["{s.name}/Assets/config.plist"]
}

持续集成

原来的 App 就是持续集成的。想当然的,我们希望新的组件化开发的 App 也能够持续集成。

Podfile 应该是这样的:这里面出现的全是私有 Pod 库。

复制代码
pod 'YTXRequest', '2.0.1'
pod 'YTXUtilCategory', '1.6.0'
pod 'PBBasicProviderModule', '0.2.1'
pod 'PBBasicChartAndSocketModule', '0.3.1'
pod 'PBBasicAppInitModule', '0.5.1'
...
pod 'PBBasicHomepageBusinessModule', '1.2.15'
pod 'PBBasicMeBusinessModule', '1.2.10'
pod 'PBBasicLiveBusinessModule', '1.2.1'
pod 'PBBasicChartBusinessModule', '1.2.6'
pod 'PBBasicTradeBusinessModule', '1.2.7'
...

如果 Pod 依赖的东西特别特别多,比如 100 多个。另外又必须依赖主 App 做集成调试。 你也可以用这种方案:把你所有的 Pod 库的依赖都展开写到主 App 的 Podfile 中。而发布 Pod 库时 podspec 中不带任何的依赖的。这样就避免了 pod install 的时候解析依赖特别耗时的问题。

各个脚本都在这个 ytx-pod-template 。先从.gitlab-ci.yml 看起。

我们持续集成的工具是 gitlab runner。

持续集成的整个流程是:

第一步:

使用 template 创建 Pod。像这样:

复制代码
pod lib create <Pod 库名称 > --template-url="http://gitlab.baidao.com/pods/ytx-pod-template"

第二步:

创建 dev 分支。用来开发。

第三步:

每次 push dev 的时候会触发 runner 自动跑 Stage: Init Lint(中的 test)

第四步:

1. 准备发布 Pod 库。修改 podspec 的版本号,打上相应 tag。 2. 使用 merge_request.sh 向 master 提交一个 merge request。

第五步:

1. 其他有权限开发者 code review 之后,接受 merge request。 2.master 合并这个 merge request 3.master 触发 runner 自动跑 Stage: Init Package Lint ReleasePod UpdateApp

第六步:

如果第五步正确。主 App 的 dev 分支会收到一个 merge request,里面的内容是修改 Podfile。 图中内容出现了 AFNetworking 等是因为这个时候在做测试。

第七步:

主 App 触发 runner,会构建一个 ipa 自动上传到 fir

Init

  • 初始化一些环境。
  • 打印一些信息。

Package

  • 二进制化打包成.a

Lint

  • Pod lib lint。二进制和源码都 lint。
  • 测试。
  • 以后考虑加入 oclint 和 deploymate。

ReleasePod

  • 把相关文件 zip 后,传到静态服务器库。以提供二进制化下载包。
  • pod repo push。发布该 Pod 库。

ReleasePod 的时候不允许 Pod 库出现警告。

UpdateApp

  • 下载 App 代码
  • 修改 Podfile 文件。如果匹配到 pod 库文件名则修改,否则添加。
  • 生成一个 merge request 到主 App 的 dev 分支。

关于 gitlab runner。

stage 这个功能非常的厉害。强烈推荐。

每一个 stage 可以跑在不同的 runner 上。每一个 stage 失败了可以单独 retry。而某一个 stage 里面的任务可以并行执行:(test 和 lint 就是并行的)


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 9 月 25 日 17:3116365

评论

发布
暂无评论
发现更多内容

深入了解kafka系列-消费者

小技术君

kafka flink 消费 kafka Kafka知识点

哈希表

Arthur

一致哈希

Arvin

Java实现一致性 Hash 算法实现(训练营第五课)

看山是山

极客大学架构师训练营 一致性hash

分布式缓存一致性hash算法实现

考尔菲德

深入了解kafka系列-生产者

小技术君

kafka Kafka知识点

第5周 - 学习总结

大海

扯淡 Java 集合

CoderLi

Java 后端 hashmap 后台

「架构师训练营」学习笔记:第 5 周 技术选型

Amy

总结 极客大学架构师训练营 消息队列 分布式缓存 第五周

架构师训练营 第五周 学习总结

一雄

学习 极客大学架构师训练营 第五周

啃碎并发(四):Java线程Dump分析

猿灯塔

1. 初识Jackson -- 世界上最好的JSON库

YourBatman

Jackson Fastjson JSON库

领域模型为核心的架构设计 初篇

小隐乐乐

领域驱动设计 架构师

第五周总结-缓存、消息中间件、负载均衡器、分布式数据库

吴建中

极客大学架构师训练营

作业-05-java实现一致性hash算法

梦子说

极客大学架构师训练营

Jira feat. Confluence助力敏捷项目管理

YY哥-杨勇

架构师训练营 - 技术选型

Pontus

极客大学架构师训练营

技术选型之缓存、队列、负载均衡

olderwei

极客大学架构师训练营

架构师训练营」第 4 周作业

edd

架构设计篇之面向对象设计

小诚信驿站

架构 架构师 架构分析 刘晓成 架构演进

架构师训练营 - 学习笔记 - 第五周

心在飞

极客大学架构师训练营

第 5 周 - 课后作业

大海

架构师训练营 Week 05 总结

Wancho

架构师训练营第五周总结

方堃

极客大学架构师训练营

【架构师训练营 - 周总结 -5】

小动物

总结 极客大学架构师训练营 第五周

Week05 作业

极客大学架构师训练营

第05周 技术选型-01 学习总结

Jaye

一致性哈希算法实现及案例测试,java版

潜默闻雨

极客时间架构师训练营 - week5 - 作业 2

jjn0703

极客大学架构师训练营

第五周总结

考尔菲德

一致性hash算法的实现和平衡性测试

周冬辉

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

iOS App组件化开发实践-InfoQ