IOS組件化方案總結

NO IMAGE

1.啥是組件化

IOS組件化方案總結

打一個比較形象的比喻,把APP比作我們的人體,把胳膊、大腿、心、肝、肺這些人體器官比作組件,各個器官分別負責他們各自的功能,但是他們之間也有主次之分,試想我們的胳膊、大腿等是不能獨立完成某個任務的,必須需要心、肺、肝、膽等的能量支持,那麼可以把胳膊、大腿這種功能性器官比作業務組件,把我們的心、肝、脾、肺、腎比作基礎組件。
那麼我們的業務組件必須要依賴於我們的基礎組件才能發揮其應有的功能,我們的基礎組件(心、肝、肺等)是高度複用的,胳膊、大腿等業務組件要解耦合,難道你的胳膊動,大腿也要跟著動嗎~,最終由大腦整合(那麼大腦可以類比成主工程)。
不知道您是否領會了精神,綜上就是我們的組件化的思路。

2.為什麼要做組件化

繼續我們上面那個驚悚的例子,但是我們把人變成機器人

IOS組件化方案總結

  • 單獨項目運行調試更快
    ~Tony直接穿戴手部機甲,肯定比穿戴全套的快呀~
  • 各組件自由選擇開發姿勢
    ~Tony開發完手部機甲用的MVC,感覺這種不好,開發腿部的時候用MVVM了~
  • 工程可以獨立開發,方便 QA 有針對性地測試
    ~Tony研發手部功能,直接把手部穿上測試就好嘍。~
  • 業務分層、解耦使代碼的可維護性更高
    ~Tony想升級手部的激光,動手就行了,別的地方都不用管!~
  • 便於各業務功能拆分、抽離,實現真正的功能複用(特別是對於多APP來說更加突出)
    ~Tony想給殘疾人研發個假腿,直接把腿部拿過來用就好嘍~
  • 業務隔離,跨團隊開發代碼控制和版本風險控制的實現
    ~Tony有錢可以僱好幾個團隊同步開發,你開發胳膊,我開發腿,互不干擾,完事裝到一起就行~

3.組件化的設計原則及目的

~注:這裡的穩定性指通俗地講就是是否需要頻繁修改代碼~

  • 越底層的模塊,應該越穩定,越抽象,越高度複用。
    穩定性取決於是否需要頻繁改變,底層庫之所以要穩定是因為其會被業務層組件頻繁調用,一旦變化,可能會影響幾乎所有業務層組件,要做到設計一套API很久都不用改變,就需要設計的時候能越抽象, 即需要我們的抽象總結能力。
  • 注意穩定性傳遞
    穩定性是可以傳遞的,例如組件A穩定,組件B不穩定,組件A依賴於組件B,那麼A其實也是不穩定的。
  • 自完備性有的時候要優於代碼複用
    緊接上例:如何實現A、B的解耦呢?假設A依賴於B中的X代碼段
    1>. 如果X是相對獨立且高度複用的,我們當然可以將其提取出來如下

    IOS組件化方案總結

    2>.如果X只是一個方法或者函數,並不適合單獨提取出一個模塊,那麼直接copy一份X代碼到B權衡之下也是沒有問題的。

那麼我們的最終目的可以總結為: 在基於模塊設計原則上, 讓模塊之間沒有循環依賴, 讓業務模塊之間解除依賴。


4.組件化耦合關係

綜上所述我們項目組件化方案示意圖可以是這樣

IOS組件化方案總結

如上圖業務組件單向耦和於基礎組件,這樣的架構完成了基礎組件的高度複用和業務組件的解耦。但是問題又來了,各個業務組件之間難免有頁面跳轉和數據交互,業務組件不耦合意味著不能直接調用,那麼我們引入一箇中間層。

IOS組件化方案總結

這裡注意,依賴一定是單項的,否則我們只是把融在一起的代碼塊拆分成多個代碼塊,而且比之前更麻煩了。

IOS組件化方案總結

5.中間層的實現方案

關於中間層實現眾說紛紜,這裡說下我們實踐的方案。

IOS組件化方案總結

