天天看點

細說IOS工程架構一、架構模式的選擇二、通用基礎庫與cocoaPod應用三、業務元件&元件通訊

一、架構模式的選擇

1. MVC模式

MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,一種軟體設計典範,用一種業務邏輯、資料、界面顯示分離的方法組織代碼,将業務邏輯聚集到一個部件裡面,在改進和個性化定制界面及使用者互動的同時,不需要重新編寫業務邏輯。MVC被獨特的發展起來用于映射傳統的輸入、處理和輸出功能在一個邏輯的圖形化使用者界面的結構中。

1.1 MVC 程式設計模式

MVC 是一種使用 MVC(Model View Controller 模型-視圖-控制器)設計應用程式的模式

  • 視圖(View):負責資料展示、監聽使用者觸摸等工作
  • 控制器(Controller):負責業務邏輯、事件響應、資料加工等工作
  • 模型(Controller):負責封裝資料、存儲和處理資料運算等工作

1.2 MVC通信特點

細說IOS工程架構一、架構模式的選擇二、通用基礎庫與cocoaPod應用三、業務元件&元件通訊

1. Model和View永遠不能互相通信,隻能通過Controller傳遞。

2. Controller可以直接與Model通信(讀寫調用Model),Model通過Notification和KVO機制與Controller間接通信。

3. Controller與View通過Target/Action,delegate和datasource三種模式進行通信。通過這三種模式,View就可以向Controller通信,Action/Target 模式來讓Controller 監聽View 觸發的事件。View又通過Data source和delegate進行資料擷取和某些通信操作。

1.3 MVC優缺點

1.易用性:與其他幾種模式相比最小的代碼量,維護起來也較為容易。 

2. 可測性:由于糟糕的分散性,隻能對Model進行測試。

3. 均衡性:厚重的ViewController、無處安放的網絡邏輯與資料邏輯。

2. MVCS模式

蘋果自身就采用的是這種架構思路,從名字也能看出,也是基于MVC衍生出來的一套架構。從概念上來說,它拆分的部分是Model部分,拆出來一個Store。這個Store專門負責資料存取。

從實際操作的角度上講,它拆開的是Controller。因為Controller做了資料存儲的事情,就會變得非常龐大,那麼就把Controller專門負責存取資料的那部分抽離出來,交給另一個對象去做,這個對象就是Store。這麼調整之後,整個結構也就變成了真正意義上的MVCS。

  • 視圖(View):使用者界面
  • 控制器(Controller):業務邏輯及處理
  • 模型(Model):資料存儲
  • 存儲器(Store):資料處理邏輯

MVCS是基于瘦Model的一種架構思路,把原本Model要做的很多事情中的其中一部分關于資料存儲的代碼抽象成了Store,在一定程度上降低了Controller的壓力。

3.MVVM模式

MVVM是Model-View-ViewModel的簡寫。它本質上就是MVC 的改進版。MVVM 就是将其中的View 的狀态和行為抽象化,讓我們将視圖 UI 和業務邏輯分開。當然這些事 ViewModel 已經幫我們做了,它可以取出 Model 的資料同時幫忙處理 View 中由于需要展示内容而涉及的業務邏輯。

細說IOS工程架構一、架構模式的選擇二、通用基礎庫與cocoaPod應用三、業務元件&元件通訊

3.1 MVVM模式的組成部分

  • 模型:模型是指代表真實狀态内容的領域模型(面向對象),或指代表内容的資料通路層(以資料為中心)。
  • 視圖:視圖是使用者在螢幕上看到的結構、布局和外觀(UI)。
  • 視圖模型:視圖模型是暴露公共屬性和指令的視圖的抽象。在視圖模型中,綁定器在視圖和資料綁定器之間進行通信。

引用特點:

View <=> Controller <=> ViewModel <=> Model。

在controller中引用view與viewmodel;實作view與model的雙向綁定

viewModel

 負責

model的資料存儲與邏輯處理

3.2 MVVM優點

  • 低耦合:View可以獨立于Model變化和修改,一個ViewModel可以綁定到不同的View上。
  • 可重用性:你可以把一些視圖邏輯放在一個ViewModel裡面,讓很多view重用這段視圖邏輯。
  • 獨立開發:開發人員可以專注于業務邏輯和資料的開發(ViewModel),設計人員可以專注于頁面設計。
  • 可測試:界面素來是比較難于測試的,而現在測試可以針對ViewModel來寫。

