1. 程式人生 > >ios 元件化方案

ios 元件化方案

1.

閱讀本篇文章以前,假設你已經瞭解了元件化這個概念。

最近兩年移動端的元件化特別火,但移動端元件化的概念追其溯源應該來自於Server端,具體來說這種概念應該是由JavaSpring框架帶來的。

Spring最初是想替代笨重的EJB,在版本演進過程中又提供了諸如AOPDIIoC等功能,推動了Java程式設計師面向介面程式設計,而面向介面程式設計在面向物件的基礎上將物件又抽象了一層,對外提供的服務只提供介面類而不直接提供物件類,這就引出了一個問題,為什麼給外部提供的是一個介面類而不是物件類?

回想下我們在編寫iOS程式碼的過程中,我們最常採用的程式碼組織方式是MVC,最常使用的開發思想是面向物件程式設計,假設現在有一個控制器AViewController

,這個控制器的UI由3部分組成,從上至下分別為頂部Banner,中間是UICollectionView管理著一些入口,底部是UITableView管理著商品列表,單一職能原則約束著我們這3部分業務邏輯最好是由3個類去管理,大家通常也是這麼做的,因此現在AViewController就要對這3個類進行引用(import),假設中間部分的入口可以跳轉到10個不用的頁面(Controller),那麼可能就會有人在AViewControllerimport這10個Controller,此時,耦合的關係就產生了,如果整個專案都按照這個流程開發,最終整個專案類與類之間的耦合關係會複雜到難以想象,當我們需要把一個類或某個功能、某條業務遷移到其他專案時,可能你就會變成這樣

what the fuck!

tmd怎麼這麼多錯誤?

怎麼解決?
1、根據IDE的錯誤提示慢慢改,缺啥補啥。
2、元件化,一勞永逸。

2.

如何進行元件化,網上已經有了不少文章講解了這方面的經驗,我這裡再簡單說一說,說不全我文章寫不下去。

第一步:規劃專案整體架構

設計專案的整體架構並不是讓你決定使用MVC還是MVVM,在我看來,MVCMVVM亦或是MVP等等等,都屬於程式碼的組織方式,嚴格意義上來說,並不能算是專案架構,專案架構需要你站在更高的緯度去統籌、規劃專案該如何分層,這個時候就需要你根據產品來對專案劃分不通的層次,業務層的程式碼就劃分到業務層,第三方庫都是通用的,就可以把這些第三方庫劃分到通用層,那麼這個層級關係誰在上誰在下?我們可以根據對業務對程式碼的依賴程度來劃分,那麼業務層就應該在最上面,通用層的程式碼在最下面。如圖:


圖中中間又多出來兩層,中間層和通用業務層,通用業務層顧名思義就是可以分別提供給業務ABCD使用的業務類的程式碼;中間層的作用是協調和解耦的作用,協調元件間的通訊,解除元件間的耦合,它要做的也就是這篇文章的標題所要講的,中間層就是元件通訊方案。

第二步:管理基礎元件

第一類基礎元件:

一個iOS專案可能會依賴很多第三方開源庫,比如AFNetworkingSDWebImageFMDB等,這些開源框架服務全球上百萬個專案,它們是對系統API的封裝,並且不依賴於業務,我們可以將他們歸到基礎元件裡,很多專案使用cocoapod來管理這些庫,也有直接把庫檔案直接拖到專案裡來的,我這裡假設使用cocoapod進行管理。

第二類基礎元件:

而在一些比較大的專案裡或要求比較高的公司往往會將這些第三方開源框架進行二次封裝,以滿足一些使用上的需要或彌補一些先天的缺陷,那麼這些進行二次封裝的庫同樣也屬於基礎元件,我們可以將自己二次封裝的庫也放到通用層這一層,那怎麼管理這些二次封裝的庫呢?推薦使用本地的私有庫,利用cocoapod進行管理。

第三類基礎元件:

在開發業務時,我們也可以從業務程式碼中抽取一些庫出來,比如很多新聞App首頁的橫向滾動頁面就可以抽取出一套UI框架,UITabbarController也可以抽取成一套UI框架,高效的切一個UI控制元件的圓角我們也可以抽取成一套小的UI框架,自定義彈窗、loading動效等都可以抽取成單獨的框架。

在整理這些基礎元件的同時,勢必要改很多業務層的程式碼,這會讓你感覺很噁心,但做這些事情的同時也是在為我們的業務元件化鋪路,也就是說,抽取基礎元件會推進我們進行業務元件化。

第三步:業務元件化

既然我們封裝的基礎元件可以使用私有pod進行管理,業務層程式碼可以用私有pod進行管理嗎?答案是可以,業務元件化也可以通過私有pod庫來解決。

我們在第一步中劃分好了專案的架構層次,最頂層的是業務層,業務層根據業務屬性劃分好了若干條業務線,那麼每條業務線就對應著一個pod私有庫,在我們打包私有庫的時候,私有repo對程式碼的檢查可是相當嚴格的,像引用了一個本repo中不存在的類,repo的校驗都是通不過的,所以這就逼你把各業務線的程式碼進行歸類,屬於哪條業務線的程式碼就劃分到相應的業務線中,這樣做下來,各業務線最後只保留了和本業務線相關的程式碼,感覺結構上和程式碼上都清晰了不少。

但還有一個新問題,業務A的程式碼呼叫業務B的程式碼怎麼辦?難道要在業務A的程式碼中import業務B的程式碼,那不又耦合了嗎?而且即便可以這樣做,私有pod也不允許我們這樣做,因為在校驗私有repo的時候,這樣的做法根本校驗不通過,為了解決這個問題,我們引入了中間層,讓中間層來解決這個問題,有句話說的好:沒有什麼問題是一箇中間件解決不了的,有就用兩個,這就引出了接下來要講的,元件間的通訊方案。

3.

iOS端通用的元件間通訊方案有如下3種:

  • URL Router
  • Target-Action
  • 面向介面程式設計(Protocol - Class)

接下來說這3種方案的具體實現原理。

URL Router

在前端,一個url表示一個web頁面。
在後端,一個url表示一個請求介面。
iOS,我們要在App中跳轉到手機系統設定中的某個功能時,方式是通過UIApplication開啟一個官方提供的url,相當於一個url也是一個頁面。

所以,參考以上幾種場景,我們也可以用一個url表示一個頁面(Controller),不止可以表示頁面,還可以表示一個檢視(UI控制元件),甚至是任意一個類的物件。

知道可以這麼做,我們就可以建立一個字典,keyurlvalue是相應的物件,這個字典由路由類去管理,典型的方案就是MGJRouter

這種方案的優點是能解決元件間的依賴,並且方案成熟,有很多知名公司都在用這種方案;缺點是編譯階段無法發現潛在bug,並且需要去註冊&維護路由表。

程式碼示例:

註冊路由
[[Router sharedInstance] registerURL:@"myapp://good/detail" with:^UIViewController *{
     return [GoodDetailViewController new];
}];

通過url獲取
UIViewController *vc = [[Router sharedInstance] openURL:@"myapp://good/detail"]

Target-Action

Target-Action可直接譯為目標-行為,在Object-CTarget就是訊息接收者物件,Action就是訊息,比如我們要呼叫Person物件的play方法我們會說向Person物件傳送了一個play訊息,此時Target就是person物件,Action就是play這個方法。

到了專案中,如何利用Target-Action機制進行解耦?別忘了,Object-C這項高階語言同樣支援反射。