主要分成四部分:

  • routerManager:routerModules(以字符串存儲各個module中相應router類的名字,方便用運行時方法調用,著也是router與各個模塊之間解耦的關鍵),routerMap:維護這url和block之間的對應關係是界面跳轉的關鍵,methodMap:與routerMap的區別就在與block中代碼塊是調用方法的。
//運用的是swift中命名空間的概念,用運行時方法NSClassFromString獲取到相應的類型
private static let routerModules:[String] = ["MessageProject.MessageProjectRouter",
"IMProject.IMProjectRouter",
"CommunityProject.CommunityProjectRouter",
"CourseProject.CourseProjectRouter",
"VideoProject.VideoProjectRouter",
"QuestionbankProject.QuestionbankProjectRouter",
"UserinfoProject.UserinfoProjectRouter",
"CustomUIProject.CustomUIProjectRouter",
"UIFrameProject.UIFrameProjectRouter",
"BasicUIServiceProject.BasicUIServiceProjectRouter",    "ActivityOperationProject.ActivityOperationProjectRouter"]
private static var routerMap:Dictionary<String,[RouterHandler]> = [:]
private static var methodMap:Dictionary<String,MethodHandler> = [:]
  • ** RegisterRoutersProtocol:聲明一個通用接口,在各個模塊的router類中去實現**
// 每個模塊需要實現一個該協議的類,用於模塊內部VC和method的註冊
public protocol RegisterRoutersProtocol {
static func registerModuleRouters()
}
  • UIViewController+Router:在各個模塊的router類中,將需要跳轉的VC進行註冊
// VC註冊,子類需要的話可以重寫
@objc open class func registerRouterVC(_ routerURL:String)
{
guard let tempRouterURL = URL(string:routerURL) else {
return
}
SDJGUrlRouterManager.registerRouterWithHandler(handler: { (transferURL:URL, transferType:SDJGTransfromType, sourceVC:UIViewController, userInfo:[String:Any]?, animated:Bool) -> UIViewController? in
if transferURL.hasSameTrunkWithURL(tempRouterURL) {
let viewController = self.init()
viewController.setRouterInfo(userInfo: userInfo)
if transferType == .push {
if let nav = sourceVC.navigationController {
// navController
nav.pushViewController(viewController, animated: animated)
} else {
// modal nav vc
sourceVC.modelVC(viewController, true, animated)
}
} else if transferType == .model {
sourceVC.modelVC(viewController, false, animated)
}else if transferType == .modelNav {
sourceVC.modelVC(viewController, true, animated)
} else {
}
return viewController
} else {
return nil
}
}, prefixURL: tempRouterURL)
}
  • 各個模塊中router類:繼承自NSObject,實現routerProtocol中的註冊方法,在註冊方法中調用各自VC的UIViewController+Route擴展方法進行跳轉和方法註冊。同時這個類中也維護著key和類的對應關係。
import Foundation
import URLRouteProject
//課程下載界面
public let kCourseFileDownloadURLString = "sina://router/downloadserviceproject/coursefiledownload"
//資料下載界面
public let kDownLoadVCURLString = "sina://router/downloadserviceproject/download"
class DownloadServiceProjectRouter: RegisterRoutersProtocol {
public static func registerModuleRouters()
{
JCourseFileDownLoadVC.registerRouterVC(kCourseFileDownloadURLString)
JDownLoadVC.registerRouterVC(kDownLoadVCURLString)
}
}

參數傳遞:為了有更多的類型參數可以傳遞,我們在router跳轉方法裡多加了一個參數,而不是用url拼接的方式,因為這樣的話只能傳遞基本類型參數,像UIImage這種就無能為力了。

// 用於註冊VC Router的閉包定義,會在頁面跳轉的時候執行閉包,參數為[String:Any]類型,這樣參數就可以隨意傳了。
public typealias SDJGRouterHandler = (_ url:URL, _ transferType:SDJGTransfromType, _ sourceVC:UIViewController, _ userInfo:[String:Any]?, _ animated:Bool) -> UIViewController?

openURL的處理:我們為openURL提供了單獨的方法跳轉,其中包含了參數的解析。


