iOS元件化實踐

銅板街技術發表於2019-03-03

前言

公司業務不斷迭代擴張,專案的功能越來越多也越來越複雜,各個業務之間也不可避免的耦合越來越多,程式碼也越來越臃腫,原來的模式已經無法滿足現有專案開發高複用、高可維護性的需求,目前業界解決業務多樣性複雜性比較好的一種架構思路就是元件化,將專案拆分成各個模組,這樣能很好的解決現有的程式碼耦合度高、複用性不足的問題,也方便管理各個模組。

技術選型

前期調研了一些元件化的方案,大致歸納為三個方案url-block、protocol-class、target-action,經過權衡後,我們最終選擇target-action這種方案,說到這裡肯定會有人問了你們是基於什麼理由去定的這個方案的呢?由於三個方案的優缺點全部描述篇幅太大,下面我主要基於呼叫方式、傳參模式給大家闡述下。

呼叫方式

元件之間的呼叫其實就是呼叫方呼叫服務方的一個過程,這裡就涉及到一個問題呼叫方如何發現服務方的問題。

  • url-block是通過url來發現服務,服務方在應用啟動時候優先註冊一系列url和對應的block到記憶體中;
  • protocol-class其實是基於url-block的一種擴充套件補充的元件化方案,服務方也需要在應用啟動的時候優先將class和protocol做一個對映存放到記憶體中;
  • target-action基於的是runtime,不涉及到任何註冊對映關係的問題。

那麼問題來了,url-block、protocol-class每次啟動的時候都需要註冊一系列的對映關係到記憶體,隨著專案的越來越龐大,不可避免的會消耗掉更多的記憶體;業務的擴充套件變更不可避免的會涉及到服務對映關係的維護(增刪改),維護成本也會不斷增加,target-action則通過runtime完全避免了類似的問題,記憶體開銷小,維護成本低。

傳參模式

元件間呼叫,不可避免的會涉及到引數的傳遞。

  • url-block是基於url,這樣弊端就暴露無遺了,url傳遞的引數限制性很強,就舉個簡單的列子,分享模組的分享圖片問題,圖片之類的引數url傳遞不了,再比如字典、陣列等,這樣url傳遞這些引數顯然不合適也不方便;
  • protocol-class正是由於url-block傳參的弊端才孕育而生的擴充套件方法,這裡雖然能解決傳遞複雜引數的問題,但是記憶體增加和難維護的問題依舊存在;
  • target-action方案提出了去model化傳參,因為如果傳遞的是對應的model,因為對應的model一般都是和對應的業務或者模組掛鉤的,這樣元件之間本質上還是沒有獨立,沒有達到去耦合的目的,最終這個方案呼叫方和服務方之間是通過字典來進行傳參,這樣做具備字典傳參的靈活性、多樣性,也具備了url-block方案不具備傳遞複雜引數的能力,引數去model化和呼叫runtime也徹底斬斷了服務方和中介軟體之間的依賴,真正意義上實現了元件化。

備註:本文我們會不斷提到一個概念叫runtime(其主要特性是訊息傳遞,如果訊息在物件中找不到,就進行轉發),如果對iOS runtime不是特別瞭解,可以先搜尋瞭解下,方便後面理解文章。

target-action元件化方案

方案架構

target-action元件化方案分為兩種呼叫方式,遠端呼叫和本地呼叫。

a. 遠端呼叫通過AppDelegate代理方法傳遞到當前應用,呼叫遠端介面並在內部做一些處理,處理完成後會在遠端介面內部呼叫本地介面,以實現本地呼叫為遠端呼叫服務。

b. 本地呼叫由performTarget:action:params:方法負責,但呼叫方一般不直接呼叫此方法,會通過一箇中間層Media層,Media層會提供明確引數和方法名的方法,在方法內部呼叫performTarget:方法和引數的轉換。

iOS元件化實踐

方案思路

元件化完整的鏈路是呼叫方 => 中介軟體 => 服務方,這樣整個呼叫算是完成,下面從後兩者的角色來闡述下大致的一個實現思路(呼叫方其實很簡單,字面意思大家都懂)。

1、中介軟體
TBJMediator(中介軟體)是基於CTMediator(target-action方案作者提供)的優化版本,基於CTMediator做了一些優化和容錯處理。首先中介軟體對外(呼叫方)暴露明確引數型別的方法,呼叫performTarget發現服務方對應的Target和Action,實現本地元件間的呼叫,實際是通過runtime(俗稱訊息分發)發現服務方和服務方對應的方法。這裡大家可以思考一個問題,每個業務或者模組所有的呼叫如果都寫在這個中介軟體中,幾十個甚至幾百上千個方法,勢必會對這個中介軟體的後期維護帶來極大的麻煩(埋坑),基於這樣的現實孕育而生了Category方案,根據每個服務方業務,對應建立一個TBJMediator的Category(中介軟體分類),這樣每個業務對外暴露的介面和這些Category一一對應,但是所有對外介面都根據業務分離。

