VIA:infoQ 架构

前因
其实我们这个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,把获取信息的职责丢出来到外面。
    我们解耦的方式就是以下几种:
    把它依赖的代码先做成一个Pod库,然后转而依赖Pod库。有点像是“依赖下沉”。

使用category的方式把依赖改成组合的方式。

使用一个block或delegate(协议)把这部分职责丢出去。

直接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 PBBasicProviderModuleYTXMODULE_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.podspecdownload_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就是并行的)


感谢徐川对本文的审校。

推荐阅读更多精彩内容