6.IOS組件化實現方案和實際開發運營

cocoapods管理:
代碼解耦只需要遵循上述原則就好,最根本的目的是業務組件的解耦,cocoapod的原理及使用在這裡不在贅述(一搜一大堆)

IOS組件化方案總結

正規的方式是
項目工程發佈tag->配置本地podSpec文件並上傳->校驗->私有庫發佈->其他工程引入。
但在實際操作中有很多情況pod lib link由於種種原因會失敗,而且發佈私有庫本身也需要時間,所以在依賴不變的情況下我們可以用其他的方式引入其他模塊代碼

//拉取對應commit代碼
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :commit => '082f8319af'
//默認拉取dev分支最新代碼
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev'
//拉取0.7.0tag的代碼
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :tag => '0.7.0'

直接修改對應提交的commit,這樣同樣缺點也很明顯,需要程序員自己保證代碼無誤才可以提交,不會有pod的校驗,所以這兩點需要我們權衡。
為了提高效率,我們採用開發時提交commit,由各個業務負責人負責維護commit,每個版本發版時發佈私有庫的方式。

    #社區項目 張三
pod 'CommunityProject',:git => 'http://172.16.117.224/ios-team/communityproject.git', :commit => '15407bae8eccafa14eab4d200e2a8ae763810f15'
#用戶信息項目 李四
pod 'UserinfoProject',:git => 'http://172.16.117.224/ios-team/userinfoproject.git',:commit => 'f1a408cd747e215f0b4fb08b4999edf00570c085'
#活動運營 王二麻子 
pod 'ActivityOperationProject',:git => 'http://172.16.117.224/ios-team/activityoperationproject.git', :commit => '432a21212fc5d6eed9d5d28eacb320e01ec9cc47'
#課程項目  李六
pod 'CourseProject',:git => 'http://172.16.117.224/ios-team/courseproject.git', :commit => 'da10da98af8d53bfe15572958cef8d0cf5e5ba2a'

7.講講坑

實際開發當中會遇到種種坑😳如下

  • 注意類和方法及屬性的權限問題public、pravite等(swift、oc不用)

  • 業務模塊當然要有自己的測試入口,否則很多業務場景都沒有入口,這就需要業務負責人自己添加自己的頁面入口,這也是組件化之後的好處,每個業務組件都可以單獨運行,單獨測試,更加輕量級。

  • Unable to satisfy the following requirements:

    IOS組件化方案總結

    這類問題是/Users/xingfan/.cocoapods/repos/master也就是cocoapod的本地索引庫沒有更新最新,裡面沒有Charts(3.1.0)版本的spec文件,導致它不知道去哪裡拉代碼。執行pod udate,一般這種問題都是嫌pod update太慢執行pod update –verbose –no-repo-update導致的

  • pod update會主動更新本地repo,如果報錯,可以指定到本地spec倉庫,一般在cd ~/.cocoapods/repos/iosspecrepo,然後git clean -f,如果再有問題,那就是組件間依賴出錯,找相關負責人處理。

  • 最後說說spec倉庫,本身就是一個git倉庫,pod repo update就相當於拉取並同步遠程spec倉庫(git pull),通過其中的spec文件(描述了目標源所在的地址、tag、依賴庫的版本等)準確的找到想拉取的代碼。

8.談談優化。

1.用cocoapods的缺點,代碼集成到主工程後同樣運行緩慢,原因是因為拉取的代碼依然是需要編譯的,本質上與原本沒有區別。
針對這一點我們可以用Cathage替代cocoapods,CocoaPods (默認)自動建立和更新一個Xcode workspace,用來管理你的項目和所有依賴。Carthage使用xcodebuild來編譯出二進制庫,剩下的集成工作完全交給開發人員。模塊變成可執行的二進制文件之後運行速度自然會快很多。
有興趣的同學可以自行研究。

相關文章

《YYModel源碼分析(一)YYClassInfo》

SDMemoryCache中的NSMapTable

關於IOS屬性atomic(原子性)的理解

GYHttpMock:使用及源碼解析