備注:MVVM可以結合ReactiveCocoa,更加優雅的實作view與viewmodel的雙向綁定,具體使用可以參照部落格iOS MVVM+RAC 從架構到實戰(MVVM-RAC-DEMO)

二、通用基礎庫與cocoaPod應用

1. 單工程項目

單工程項目的業務代碼統一放在一個project工程,沒有實作業務元件化,适用于混合開發架構(原生業務較少),或者APP團隊規模較小且業務較為單一的情況。業務元件化既需要快速的內建,也可以作為一個單獨的工程跑起來,下文會簡單講一下業務元件相關内容。

下圖是本人在以往單工程項目的架構設計,僅供參考,實際應用以具體項目為主。

細說IOS工程架構一、架構模式的選擇二、通用基礎庫與cocoaPod應用三、業務元件&amp;元件通訊

2. 通用基礎庫的目錄分布

細說IOS工程架構一、架構模式的選擇二、通用基礎庫與cocoaPod應用三、業務元件&amp;元件通訊

【Service】業務服務層,存放公共業務邏輯與通用服務,如公共接口、基礎元件配置、使用者資訊、共享資料、頁面路由配置等。

【BaseKit】基礎層,存放着通用的基礎庫,應用于各個業務子產品。

   CommonLib:通用功能元件,具體功能的實作和應用,如APM、資料上報、頁面路由、OCR等。

   Views:通用視圖元件,如BannerView、AlertView、pageControl、loadingView等。

   Kernal:基礎服務元件,提供基礎服務能力,如Network、Log、dataBase、webImage等。

   Basic:基礎類,又分為MVC三個子目錄,封裝存放Basic類。

   Marco:通用聲明,宏定義-define、常量聲明-extern。   

   Utils:工具類集合,如Tools、Authoritys、Encrypt、NetworkAccessible等 。

   Categorys:公共類别擴充,可按照類屬性的不同分為不同的子目錄,如View、Datas、Image等。

備注:自上而下的依賴關系

3. cocoaPod應用

cocoaPod應用IOS開發者應該都比較熟悉,主要是用關聯第三方庫與私有元件庫,友善版本疊代管理。

3.1 GitHub第三方庫

CocoaPods詳解之-使用篇:CocoaPods詳解之----使用篇_SpeedBoy007的專欄-CSDN部落格

3.2 GitLab私有庫

CocoaPod-spec私有庫配置:cocoapod-podspec私有庫配置_z119901214的部落格-CSDN部落格

三、業務元件&元件通訊

1. 業務元件

業務元件主要是指作為一個大的業務子產品,單獨分離成一個元件的形式,如電商子產品、聊天子產品、部落格等。業務元件之間不存在耦合代碼,由元件通訊中間層實作彼此的通訊。

1.1 組價分層方式

細說IOS工程架構一、架構模式的選擇二、通用基礎庫與cocoaPod應用三、業務元件&amp;元件通訊

通過xcworkspace的方式分層,A為主工程,B、C為兩個業務子產品

  • 目錄分層:在主業務目錄下,按照不同的業務元件建立不同的目錄,業務子產品獨立,元件之間不直接調用API。
  • xcworkspace分層:通過xcworkspace的方式,不同的業務子產品建立各自的project工程,業務project工程run成功後,将framework引入主工程中。
  • pod元件分層:以pod-specs的方式引入業務元件,各個業務子產品都獨立成一個工程,具體可以參考github元件化項目-iOS-Component-Pro

1.2 元件分層方式的特點

  • 目錄分層:目錄分層比較簡單,沒有實作真正的代碼分割,是以目錄與目錄直接的類是可以引用的,這樣就很依賴于團隊開發的規範性。
  • xcworkspace分層:xcworkspace分層實作了不同業務元件代碼的分割,作為framework的形式引入。動态編譯的形式引入會影響到編譯打包的效率;打包成靜态framework的形式引入更新疊代成本比較高,而且不利于cocoaPod的使用。
  • pod元件分層:實作了不同業務元件代碼的分割,又解決了xcworkspace分層影響編譯效率與不利于cocoaPod使用的問題,業務元件與基礎元件統一使用cocoaPod管理。

