1. 程式人生 > >iOS 混編 模組化/元件化 解耦

iOS 混編 模組化/元件化 解耦

1. 開篇

本文的初衷,是為了給正在做混編或者模組化的同學們一個建議和參考。

因為來餓廠以後做的專案是全公司唯一一個 Swift/OC 混編的 iOS 專案,所以一路上踩坑無數,現在把一些踩坑的過程和經驗總結起來,供大家參考。

相信在瀏覽本文後,一定會有所收穫。

我來的時候專案已經開始 Swift 改造了,慢慢的把專案 Swift 化,新程式碼都是 Swift 的。

先公佈七個月成果,下圖是我們最終的專案結構:

對於我們混編的情況,在五個月前大家就展開了討論。

給我們的選擇有兩種:

  1. 慢慢將 OC 程式碼替換成 Swift

  2. 儘快模組化,分離兩種語言程式碼

一開始我們是從 選擇1 開始做的,但是很快我們就發現,對於我們 74% 都是 OC 程式碼的專案來說,太痛了,太漫長了,而且期間迭代的過程中還在不斷地迭代,不斷的耦合。

所以在經過一番利害分析後我們迅速投入到了 選擇2 中。一方面,模組化本身就是越來越臃腫的專案的最終歸宿,一方面可以慢慢將兩種語言剝離。

2. 模組劃分

刀怎麼切,是混編模組化最重要的一步,完全決定了後續工作的難與否。

不用從業務模組拆分,類似『實時訂單模組』、『歷史訂單模組』、『個人中心』這樣直接拆分,保準你後面哭到無法自已。

正確的做法應該從底層部分開始抽離,首先能想到的應該是『類擴充套件 Extension』、『工具類』、『網路庫』、『DB 管理』(當然這個我們沒有用到比較重的 DB)。

平常我們看到一些大型庫,或者一些公司介紹自己產品架構時候都是什麼樣的?是不是下層有 OpenGL ES 和 Core Graphics 才有上層 Core Animation,再到 UIKit。下層決定上層,只有把複用率高的部分抽出才能逐步構建上層業務。

所以首先我們做的就是抽工具類和 Extension,諸如:

  1. 各類 Constants 檔案

  2. NSTimer、NSString、UILabel 等等類的 Extension

  3. RouterHelper、JavascripInterface 等等 Utils 和 Helper

這一塊的工作,不僅僅可以抽出 OC 程式碼,也同時可以抽出 Swift 的程式碼。我們將 OC 部分的程式碼新建了庫為 LPDBOCFoundationGarbage,Swift 部分的程式碼新建庫為 LPDBPublicModule。

2.1 LPDBOCFoundationGarbage

先說 LPDBOCFoundationGarbage,叫這個名字顯然不僅僅會放入上面所提到的檔案。LPDBOCFoundationGarbage 還會大量放入長期不跟隨業務變動的 OC 程式碼。這是因為,在實踐中,我們發現總是『理想很美好』,雖然大家都抱有把舊程式碼整理一遍的願望,但是實際上,我們專案的舊程式碼已經到了剪不斷理還亂的地步,所以期望一邊整理、一邊分離的想法基本是不可靠的。這時候就要借用 MM 大佬給我們傳授的一句話『讓噁心的程式碼噁心到一起』,LPDBOCFoundationGarbage 正是為此而建立。

大量放入長期不跟隨業務變動的 OC 程式碼包括

  1. 自定義的 Customer View,諸如:Refresh 控制元件、Loading 控制元件、紅點控制元件等等

  2. 自定義的小型控制器,諸如:TextField 和其五六個過濾器 PhoneNumValidator、IDCardValidator 等等

  3. 不隨業務變動的 Controller,諸如:自定義的 AlertController、自定義的 WebController、自定義的 BaseViewController 等等

關於字首說兩句。我們所有抽出的庫都帶有字首 LPDB,但是針對 Swift 庫和 OC 庫稍有區分的是,OC 庫內的檔案也都帶有字首,而 Swift 庫是去掉了字首,這也符合兩種語言的規範。

2.2 LPDBPublicModule

LPDBPublicModule 情況很簡單,主要是新業務迭代時候產生的一些複用性高的程式碼,但是這顯然和 OC 那個垃圾桶庫不一樣,要乾淨整潔的多。主要存放的是:

  1. Swift Extension

  2. Lotusoot 及其他公開協議

