iOS 模組化進階整理記錄

WhatsXie發表於2018-04-11

先說模組化可能給專案帶來的改變:

  • 程式碼提交更規範,分工更為明確,質量提高

  • 編譯加快

    在原模式中,需要 150s 左右整個編譯完畢,然後開發人員才可以開始除錯。而現在元件化之後,某個業務元件只需要 10s ~ 20s 左右即可開工

  • 結合 MVVM

    更加細化的單元測試,提高程式碼質量,保證 App 穩定性

  • 回滾更方便

    面對發生業務或者 UI 變回之前版本的情況,以前我們都是 checkout 出之前的程式碼。而現在元件化了之後,我們只需要使用舊版本的業務元件 Pod 庫,或者在舊版本的基礎上再發一個 Pod 庫便可。

  • 上面都是忽悠你來看的,別當真 ?

模組化抽離

最近一直在調研模組化的相關知識,基本掌握了“初級封裝抽離”的水平,正在迷茫之際,遇大神指點迷津,探索出了後面的進階路線,心中默默感謝大神一刻鐘...

1)初級封裝抽離:

主要工作就是把 App 之間重用的 Util、Category、網路層和本地儲存等抽成了 Pod 庫,由於三方庫自帶一定的解耦性,對後期的元件化開發也比較有幫助。另一方面工作比如Chart,ChartSocket這些功能在各個 App 之間重用的卻不會過於耦合,所以拆分難度也不會太高。

這一級的抽離相對簡單,難點倒是對 cocopods 等工具的使用,目前的我元件化學習就只到這個水平,大家共同學習!

相關元件化工具的使用參考:《使用 CocoaPods 對公有庫開源和私有庫元件》https://juejin.im/post/5ab21daaf265da239e4df64e

都是自己摸著石頭過河,有什麼不對的地方,大家探討哈~

2)中級解耦抽離:

以 Analytics 統計功能為例,Analytics 是依賴 UMengAnalytics 來做統計的,用於收集資料的方法處理不好極易發生耦合,如既依賴了 User,還依賴了 currentServerId等。

應對 Analytics 這類情況,網上資料有幾種方法來解耦:

  • 1.把它依賴的程式碼先做成一個 Pod 庫,然後轉而依賴 Pod 庫。有點像是“依賴下沉”。
  • 2.使用 category 的方式把依賴改成組合的方式。
  • 3.使用一個 block 或 delegate(協議)把這部分職責丟出去。
  • 4.直接 copy 程式碼,其實我首先想到的就是這個 ?,copy 程式碼這個事情看起來很不優雅,但是它的好處就是快。對於一些不重要的工具方法,也可以直接 copy 到內部來用。

對於解耦,網上類似的資料還有利用中介軟體 Mediator的方式:

iOS 模組化進階整理記錄

應對上面的情景,最直接的方法就是增加一箇中介軟體,各個模組跳轉通過中介軟體來管理。這樣,所有模組只依賴這個中介軟體。

但是中介軟體怎麼去呼叫其他模組那?好吧,中介軟體又會依賴所有模組。好像除了增加程式碼的複雜度,並沒有真正解決任何問題。

iOS 模組化進階整理記錄

有沒有一種方法,可以完美的解決這個依賴關係那?

我們希望做到:每個模組之間互相不依賴,並且每個模組可以脫離工程由不同的人編寫、單獨編譯除錯。

下面的方案通過對中介軟體的改造,很好的解決了這個問題,解決後的模組間依賴關係如下:

iOS 模組化進階整理記錄

實現方案 demo 原始碼地址: https://github.com/zcsoft/ZC_CTMediator,搞來學習吧

目錄結構:

iOS 模組化進階整理記錄

所有模組的引用關係如圖:

iOS 模組化進階整理記錄

由於 demo 中只是從 ViewController.h.m 中跳轉到 DemoModule 模組,所以只需要 ViewController.h.m 依賴 CTMediator,CTMediator 到 DemoModule 模組的呼叫是使用執行時完成了(圖片中的藍線),在程式碼中不需要相護依賴。

也就是說,如果一個模組不需要跳轉到其他模組,就不需要依賴 CTMediator。

完整的內部呼叫關係圖:

iOS 模組化進階整理記錄

響應過程:

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;
完成跳轉。
複製程式碼

事件響應斷點:

iOS 模組化進階整理記錄

Demo 中各個檔案功用說明:

<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 模組。

3)高階初始化抽離:

AppDelegate 充斥著各種初始化和第三方的註冊,這些初始化會被各個業務元件使用,而且第三方庫基本都需要註冊一個 AppKey ,特別是一些第三方的庫需要在 application: didFinishLaunchingWithOptions: 時初始化。

面對這種高難度的耦合場景,我想到了一個基於 runtime 的 AOP 解決方案。

關於AOP的簡單介紹參考: 《基於 Aspects 簡單展示 AOP 面向切面程式設計(中英文)》https://juejin.im/post/5a7abf495188257a61322204

原理就是利用 runtime,不需要在 AppDelegate 中新增任何程式碼,就可以捕獲 App 生命週期,具體的解決方案還有待探討。

這裡引用《iOS App元件化開發實踐》的解決方案,通過建立一個 PBBasicProviderModule 弱業務元件:

  • 它通過依賴YTXModule來捕捉App生命週期。
  • 它來負責初始化自己的和第三方的東西。
  • 所有業務元件都可以依賴這個弱業務元件。
  • 它來保證所有東西一定是是初始化完畢的。
  • 它來統一管理。
  • 它來暴露一些類和功能給業務元件使用。