2. 業務元件之間的通訊

添加中間件實作元件解耦,避免元件之間循環依賴。

2.1 URL-Block(MGJRouter)

/*********** 業務元件中注冊 ***********/
[[MKRouter sharedInstance] registerHandler:^(MKRouteRequest *request) {
// 跳轉至商祥頁
// request.callBack(nil, @{@"productId" : request[@"productId"]});
} forRoute:MKString(@".*product/detail.*\\?(.*)\\&(.*)$")];


/*********** 子產品調用 ***********/
[[MKRouter sharedInstance] handleURL:[NSURL URLWithString:@"weixin://com.apple.iphone/product/detail?productId=ID985632"] params:nil targetCallBack:^(NSError *error, NSDictionary *responseObject) {

}];
           

資料埋點: "appData/click?params='jsonString的URL編碼'"

原生商詳頁: "product/detail?param='jsonString的URL編碼'"

weex商詳頁: "weex/page?params='jsonString的URL編碼'"

web商詳頁: "web/page?params='jsonString的URL編碼'"

flutter商詳頁: "flutter/page?params='jsonString的URL編碼'"

2.2 Target-Action(CTMediator)

/*********** 公共實作 ***********/
#import "MKMediator+Feature.h"

static NSString * const kTargeFeature = @"Feature";

static NSString * const kActionViewControllerForFeature = @"viewControllerForFeature";

@implementation MKMediator (Feature)

- (UIViewController *)mediator_ViewControllerForFeature:(MKFeatureActionModel *)params {
    return  [self call:kTargeFeature action:kActionViewControllerForFeature parameters:params];
}

@end

/*********** 業務元件中實作 ***********/
#import "MKMediator+Feature.h"
#import "MKTarget_Feature.h"
#import "AudioVideoPraticeVC.h"

@implementation MKTarget_Feature

- (UIViewController *)action_viewControllerForFeature:(MKActionModel *)params {
    AudioVideoPraticeVC* audioVC = [[AudioVideoPraticeVC alloc] init];
    return audioVC;
}

@end

/*********** 子產品調用 ***********/
MKActionModel* acitonModel = [[MKActionModel alloc] init];
actionModel.keyValues = @{};
actionModel.complete = ^(id result) {

};
UIViewController* audioVC = [[MKMediator shared] mediator_ViewControllerForFeature:acitonModel];
           

原生->原生錄音頁:直接調用對應業務元件的公共API

外部->原生錄音頁: "urlScheme://appPage?pageId=audioPageId&params="、"urlScheme://appRouter?url=pageUrl"(通過url正則比對到頁面的相關配置,頁面key與映射表比對出對應的原生頁)

打開hybrid容器頁:通過url正則比對到頁面的相關配置,再調用業務容器元件的公共API實作頁面的跳轉和渲染

2.3 Protocol-Class

面向接口程式設計,通過protocol定義協定接口與屬性(注意:protocol的屬性隻是一個聲明,并沒有實際用途,需要實作協定的類本身定義了該屬性)。

/* 定義協定方法與屬性 */
@protocol MKOpenURLProtocol <NSObject>

@property (nonatomic, assign) BOOL isPresent;

- (BOOL)openURLWithURLString:(NSString *)URLString params:(NSDictionary *)params;

@end
           

 MKOpenURLExecutor聲明了MKOpenURLProtocol,并實作其協定方法。

/* 執行者-MKOpenURLExecutor.h */
@interface MKOpenURLExecutor : NSObject <MKOpenURLProtocol>

@property (nonatomic, assign) BOOL isPresent;

@end

/* 實作者-MKOpenURLExecutor.m */
@implementation MKOpenURLExecutor

- (BOOL)openURLWithURLString:(NSString *)URLString params:(NSDictionary *)params {
    // do something
    return YES;
}

@end
           

上面講的這種聲明協定再到類的實作,雖然是應用了接口程式設計的方式,但是業務元件之間還是會有直接調用的關系,是以需要有個中間者實作protocol與接口對象的一一比對。

/* 中間者-MKProtocolManager.h */
@interface MKProtocolManager : NSObject

+ (instancetype)sharedInstance;

- (void)setClass:(Class)protocolClass protocol:(Protocol *)protocol;

- (Class)classWithProtocol:(Protocol *)protocol;