Lotusoot 是個由我開發的模組化工具和規範,一開始我叫它『路由』,但是隨後發現部門這邊因為叫它『路由庫』而曲解了它的意思,所以後來我就叫『模組化工具』了。關於 Lotusoot 將會有另一篇文章。

2.3 LPDBNetwork

這塊毋庸置疑,不管什麼專案都基本有的一塊,基本上我們專案中網路相關的舊程式碼都是 OC 的,唯一比較麻煩的是,我們的網路層,早期人員寫的比較粗糙,甚至和 UI 層程式碼有很多耦合,比如網路請求中和網路請求失敗有一些 HUD 顯示,轉轉菊花什麼的。所以導致在從主工程抽離的時候有很多噁心的地方。

所以對於這種強耦合,最後解決的方式是分成了兩遍程式碼改造,第一遍先通過反射先將 OC 程式碼抽出,保證程式碼可用,通過基礎測試。第二遍是通過協議來代替原先的反射。第三遍是使用 Lotusoot 徹底規範服務呼叫。在後面一節『過程中的一些難點總結』中會介紹

2.4 LPDBUIKit

這塊是 Swift 的 UI 庫,一些比較常用到的控制元件等等。

2.5 LPDBEnvironment

這塊是用於環境控制的,切換要訪問的伺服器環境,這塊本身可以不抽出的,但是由於有其他基礎模組,比如 LPDBNetwork 依賴,而且其中相關程式碼比較多,環境相關的程式碼也比較獨立,所以單獨抽出。

3. 業務模組抽離

到這裡為止,比較底層的程式碼就基本抽出結束了,剩下的就可以較為輕鬆一些的抽取業務庫了。

抽取業務庫的重點在於:

  1. 抽取的業務庫不會經常改動,以防止在抽取、重構過程中由於業務需求發生更動

  2. 抽取的業務庫可以高度獨立,抽取後應當和積木一樣,如 LPDBLoginModule,抽取後快速被整合在任何模組,並能保證登入功能,更好的服務其他模組

我們目前抽出的三個業務模組分別是: LPDBHistoryModule、LPDBUserCenterModule、LPDBLoginModule。

4. 過程中的一些重難點

剩下的就是,來說一下在這個過程中的疑難問題。

4.1 處理模組耦合程式碼-反射呼叫

抽取程式碼第一遍使用反射的原因主要是,通常你在遞迴某個檔案的依賴的時候,會遞迴出非常多的東西(尤其是我們的蜜汁舊程式碼),往往就是 A->B->C->D->F,中間有各種依賴,甚至到最後一層的時候還引用了 Swift 的類。直到最後你看 #import 就想吐。

為什麼沒有辦法一步到位,通過協議解決耦合?

這主要是因為單個 Pod 庫開發時使用開發模式是很容易除錯的,但是兩個 Pod 庫同時在不發版本的情況下使用開發模式是比較難處理的。這種情況下,反覆操作兩個或者兩個以上的庫是麻煩的,所以優先考慮將程式碼儘快分離開來,並能通過基本測試,不影響功能。

所以在這一遍處理結束後,子庫中出現了很多 NSClassFromString 等等。

以 LPDBLoginMoudle 為例:

NSString *className = [NSString stringWithFormat:@"%@.`AuthLoginManager", [NSString targetName]];

id authLoginManager = NSClassFromString(className);

if (![authLoginManager conformsToProtocol:@protocol(authLoginSuccess)]) {

    return;

}

[authLoginManager authLoginSuccess];

id delegate = [[UIApplication sharedApplication] delegate];

[delegate jumpToShopListVC:shops];

4.2 處理模組耦合程式碼-協議呼叫

保持第一遍中充滿 NSClassFromString 是不可取的,因為這類程式碼往往屬於硬編碼,不能在類名出現改動、或者方法名出現改動的時候及時在編譯階段丟擲 error。

在這裡引出一段討論。

之前跟大神們討論元件化(模組化)的具體實踐時候,說到了主流的元件化可能都借用了 + (void)load 方法和 rumtime 操作來註冊路由和服務。這時候 casa 大神提出了一種說法『元件化的根本目的是隔離、隔離問題影響域、隔離業務、隔離開發時的依賴。所以讓兩個本來有關係的人變得沒有關係,就需要一箇中間人,如果不用 runtime 能省掉不少事,如果考慮可維護性,用字串足夠了,也就是用字串來表徵方法、方法字典表徵引數,URL 是字串的更復雜表徵,其本質還是字串,用 URL 不如用字串』。