什麼是業務元件和弱業務元件?

業務元件裡面基本都有: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 模組化進階整理記錄

《iOS App元件化開發實踐》推行的元件化規範:

  • 業務元件之間不能有依賴關係。
  • 按照圖示不能跨層依賴。
  • 所謂弱業務元件就是包含著少部分業務,並且可以在這個App內的各個業務元件之間重用的程式碼。
  • 要依賴YTXModule的元件一定要以Module結尾,而且它一定是個業務元件或是弱業務元件。
  • 弱業務元件以App代號開頭(比如PB),以Module結尾。例:PBBasicProviderModule。
  • 業務元件以App代號開頭(比如PB)BusinessModule結尾。例:PBHomePageBusinessModule。
  • 業務元件之間不能有依賴關係,這是公認的的原則。否則就失去了元件化開發的核心價值。

由於引入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和業務元件之間是隔絕的。

上面這些表示一臉懵逼,來源下面有地址,大家自行理解。

坑點之 Debug/Release:

在對二進位制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。

業務元件間通訊

App路由能解決哪些問題:

iOS 模組化進階整理記錄

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裡面註冊單例來實現?

App之間跳轉實現

1)URL Scheme方式 2)Universal Links方式

元件間通訊的三方庫支援也有許多如:

  • 1.主流的有類似JLRoutes,主打通過URL跳轉協議(https://github.com/joeldev/JLRoutes)
  • 2.HHRouter:這是布丁動畫的一個Router,靈感來自於 ABRouter 和 Routable iOS。
  • 3.美麗聯合開源的三方庫MGJRouter(https://github.com/meili/MGJRouter),使用專案包括旗下的:蘑菇街、美麗說等。

關於JLRoutes簡單介紹:《iOS 模組化之 JLRoute 路由示例 (中英文)》(https://github.com/ReverseScale/JLRouteDemo)

搬運來的一些注意事項:

1.頁面跳轉

頁面跳轉解決方案與業務元件之間通訊問題是一樣的。

但是需要注意的是,你一個業務元件內部的頁面跳轉也請使用URL+Router的方式跳轉,而不要自己直接pushViewController。

這樣的好處是:如果將來某些內部跳轉頁面需要給其他業務元件呼叫,你就不需要再註冊個URL了。因為本來就有。

2.是否去Model化

去Model化主要體現在業務元件間通訊,要不要傳一個Model過去(傳過去的Dictionary中的某個鍵是Model)。

如果去Model化,這個業務元件的開發者如何確定Dictionary裡面有哪些內容分別是什麼型別呢?那需要有個地方傳播這些資訊,比如寫在標頭檔案,wiki等等。

如果不去Model化的話,就需要把這個Model做成Pod庫。兩個業務元件都去依賴它。

最後決定不去Model。因為實際上有一些Model就是在各個業務元件之間公用的(比如User),所以肯定就會有Model做成Pod庫。我們可以把它做成重Model,Model裡可以帶網路請求和本地儲存的方法。唯一不能避免的問題是,兩個業務元件的開發者都有可能去改這個Model的Pod庫。

3.資訊的披露

不同業務開發者如何知曉這些資訊。 使用去Model化和不使用去Model化,我們都有各自的方案。 去Model化,則披露標頭檔案,在標頭檔案裡面寫詳細的註釋。

如果不去Model化,則就看Model就可以了。如有特殊情況,那也是文件寫在標頭檔案內。 總結的話:資訊披露的方式就是把註釋文件寫在標頭檔案內。

4.元件的生命週期

業務元件的生命週期和App一樣。它本身就是個類,只暴露類方法,不存在需要例項,所以其實不存在生命週期這個概念。而它可以使用類方法建立很多ViewController,ViewController的生命週期由App管理。哪怕這些ViewController之間需要通訊,你也可以使用Bus/YTXModule/協議等等方式來做,而不應該讓業務元件這個類來負責他們之間的通訊;也不應該自己持有ViewController;這樣增加了耦合。

弱業務元件的生命週期由建立它的物件來管理。按需建立和ARC自動釋放。 基礎功能元件和第三方的生命週期由建立它的物件來管理。按需建立和ARC自動釋放。

5.版本規範

所有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)

6.單元測試

單元測試我們用的是 Kiwi 。 結合MVVM模式,對每一個業務元件的ViewModel都進行單元測試。每次push程式碼,gitlab-runner都會自動跑測試。一旦開發人員發現測試掛了就能夠及時找到問題。也可以很容易的追溯哪次提交把測試跑掛了。

iOS 模組化進階整理記錄

7.持續整合

原來的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)

iOS 模組化進階整理記錄

第四步:

1.準備釋出Pod庫。修改podspec的版本號,打上相應tag。

2.使用merge_request.sh向master提交一個merge request。

iOS 模組化進階整理記錄

第五步:

1.其他有許可權開發者code review之後,接受merge request。

2.master合併這個merge request 3.master觸發runner自動跑Stage: Init Package Lint ReleasePod UpdateApp

第六步:

如果第五步正確。主App的dev分支會收到一個merge request,裡面的內容是修改Podfile。 圖中內容出現了AFNetworking等是因為這個時候在做測試。

iOS 模組化進階整理記錄

第七步:

主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)

相關文章