@end

/* 中間者-MKProtocolManager.m */
@interface MKProtocolManager ()

@property (nonatomic, strong) NSMutableDictionary* protocolSet;

@end

@implementation MKProtocolManager

+ (instancetype)sharedInstance {
    static MKProtocolManager* instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
        instance.protocolSet = [NSMutableDictionary dictionary];
    });
    return instance;
}

- (void)setClass:(Class)protocolClass protocol:(Protocol *)protocol {
    if (protocolClass && protocol) {
        [_protocolSet setObject:protocolClass forKey:NSStringFromProtocol(protocol)];
    }
}

- (Class)classWithProtocol:(Protocol *)protocol {
    if (protocol) {
      return [_protocolSet objectForKey:NSStringFromProtocol(protocol)];
    }
    return nil;
}

@end
           

業務元件A的協定與接口對象的綁定,以及其他業務元件中調用業務元件A的協定接口。

/* 業務元件A的協定與接口對象的一一比對 */
+ (void)registerProtocol {
    [[MKProtocolManager sharedInstance] setClass:[MKOpenURLExecutor class] protocol:@protocol(MKOpenURLProtocol)];
}

/* 其他業務元件中調用業務元件A的協定接口 */
+ (void)openURL:(NSString *)URLString params:(id)params {
    Class protocolClass = [[MKProtocolManager sharedInstance] classWithProtocol:@protocol(MKOpenURLProtocol)];

    id <MKOpenURLProtocol> executor = [[protocolClass alloc] init];
    executor.isPresent = NO; // 内部屬性配置

    [executor openURLWithURLString:URLString params:params];
}
           

原生->原生錄音頁:直接調用對應業務元件的公共API

外部->原生錄音頁: "urlScheme://appPage?pageId=audioPageId&params="、"urlScheme://pageRouter?url=pageUrl"(通過url正則比對到頁面的相關配置,頁面key與映射表比對出對應的原生頁)

打開hybrid容器頁:通過url正則比對到頁面的相關配置,再調用業務容器元件的公共API實作頁面的跳轉和渲染

2.4 三種通訊方式的特點

  • URL-Block:能解決元件間的依賴,路由配置靈活,但路由注冊與中間件存在耦合,且需要去注冊&維護路由表,方法調用不夠直覺。
  • Target-Action:統一了元件api服務,接口實作不依賴中間件,但需要額外維護中間件類擴充,方法調用不夠直覺。
  • Protocol-Class:面向接口程式設計,方法調用比較直覺,但協定注冊與中間件存在耦合,且每個業務元件都需要聲明一個對外的協定,和一個用于接口實作的類。

3. 頁面路由規則配置(推送、scheme、通用連結、JSAPI-openURL)

1)頁面路由配置

細說IOS工程架構一、架構模式的選擇二、通用基礎庫與cocoaPod應用三、業務元件&amp;元件通訊

2)具體實作

通過url正則比對到相關的路由配置,生成對應類型的ViewController(scheme和universal link的頁面連結需要url編碼)。Native頁面可以通過MGJRouter的方式生成;或者通過頁面key映射VC類名,用runtime的方式生成。頁面的通用參數用UIViewController類别添加。

native:通過頁面配置資訊與url參數,生成對應的原生頁面,最終實作頁面的跳轉與渲染。

web:直接通過url,生成WebVC,實作頁面跳轉和H5資源加載。

weex:擷取wx的資源連結及頁面配置參數,傳遞給wx容器實作資源加載和頁面渲染。

react naive:擷取rn的資源連結及頁面配置參數,傳遞給rn容器實作資源加載和頁面渲染。

flutter:配置相關的routeName及頁面配置參數,傳遞給Flutter容器實作flutter端頁面的渲染。

3)場景應用

scheme喚起:urlScheme://pageRouter?url=https%3a%2f%2fm.domain.com%2forder_detail.html

universal link喚起:https://host/path?url=https%3a%2f%2fm.domain.com%2forder_detail.html

内部打開(比對頁面路由):https://m.domain.com/order_detail.html

備注:在雲端頁面路由配置較多的情況下,頻繁正則比對會有一定的性能開銷,native與web頁可以不采用雲端下發的路由比對機制,直接通過本地規則實作(以url字首是否為http來區分)。