那個時候聽了 casa 大神的說法覺得『哎?好像有點道理』,但是在後期的實踐中,我覺得就我個人的程式碼習慣,是希望儘可能的將問題暴露在編譯階段,能讓它丟擲 error 就丟擲 error,縱使使用字串可以定義常量,但由於大家不是獨立負責專案,在其他人看到你的方法引數時,比如:+ (void)callService:(NSString *)sUrl 或者 + (void)openURL:(NSString *)url ,對方發現你的引數是 NSStrring,很有可能直接硬編碼字串而不去查閱常量列表,這是習慣性編碼很容易出現的問題。

最後一點原因是,反射或者通過類/方法字串字典的方式實在太 OC 了,不管怎麼樣我們是一個儘量 Swift 化的專案,應該儘量吸取其優點,雖然抽出的 OC 庫可以使用反射,那 Swift 庫咋辦?目前 Swift3 與 4 都沒有很好的支援反射。

所以,第二遍處理使用協議替換反射是很有必要的。但實質上,處理的並不是很好。大致如下(我們以 LPDBLoginModule 為例):

4.2.1 在 LPDBLoginModule 整理用到的服務,歸類整理

如我們的 LPDBLoginModule 用到了 AppDelegate 中的一些方法,同事用到了 AuthLogin 相關類中的一些方法

4.2.2 在 LPDBLoginModule 中建立相應的協議

即建立 AuthLoginDelegate.h 和 AppDelegateProtocol

大致的程式碼如下:

@protocol AppDelegateProtocol <NSObject>

- (void)jumpToHomeVC;

- (void)jumpToShopListVC:(NSArray *)shops;

- (CLLocationCoordinate2D)getCoordinate;

@end

@protocol AuthLoginDelegate <NSObject>[Pods](media/Pods.)

+ (void)authLoginSuccess;

@end

4.2.3 在主工程中去實現協議

AppDelegateProtocol 由 AppDelegate 擴充套件實現:

@import LPDBLoginModule;

@interface AppDelegate (Protocol)  <AppDelegateProtocol>

@end

@implementation AppDelegate (Protocol)

- (CLLocationCoordinate2D)getCoordinate {

    ...

}

- (void)jumpToHomeVC {

    ...

}

- (void)jumpToShopListVC:(NSArray *)shops {

    ...

}

@end

AuthLoginDelegate 由 AuthLoginManager(這個 Manager 在主工程中是 swift 編寫的) 實現:

extension AuthLoginManager: AuthLoginDelegate {

    static func authLoginSuccess() {

        ...

    }

}

4.2.4 在 LPDBLoginModule 呼叫服務

id delegate = [[UIApplication sharedApplication] delegate];

if (![delegate conformsToProtocol:@protocol(AppDelegateProtocol)]) {

    return;

}

CLLocationCoordinate2D coordinate = [delegate coordinate];

NSString *className = [NSString stringWithFormat:@"%@.AuthLoginManager", [NSString targetName]];

id authLoginManager = NSClassFromString(className);

if (![authLoginManager conformsToProtocol:@protocol(LPDBAuthLoginDelegate)]) {

     return;

}

[authLoginManager authLoginSuccess];

[self jumpToSelectShopView:shops];

但是,可以很明顯感受到,這次的改變並不徹底:

  1. 還是存在大量的 ![delegate conformsToProtocol:@protocol(AppDelegateProtocol)] 這樣的判斷,僅僅是起到了容錯,保證不會 crash,但是卻不能將問題暴露在編譯階段。

  2. AppDelegateProtocol 明明是一個公共的,多個模組使用的協議,卻被定義到了 LPDBLoginModule

  3. 概念顛倒,理想狀態下,應該是各個子模組提供協議和實現,告知其他模組可以呼叫該模組哪些功能。而目前是子模組告知其他模組需要呼叫哪些方法,由其他模組實現。

那麼為了徹底解決問題,我們引入了 Lotusoot —— 元件通訊和工具。

4.3 處理模組耦合程式碼-Lotusoot

Lotusoot 的最初目的就是為了解決模組間的耦合,並且同時支援 OC 和 Swift 使用,也是這幾個月中去做的一個比較重要的東西,庫本身小巧靈活,包含的東西也很少,但是起到的規範作用卻是我非常滿意的一點。

