代碼提交更規範,分工更爲明確,質量提升html
編譯加快ios
在原模式中,須要 150s 左右整個編譯完畢,而後開發人員才能夠開始調試。而如今組件化以後,某個業務組件只須要 10s ~ 20s 左右便可開工git
結合 MVVMgithub
更加細化的單元測試,提升代碼質量,保證 App 穩定性編程
回滾更方便ruby
面對發生業務或者 UI 變回以前版本的狀況,之前咱們都是 checkout 出以前的代碼。而如今組件化了以後,咱們只須要使用舊版本的業務組件 Pod 庫,或者在舊版本的基礎上再發一個 Pod 庫即可。bash
上面都是忽悠你來看的,別當真 🤣網絡
最近一直在調研模塊化的相關知識,基本掌握了「初級封裝抽離」的水平,正在迷茫之際,遇大神指點迷津,探索出了後面的進階路線,心中默默感謝大神一刻鐘...架構
主要工做就是把 App 之間重用的 Util、Category、網絡層和本地存儲等抽成了 Pod 庫,因爲三方庫自帶必定的解耦性,對後期的組件化開發也比較有幫助。另外一方面工做好比Chart,ChartSocket這些功能在各個 App 之間重用的卻不會過於耦合,因此拆分難度也不會過高。app
這一級的抽離相對簡單,難點卻是對 cocopods 等工具的使用,目前的我組件化學習就只到這個水平,你們共同窗習!
相關組件化工具的使用參考:《使用 CocoaPods 對公有庫開源和私有庫組件》https://juejin.im/post/5ab21daaf265da239e4df64e
都是本身摸着石頭過河,有什麼不對的地方,你們探討哈~
以 Analytics 統計功能爲例,Analytics 是依賴 UMengAnalytics 來作統計的,用於收集數據的方法處理很差極易發生耦合,如既依賴了 User,還依賴了 currentServerId等。
應對 Analytics 這類狀況,網上資料有幾種方法來解耦:
對於解耦,網上相似的資料還有利用中間件 Mediator的方式:
應對上面的情景,最直接的方法就是增長一箇中間件,各個模塊跳轉經過中間件來管理。這樣,全部模塊只依賴這個中間件。
可是中間件怎麼去調用其餘模塊那?好吧,中間件又會依賴全部模塊。好像除了增長代碼的複雜度,並無真正解決任何問題。
有沒有一種方法,能夠完美的解決這個依賴關係那?
咱們但願作到:每一個模塊之間互相不依賴,而且每一個模塊能夠脫離工程由不一樣的人編寫、單獨編譯調試。
下面的方案經過對中間件的改造,很好的解決了這個問題,解決後的模塊間依賴關係以下:
實現方案 demo 源碼地址: https://github.com/zcsoft/ZC_CTMediator,搞來學習吧
目錄結構:
全部模塊的引用關係如圖:
因爲 demo 中只是從 ViewController.h.m 中跳轉到 DemoModule 模塊,因此只須要 ViewController.h.m 依賴 CTMediator,CTMediator 到 DemoModule 模塊的調用是使用運行時完成了(圖片中的藍線),在代碼中不須要相護依賴。
也就是說,若是一個模塊不須要跳轉到其餘模塊,就不須要依賴 CTMediator。
完整的內部調用關係圖:
響應過程:
1.ViewController 中判斷Cell選中
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
->
2.CTMediator+CTMediatorModuleAActions 中圖片加載響應方法
- (void)CTMediator_presentImage:(UIImage *)image;
->
3.CTMediator 中本地組件調用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
->
4.Target_A
- (id)Action_nativePresentImage:(NSDictionary *)params;
完成跳轉。
複製代碼
事件響應斷點:
<1>CTMediator.h.m 功能: 指定目標(target,類名)+動做(action,方法名),並提供一個字典類型的參數。
CTMediator.h.m 會判斷 target-action 是否能夠調用,若是能夠,則調用。因爲這一功能是經過 runtime 動態實現的,因此在 CTMediator.h.m 的實現中,不會依賴任何其餘模塊,也不須要知道 target-action 的具體功能,只要 target-action 存在,就會被執行 target-action 具體的功能由 DemoModule 本身負責)。
CTMediator.h 裏實際提供了兩個方法,分別處理 url 方式的調用和 target-action 方式的調用,其中,若是使用 url 方式,會自動把 url 轉換成 target-action。
<2>CTMediator+CTMediatorModuleAActions.h.m 功能:CTMediator 的擴展,用於管理跳轉到 DemoModule 模塊的動做。其餘模塊想要跳轉到 DemoModule 模塊時,經過調用這個類的方法來實現。
可是這個類中,並不真正去作跳轉的動做,它只是對 CTMediator.h.m類的封裝,這樣用戶就不須要關心使用CTMediator.h.m跳轉到DemoModule模塊時具體須要的target名稱和action名稱了。
<3>‘CTMediator.h.m’+‘CTMediator+CTMediatorModuleAActions.h.m’ 共同組成了一個面相 DemoModule 的跳轉,而且它不會在代碼上依賴 DemoModule,DemoModule 是否提供了相應的跳轉功能,只體如今運行時是否可以正常跳轉。
至此,CTMediator 這個中間層實現了徹底的獨立,其餘模塊不須要預先註冊,CTMediator也不須要知道其餘模塊的實現細節。惟一的關聯就是須要在 ‘CTMediator+CTMediatorModuleAActions.h.m’ 中寫明正確的 target+action 和正確的參數,並且這些 action 和參數只依賴於 Target_A.h.m。
action 和參數的正確性只會在運行時檢查,若是 target 或 action 不存在,能夠在 ‘CTMediator.h.m’ 中進行相應的處理。既:CTMediator 不須要依賴任何模塊就能夠編譯運行。
<4>Target_A.h.m 提供了跳轉到 DemoModule 模塊的對外接口,與 CTMediator+CTMediatorModuleAActions.h.m 相互對應,能夠說它只用來爲 CTMediator+CTMediatorModuleAActions.h.m 提供服務,因此在實現 CTMediator+CTMediatorModuleAActions.h.m時只須要參考 TargetA.h.m 便可,足夠簡單以致於並不須要文檔來輔助描述。其餘模塊想跳轉到這個模塊時,不能直接經過 Target_A.h.m 實現,而是要經過 CTMediator+CTMediatorModuleAActions.h.m 來完成。
這樣,就實現了模塊間相互不依賴,而且只有須要跳轉到其餘模塊的地方,才須要依賴 CTMediator。
<5>DemoModuleADetailViewController.h.m DemoModule 模塊的主視圖,這個例子中,會從 ViewController.h.m 跳轉到這個模塊。
<6>AppDelegate.h.m APP 入口,從應用外經過 Scheme 跳入程序時會通過這個類。
<7>ViewController.h.m APP 主視圖,須要在這裏跳轉到 DemoModule 模塊。
AppDelegate 充斥着各類初始化和第三方的註冊,這些初始化會被各個業務組件使用,並且第三方庫基本都須要註冊一個 AppKey ,特別是一些第三方的庫須要在 application: didFinishLaunchingWithOptions: 時初始化。
面對這種高難度的耦合場景,我想到了一個基於 runtime 的 AOP 解決方案。
關於AOP的簡單介紹參考: 《基於 Aspects 簡單展現 AOP 面向切面編程(中英文)》https://juejin.im/post/5a7abf495188257a61322204
原理就是利用 runtime,不須要在 AppDelegate 中添加任何代碼,就能夠捕獲 App 生命週期,具體的解決方案還有待探討。
這裏引用《iOS App組件化開發實踐》的解決方案,經過建立一個 PBBasicProviderModule 弱業務組件:
業務組件裏面基本都有: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 ......;
}
......
複製代碼
《iOS App組件化開發實踐》介紹的層級結構設計圖:
《iOS App組件化開發實踐》推行的組件化規範:
因爲引入PBBasicProviderModule解決AppDelegate中的各類問題,會致使PBBasicProviderModule體量激增,如下是《iOS 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和業務組件之間是隔絕的。
上面這些表示一臉懵逼,來源下面有地址,你們自行理解。
在對二進制Pod庫跑測試的發現,源碼能過,二進制(.a)不能過。 問題源頭(這是二進制化的鍋):
#ifdef DEBUG
#endif
複製代碼
因爲DEBUG在編譯階段就已經決定了。二進制化的時候已經編譯完成了。
解決方案:
建立了一個 PBEnvironmentProvider 你們都去依賴它。
而後原來判斷宏的代碼改爲這樣:
if([PBEnvironmentProvider testing])
{
//...
}
複製代碼
在主App的AppDelegate中這樣:
#if DEBUG && TESTING
//PBEnvironmentProvider提供的宏
CONFIG_ENVIRONMENT_TESTING
#endif
複製代碼
原理是: 若是AppDelegate有某個方法(CONFIG_ENVIRONMENT_TESTING宏會提供這個方法),[PBEnvironmentProvider testing]獲得的結果就是YES。
1)3D-Touch功能或者點擊推送消息,要求外部跳轉到App內部一個很深層次的一個界面。
2)自家的一系列App之間如何相互跳轉?
3)如何解除App組件之間和App頁面之間的耦合性?
4)如何能統一iOS和Android兩端的頁面跳轉邏輯?甚至如何能統一三端的請求資源的方式?
5)若是使用了動態下發配置文件來配置App的跳轉邏輯,那麼若是作到iOS和Android兩邊只要共用一套配置文件?
6)若是App出現bug了,如何不用JSPatch,就能作到簡單的熱修復功能?
7)如何在每一個組件間調用和頁面跳轉時都進行埋點統計?每一個跳轉的地方都手寫代碼埋點?利用Runtime AOP ?
8)如何在每一個組件間調用的過程當中,加入調用的邏輯檢查,令牌機制,配合灰度進行風控邏輯?
9)如何在App任何界面均可以調用同一個界面或者同一個組件?只能在AppDelegate裏面註冊單例來實現?
1)URL Scheme方式 2)Universal Links方式
關於JLRoutes簡單介紹:《iOS 模塊化之 JLRoute 路由示例 (中英文)》(https://github.com/ReverseScale/JLRouteDemo)
頁面跳轉解決方案與業務組件之間通訊問題是同樣的。
可是須要注意的是,你一個業務組件內部的頁面跳轉也請使用URL+Router的方式跳轉,而不要本身直接pushViewController。
這樣的好處是:若是未來某些內部跳轉頁面須要給其餘業務組件調用,你就不須要再註冊個URL了。由於原本就有。
去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(https://semver.org), RubyGems Versioning Policies(http://guides.rubygems.org/patterns/#semantic-versioning)
單元測試咱們用的是 Kiwi 。 結合MVVM模式,對每個業務組件的ViewModel都進行單元測試。每次push代碼,gitlab-runner都會自動跑測試。一旦開發人員發現測試掛了就可以及時找到問題。也能夠很容易的追溯哪次提交把測試跑掛了。
原來的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'
...
複製代碼
持續集成(工具: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 。
以上注意內容來自:https://blog.csdn.net/u013602835/article/details/52668894,還沒機會實踐,先存着
本文整理內容參考瞭如下文章,在此對原做者們表示感謝:
《iOS應用架構談 組件化方案》(https://casatwy.com/iOS-Modulization.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io)
《路由設計思路分析》(https://juejin.im/post/58b2aad6b123db0052cc9edd)
《iOS 組件化方案探索》(http://blog.cnbang.net/tech/3080/)
《iOS App組件化開發實踐》(http://www.infoq.com/cn/articles/ios-app-component-development-practice)
《iOS 業務模塊間互相跳轉的解耦方案》(https://blog.csdn.net/cuibo1123/article/details/51017376)