之前我們在AViewControllerpushBViewController,需要在AViewController類檔案中importBViewController,這樣二者就會產生耦合,現在利用Target-Action機制,我們不再直接importBViewController,而是利用NSClassFromString(<#NSString * _Nonnull aClassName#>)這個apiBViewController這個字串反射成BViewController這個類,這樣我們就可以根據反射後的類進行例項化,再呼叫例項化物件的各種方法。

利用Target-Action機制,我們可以實現各種靈活的解耦,將任意類的例項化過程封裝到任意一個Target類中,同時,相比於URL RouterTarget-Action也不需要註冊和記憶體佔用,但缺點是,編譯階段無法發現潛在的BUG,而且,開發者所建立的類和定義的方法必須要遵守Target-Action的命名規則,呼叫者可能會因為硬編碼問題導致呼叫失敗。

這種方案對應的開源框架是CTMediator和阿里BeeHive中的Router,二者都是通過反射機制拿到最終的目標類和所需要呼叫的方法(對應的api是NSSelectorFromString(<#NSString * _Nonnull aSelectorName#>)),最終通過runtimeperformSelector:執行targetaction,在action中進行類的例項化操作,根據具體的使用場景來決定是否將例項物件作為當前action的返回值。

這裡不再列舉demoCTMediatorBeeHivegithub中都可以搜到。

面向介面程式設計

我們在第1部分囉嗦了一大堆就是為了給面向介面程式設計這一部分做鋪墊,傳統的MVC+面向物件程式設計的程式設計方式引出的問題我們在第1部分簡單闡述了一些,而除了這些問題之外,還會產生哪些問題?接下來會講述一些例子。

Java中,介面是Interface,在Object-C中,介面是Protocol,所以在Object-C中,面向介面程式設計又被稱為面向協議程式設計,在Swift中,Apple強化了面向介面程式設計這一思想,而這一思想,早已稱為其他語言的主流程式設計思想。

什麼是面向介面程式設計?面向介面程式設計強調我們再設計一個方法或函式時,應該更關注介面而不是具體的實現。

舉個具體的業務需求作為例子:

彈窗幾乎在所有App中都存在,大廠App中的彈窗相對來說比較剋制,除了升級之外的彈窗幾乎見不到其他型別,中小型App中的彈窗就比較多,比如升級彈窗、活動彈窗、廣告彈窗等等,當然,需求複雜的時候,產品還會要求彈窗時機以及彈窗的優先順序等條件。

當我們使用面向物件程式設計思想時,解決方案大概是下面這樣的:
PS:以下程式碼示例基於下面兩個條件
1、如果彈窗介面來自於多個Service
2、如果專案大,彈窗這個業務需求也可能來自於不同的業務線,有時候你無法強制要求其他業務線的開發人員必須使用你定製好的類進行開發,可能你覺得你定義的類能適用很多場景,但人家未必這樣認為。

  • 需求第1期:升級彈窗
資料型別
@interface UpgradePopUps : NSObject
@property(nonatomic, copy) NSString *content;  //內容
@property(nonatomic, copy) NSString *url; //AppStore連結
@property(nonatomic, assign) BOOL must; //是否強制升級
@end

升級彈窗
@interface UpgradView : UIView 
- (void)pop;
@end
  • 需求第2期:廣告彈窗
資料型別
typedef NS_ENUM(NSUInteger, AdType) {
    AdTypeImage,  //圖片
    AdTypeGif,    //GIF
    AdTypeVideo,  //視訊
};

@interface AdPopUps : NSObject
@property(nonatomic, copy) AdType type; //廣告型別
@property(nonatomic, copy) NSString *content;  //內容
@property(nonatomic, copy) NSString *url; //路由url(可能是native頁面也可能是H5)
@end

廣告彈窗
@interface AdView : UIView 
- (void)pop;
@end
  • 需求第3期,彈窗太多了,給加個優先順序,根據優先順序彈窗。
  • 需求第4期,加個活動彈窗,定個優先順序。
  • 需求第5期,加個XX彈窗,定個優先順序。

估計此刻的你應該是這樣的:

現在使用面向介面程式設計思想對業務進行改造,我們抽象出一個介面如下:

@protocol PopUpsProtocol <NSObject>
//活動型別(識別符號)
@property(nonatomic, copy) NSString *type;

//跳轉url
@property(nonatomic, copy) NSString *url;

//文字內容
@property(nonatomic, copy) NSString *content;

@required
//開啟執行,在這個方法中展示出彈窗
- (void)execute;
@end

一個簡單的介面就抽象完了,下次如果有新的彈窗需要接入,只需要讓新的彈窗類遵守這個PopUpsProtocol就可以了,例項化一個彈窗物件的方法如下所示:

id<PopUpsProtocol> popUps = [[AdPopUps alloc] init];
popUps.url = @"...";
popUps.content = @"...";
popUps.type = @"...":

//show
[popUps execute];

AdPopUps中程式碼如下:

@interface AdPopUps : NSObject <PopUpsProtocol>
@property(nonatomic, copy) NSString *type;
@property(nonatomic, copy) NSString *url;
@property(nonatomic, copy) NSString *content;
@end

@implementation AdPopUps
- (void)execute {
    AdView *adView = [AdView alloc] init];
    [adView show];
}
@end

現在我們把這些彈窗事件封裝到Task(任務)物件中,這個自定義物件可以設定優先順序,然後當把這個任務加入到任務佇列後,佇列會根據任務的優先順序進行排序,整個需求就搞定了。下面來看一下Task類:

typedef NS_ENUM(NSUInteger, PopUpsTaskPriority) {
    PopUpsTaskPriorityLow,        //低
    PopUpsTaskPriorityDefault,    //預設
    PopUpsTaskPriorityHigh,       //高
};

@interface MSPopUpsTask : NSObject

//任務的唯一識別符號
@property(nonatomic, copy) NSString *identifier;

//優先順序
@property(nonatomic, assign) PopUpsTaskPriority priority;

//任務對應的活動
@property(nonatomic, strong) id<PopUpsProtocol> activity;

//初始化方法
- (instancetype)initWithPriority:(PopUpsTaskPriority)priority
                        activity:(id<PopUpsProtocol>)activity
                      identifier:(NSString *)identifier;

//執行任務
- (void)handle;

@end


@implementation MSPopUpsTask

- (void)handle {
    if ([_activity respondsToSelector:@selector(execute)]) {
        [_activity execute];
    }
}

@end

大家看到了,Task沒有直接依賴任何PopUps類,而是直接依賴介面PopUpsProtocol

一個面向介面程式設計的小例子這裡就講述完了,這個例子中的對於介面的使用方法只是其中一種,在實際應用中,還有其他使用方法,大家可自行搜尋。

接下來說採用面向介面程式設計思想輸出的程式碼會帶來的哪些好處?

1.介面比物件更直觀

讓程式設計師看一個介面往往比看一個物件及其屬性要直觀和簡單,抽象介面往往比定義屬性更能描述想做的事情,呼叫者只需要關注介面而不用關注具體實現。

2.依賴介面而不是依賴物件

剛才我們使用面向介面程式設計的方式建立了一個物件:

id<PopUpsProtocol> popUps = [[AdPopUps alloc] init];

現在我們除了要引用AdPopUps這個類外,還要引用PopUpsProtocol,一下引用了兩個,好像又把問題複雜化了,所以我們想辦法只引用protocol而不引用類,這個時候就需要把protcol及這個protocol的具體實現類繫結在一起(protocol-class),當我們通過protocol獲取物件的時候,實際上獲取的是遵守了這個protocol協議的物件,那如果一個protocol對應多個實現類怎麼辦?別忘了有工廠模式。

所以,我們需要將ProtocolClass繫結到一起,程式碼大概是這種形式的:

[self bindBlock:^(id objc){
      AdPopUps *ad = [[AdPopUps alloc] init];
      ad.url = @"...";
      return (id<PopUpsProtocol >)ad;
} toProtocol:@protocol(PopUpsProtocol)];

獲取方式就是這樣的:

id<PopUpsProtocol> popUps = [self getObject:@protocol(PopUpsProtocol)];

呼叫方法:

[popUps execute];

這樣就把問題解決了。

好了,我們就可以將這個彈窗管理系統作為一個元件去釋出了,所以,為了實現基於元件的開發,必須有一種機制能同時滿足下面兩個要求:
(1)解除Task對具體彈窗類的強依賴(編譯期依賴)。
(2)在執行時為Task提供正確的彈窗例項,使彈窗管理系統可以正確展示相應的彈窗。

換句話說,就是將TaskPopUps的依賴關係從編譯期推遲到了執行時,所以我們需要把這種依賴關係在一個合適的時機(也就是Task需要用到PopUps的時候)注入到執行時,這就是依賴注入(DI)的由來。

需要注意的是,TaskPopUps的依賴關係是解除不掉的,他們倆的依賴關係依然存在,所以我們總說,解除的是強依賴,解除強依賴的手段就是將依賴關係從編譯期推遲到執行時。

其實不管是哪種程式設計模式,為了實現鬆耦合(服務呼叫者和提供者之間的或者框架和外掛之間的),都需要在必要的位置實現面向介面程式設計,在此基礎之上,還應該有一種方便的機制實現具體型別之間的執行時繫結,這就是依賴注入(DI)所要解決的問題。

如何簡單理解依賴注入?
我們可以將執行中的專案當做是主系統,這些介面及其背後的具體實現就是一個個的外掛,主系統並不依賴任何一個外掛,當外掛被主系統載入的時候,主系統就可以準確呼叫適當外掛的功能。

下面,就要開始分享Object-CDI的具體實現了,這裡需要引入一個框架Objectiongithub上可以搜尋到。

4.

DI往往和IoC聯絡到一起的,IoC更多指IoC容器。

IoC即控制反轉,該怎麼理解IoC這個概念?

簡單理解,從前,我們使用一個物件,除了銷燬之外(iOSARC進行記憶體管理),這個物件的控制權在我們開發人員手裡,這個控制權體現在物件的初始化、對屬性賦值操作等,因為物件的控制權在我們手裡,所以我們可以把這種情況稱為“控制正轉”。

那麼控制反轉就是將控制權交出去,交給IoC容器,讓IoC容器去建立物件,給物件的屬性賦值,這個物件的初始化過程是依賴於DI的,通過DI(依賴注入)實現IOC(控制反轉)

DI提供了幾種注入方式,這裡說幾個最常用的:

  • (1)構造器注入

也就是通過我們指定的初始化方法進行注入,比如針對於Task這個類,它的構造器就是:

- (instancetype)initWithPriority:(PopUpsTaskPriority)priority
                        activity:(id<PopUpsProtocol>)activity
                      identifier:(NSString *)identifier;

IoC容器會根據這個構造器的引數將依賴的屬性注入進來,並完成最終的初始化操作。

(2)屬性注入

也叫setter方法注入,即當前物件只需要為其依賴物件所對應的屬性新增setter方法,IoC容器通過此setter方法將相應的依賴物件設定到被注入物件的方式即setter方法注入。在Java Spring中,可以在XML檔案中配置屬性注入的預設值,比如:

<beans>
  <bean id="Person" class="com.package.Person">
      <property name="name">
          <value>張三</value>
      </property>
  </bean>
</beans>

iOS中可以通過plist檔案來儲存這些預設值。

  • (3)介面注入

介面注入和以上兩種注入方式差不多,但首先你要告訴IoC容器這個介面對應哪個實現類,否則光注入一個介面有什麼用呢?所以我們需要在專案內給每一個介面建立一個實現類,使介面與類是一一對應的關係(protocol-class)。

在上面的例子中,因為Task有個屬性實現了這個PopUpsProtocol介面,所以IoC注入的是這個介面的實現類,所以從這個角度來說,介面注入實際上與setter注入是等價的。

Java Spring中,介面注入同樣是通過XML檔案進行配置的,但現在更多的是用註解來替代XML注入。

5.