前因
其實我們這個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"
參考: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就是并行的)