2、服務方
服務方顧名思義服務的提供方,其實Target-Action這個方案名稱已經提前劇透了,每個Target就是對應服務方提供的服務類,其中的每個Action就是具體的某項服務。每個元件可以根據實際需要提供一個或者多個Target類,在Target類中宣告Action方法,TBJMediator通過runtime主動發現服務。

iOS元件化實踐

具體實施

元件化的目的就是為了降低耦合,但是專案中不可能不存在耦合,換句話說專案中各個業務都是有一定的關聯性,我們要做的就是不斷降低不必要的耦合,讓專案變的架構清晰明瞭。為了能優先完成整個元件化方案,我們將拆分的維度適當放寬,剝離各個基礎元件和業務元件,並保證每個元件的獨立性。

具體劃分出基礎元件、基礎業務模組、業務模組。

  • 基礎元件主要包含業務完全無關的一些UI控制元件、UI工廠類、基礎工具類、網路請求等;
  • 基礎業務模組包含分享模組、外掛模組、以及基礎服務模組等;
  • 業務模組主要包含產品模組、使用者模組等。

iOS元件化實踐
每個模組都基於CocoaPods進行管理,並相互保持獨立,業務模組相互之間的呼叫也均通過中間層去呼叫,相互之間沒有直接引用。在拆分層級過程中需要注意,上層不能對下層有依賴,下層中不能包含上層的業務邏輯,對於專案中的公共資源和程式碼,儘量下沉到下層中。

技術實現(產品模組為例)

呼叫方在某處呼叫[[TBJMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向TBJMediator發起跨元件呼叫,TBJMediator根據獲得的target和action資訊,通過objective-C的Runtime轉化生成Target例項以及對應的Action,然後最終呼叫到目標業務。

呼叫方

// ViewController.h
#import "TBJMediator+TBJProdModul.h"

UIViewController *netLoanListVC = [TBJMediator getNetLoanListVCWithTypeId:typeId title:title];
[self.navigationController pushViewController:netLoanListVC  animated:YES];
複製程式碼

TBJMediator分類(中介軟體)

// TBJMediator+TBJProdModul.h

+ (UIViewController *)getNetLoanListVCWithTypeId:(NSString *)typeId title:(NSString *)title;

//  TBJMediator+TBJProdModul.m
+ (UIViewController *)getNetLoanListVCWithTypeId:(NSString *)typeId title:(NSString *)title
{
    id typeIdArg = NilObj(typeId);
    id titleArg = NilObj(typeId);
    return [[TBJMediator sharedInstance] performTarget:@"ProdModul" action:@"getNetLoanListVC" params:@{@"typeId": typeIdArg, @"title":titleArg} shouldCacheTarget:NO];
}
複製程式碼

服務方

// Target_ProdModul.h

- (UIViewController *)getNetLoanListVC:(NSDictionary *)params;
複製程式碼

上面程式碼中呼叫方只需要依賴TBJMediator+TBJProdModul,進而達到了呼叫某個產品列表的目的,我們解耦的目的達到了。當然我們也能觀察到,現在的資料傳遞是通過字典,沒有用model傳遞,這樣做避免了直接依賴model,避免model暴露給所有元件,而且字典傳遞引數很靈活,可以傳遞各種想要的資料型別。當然字典傳遞引數也不是沒有缺點,為了呼叫方清晰,當引數個數比較多的時候,方法會看上去比較冗長,而且還需要特別注意引數的非空判斷。

總結

模組化拆分時候需要注意的點

1.合理的拆分粒度

一開始拆分的時候粒度要適中,粒度太細的話拆分很困難,俗話說拔出蘿蔔帶出泥,先將相對粗粒度的業務獨立的元件拆分出來,後續如果一個拆分完成的庫仍然比較臃腫的化,說明仍然存在細化拆分的餘地。

2.制定拆分計劃

前期將專案元件大致梳理一遍,制定一個合理的拆分計劃,制定詳細的整體規劃能夠將一些前期不合理的依賴、不合理的維度暴露出來,提升後續拆分的效率。

3.拆分原則

在拆分層級過程中需要注意,上層不能對下層有依賴,下層中不能包含上層的業務邏輯。對於專案中的公共資源和程式碼,儘量下沉到下層中。

模組化後相比單專案的一些缺點

1.當然模組化雖然有很多優點,但是實際操作過程中由於CocoaPods上傳私有庫步驟繁瑣,如果每個庫都是手動去上傳,就會比較費勁,還是需要一些額外的指令碼配合。

2.由於涉及到打包編譯順序問題(CocoaPods維護的私有庫優先編譯),有些預編譯巨集要格外注意,不然可能編譯後的程式碼並不是你想要的,可能編譯成了測試環境或者其他測試環境的程式碼。

3.另外每次上線之前app打包也必須要保證每個模組必須是最新的版本,相對單專案就沒有這個問題。

元件化目前也只是邁出了這一步,後期還有很多需要優化改進,也希望有更多的技術大咖能給出建議。

作者簡介

狄仁傑,銅板街 iOS 開發工程師,2013年12月加入團隊,目前主要負責 APP 端 iOS 日常開發。

iOS元件化實踐

更多精彩內容,請掃碼關注 “銅板街技術” 微信公眾號。

相關文章