Lotusoot 規範的核心思想主要是以下幾步,我們同樣使用上面的 LPDBLoginModule 為例:

4.3.1 建立共用模組——LPDBPublicModule

LPDBPublicModule中定義了各個模組可以提供的服務,做成協議,稱為 Lotus,一個 Lotus 協議包含了一個模組的所有的能呼叫的方法的列表。舉例如下:

@objc public protocol AppDelegateLotus {

    func jumpToHomeVC()

    func jumpToSelectShopVC(shops: [Any], isNapos: Bool)

    func getCoordinate() -> CLLocationCoordinate2D

}

@objc public protocol MainLotus {

    func authLoginSuccess()

}

4.3.2 各個模組中,實現 LPDBPublicModule 中對應的 Lotus 協議

實現協議的 Class 稱為 Lotusoot。舉例如下:

class AppDelegateLotusoot: NSObject, AppDelegateLotus {

相關推薦

iOS 模組/元件

1. 開篇 本文的初衷,是為了給正在做混編或者模組化的同學們一個建議和參考。 因為來餓廠以後做的專案是全公司唯一一個 Swift/OC 混編的 iOS 專案,所以一路上踩坑無數,現在把一些踩坑的過程和經驗總結起來,供大家參考。 相信在

Android 模組 元件 外掛的關係

模組化:一個程式按照其功能做拆分,分成相互獨立的模組(例如:登陸,註冊)。模組化的具體實施方法分為外掛化和元件化。 元件化:開發模式下面module本來就是一個獨立app,只是釋出模式下變成library。 外掛化:就是不存在釋出模式開發模式,每個元件業務就是一個獨立

IOS C 報file not found的問題處理

xcode 編譯.m檔案使用 C compiler (clang or llvm-gcc)編譯器, 而編譯.mm時使用clang++ 或llvm-g++編譯器。 例如程式碼使用了 #inclu

iOS 藕、元件最佳實踐

iOS 解藕、元件化最常用的是使用統跳路由的方式,目前比較常用的 iOS 開源路由框架主要是JLRoutes、MGJRouter、HHRouter等,這些路由框架各有優點和缺點,基本可以滿足大部分需求。目前最常用來作路由跳轉,以實現基本的元件化開發,實現各模組之間的解藕。但是,在實際中開發中會發現,無法徹底使

iOS 元件,外掛模組設計思路分析

前言 隨著使用者的需求越來越多,對App的使用者體驗也變的要求越來越高。為了更好的應對各種需求,開發人員從軟體工程的角度,將App架構由原來簡單的MVC變成MVVM,VIPER等複雜架構。更換適合業務的架構,是為了後期能更好的維護專案。 但是使用者依舊不滿意,繼續對開發人員提出

58 同城 iOS 客戶端搜尋模組元件實踐

【編者按】58 同城 App 自從 1.0 版本開始,便已經提供了搜尋功能。隨著版本的迭代、業務的複雜,搜尋框架也在不斷受到挑戰。諸如程式碼不能複用、耦合度高、業務功能接入成本高等問題日積月累,成為需要迫切解決的問題。本文從具體實際問題入手,詳述了利用元件

iOS元件(四)-程式碼耦合

很多的元件化文章通常是教授技術上的經驗,但是在實際元件化中,尤其一個老專案進行元件化改造時,最為耗時的卻是業務程式碼的解耦合工作。這部分工作並不高階,由於很多的程式碼經過不斷的改動,並且改動人員水平參差不齊,解耦程式碼更多的時候是體力活。那麼怎麼高效的完成這

iOS元件、外掛模組之路(一)

前言:公司一年多的小專案,進行專案拆分,要求是每個業務模組都可以單獨打包。在開發過程中,如:酒店模組,只修改酒店單元,測試也只測試酒店部分。模組間相互不干擾,就有了,今天元件化之路。 一、元件化的目的。 說是元件化,其實更多的是模組化,對模組之間相互之間不干

元件開發之路由器模組(ActivityRouter原始碼詳

    路由器的作用是什麼?通俗的講,路由器的作用就是一根網線滿足多人上網的需求。而在開發中路由器模組的作用就是實現中轉分發,也就是說將原來有關係的模組(有依賴的模組分開),產生一箇中間的模組,讓原來依賴的兩個模組都去和路由模組互動,從而將原來兩個有關係的模組拆分開,利如我現

Category 特性在 iOS 元件中的應用與管控

背景 iOS Category功能簡介 Category 是 Objective-C 2.0之後新增的語言特性。 Category 就是對裝飾模式的一種具體實現。它的主要作用是在不改變原有類的前提下,動態地給這個類新增一些方法。在 Objective-C(iOS 的開發語言,下文用 OC 代替)中

元件模組、外掛

模組化 模組化開發將一個程式按照其功能做拆分,分成相互獨立的模組,以便於每個模組只包含與其功能相關的內容。模組我們相對熟悉,比如登入功能可以是一個模組,搜尋功能可以是一個模組,汽車的傳送機也可是一個模組。   元件式開發基於可重用的目的,將一個大的軟體系統按照分離關注點的形式,拆分成多個獨立的

iOS 一個輕量級的元件思路

前言 說起元件化大家應該都不陌生,不過也再提一下,由於業務的複雜度擴充套件,各個模組之間的耦合度越來越高,不但造成了“牽一髮動全身”的尷尬境地,還增加了測試的重複工程,此時,元件化就值得考慮了。元件化就是將APP拆分成各個元件(或者說模組),同時解除這些元件之間的耦合,然後通過路由中介軟體將專案所需要的元件

iOS元件解決圖片顯示問題

在元件化時,對於圖片資源,我們需要把對應元件的圖片資源放到對應元件如下位置: 這裡有個注意的地方: 在上圖Assets目錄下是直接把相關圖片匯入進來還是在Assets下新建一個資料夾,再把圖片匯入到該資料夾,取決於podspec檔案的下圖位置: 對應下圖: 修改podspec檔案 總

Android:關於專案元件/模組的設計

隨著技術越來越成熟,這兩年,元件化開發與外掛化開發的熱度一度高漲。對於元件化,有的人也喜歡稱之為模組化開發,我也比較喜歡稱之為模組化開發。使用模組化開發也已經有一段時間了,特此總結一下模組化開發的心得,防止以後忘記。 什麼是模組化開發 對於模組化開發的概念,有的人可

iOS元件開發

在一個APP開發過程中,如果專案較小且團隊人數較少,使用最基本的MVC、MVVM開發就已經足夠了,因為維護成本比較低。 但是當一個專案開發團隊人數較多時,因為每個人都會負責相應元件的開發,常規開發模式耦合會越來越嚴重,而且導致大量程式碼衝突,會使後期維護和升級過程中程式碼“牽一髮而動全身”,額外

Android模組開發、元件開發;

模組化開發:優點嘛,專案過大時便於管理; 1、在根目錄的gradle.properties檔案下新增 isBuildModule=false; 使用isBuildModule來控制這個是Library還是獨立的APP; 2、建立一個新的Module,在其build.gra

iOS元件開發-CocoaPods簡介

CocoaPods簡介 任何一門開發語言到達一定階段就會出現第三方的類庫管理工具,比如Java的Maven、WEB的Webpack等。在iOS中類庫的管理工具-CocoaPods。 利用CocoaPods管理第三方庫可以自動化幫我們完成各種庫的依賴和配置,包括配置編譯階段、連結器選項、甚至是ARC環境下的

C程式設計|用函式實現模組程式設計詳(一)

目錄 一、為什麼要用函式 使用函式可使程式清晰、精煉、簡單、靈活。 函式就是功能。每一個函式用來實現一個特定的功能。函式名應反映其代表的功能。 在設計

Android元件框架專案詳

簡介 什麼是元件化? 專案發展到一定階段時,隨著需求的增加以及頻繁地變更,專案會越來越大,程式碼變得越來越臃腫,耦合會越來越多,開發效率也會降低,這個時候我們就需要對舊專案進行重構即模組的拆分,官方的說法就是元件化。 元件化帶來的好處 那麼,採用元件化能帶來什麼好處呢?主要有以下兩點: 1、

iOS 從零到一搭建元件專案框架

隨著公司業務需求的不斷迭代發展,工程的程式碼量和業務邏輯也越來越多,原始的開發模式和架構已經無法滿足我們的業務發展速度了,這時我們就需要將原始專案進行一次重構大手術了。這時我們應該很清晰這次手術的動刀口在哪,就是之前的高度耦合的業務元件和功能元件,手術的目的就是將這些耦合拆分成互相獨立的各個元件。 工程