1、前言編程
隨着業務不停地迭代,優酷 APP 用於分發視頻資源的 UI 控件越寫越多,也愈來愈複雜,而且同時類似相近的代碼也很是多。仔細研究以後,發現是不少耦合致使的問題:設計模式
1)佈局代碼耦合數據模型,類似佈局組件各自一套佈局代碼;緩存
2)數據模型、UIView 繼承關係太長,改動時牽一髮而動全身,爲保險計不得不自立門戶;安全
3)依賴引入,一個組件在另外一 bundle 下使用時將引入連串依賴。網絡
有鑑於此,咱們須要尋找一種可以進一步下降通用能力接入門檻,提高單個組件的開發效率;進一步下降組件與頁面的耦合,創建各種組件的在不一樣頁面的通用投放能力的架構。架構
2、插件化頁面架構的探索框架
咱們先來看一份 ViewController 代碼節選,ViewController 內實現 3 個 feature 分別是 A,B,C,而且這些稍微複雜的 feature 沒法一次性單步完成(具體一點的話,能夠聯想成這是一些用戶交互的 feature、網絡請求等),在某一時機觸發,接着在某回調完成餘下操做,最終構成了一個完整的 feature。模塊化
複製代碼工具
@implementation ViewController - (void)viewDidLoad { [featureA step1]; [featureB step1]; [featureC step1];} - (void)callback_xxx { [featureA step2]; [featureB step2];} - (void)callback_yyy { [featureC step2];} @end佈局
這是一種基本的代碼組織形式,可是面臨着兩個痛點:
一是依賴爆炸問題,每接入一個 feature 就無可避免地引入一批依賴,當 feature 數量上去以後,光是 import 語句都好幾十行;
二是代碼分散問題,同一 feature 相關代碼分散在各處 callback,複用到另外一 ViewController 或者將其廢棄下架都必需要求開發者對該 feature 每一步驟甚至每一行代碼都極爲熟悉。如何才能解決上述痛點是咱們在作架構藍圖時的一個突破口。這時,試圖把圍繞 ViewContorller 的代碼組織形式轉變成圍繞 feature 代碼組織形式,那麼就可獲得下面 3 段代碼節選:
複製代碼
@implementation FeatureA - (void)recvViewDidLoad { [self step1];} - (void)recvCallback_xxx { [self step2];} @end
複製代碼
@implementation FeatureB - (void)recvViewDidLoad { [self step1];} - (void)recvCallback_xxx { [self step2];} @end
複製代碼
@implementation FeatureC - (void)recvViewDidLoad { [self step1];} - (void)recvCallback_yyy { [self step2];} @end
不難發現,代碼通過從新組織以後分散的問題已經迎刃而解。依賴爆炸的問題在單個 feature 上來看,多個依賴已收斂到 feature 內部,接入 feature 的時候依賴已從 N 個降至 1 個,只要使用得當的方式,也可把最後一個依賴也一併消除。
此時須要發揮一下咱們的想象力,把每一個 feature 想象成是一個電器,它們都配有統一規格的插頭。ViewController 比如一個插線板,電器不管插在哪一個板上也是能夠工做的。推而廣之,不只 ViewController 是一塊插線板,任意一個類也看看做爲一塊插線板,它們的功能業務邏輯依然以 feature 的模式來組織。插件化頁面架構的基調就被肯定了。
插件化是業內廣泛使用的解耦方案之一,咱們不約而同地朝着這一方向來對現架構的改造,同時結合優酷的實際狀況,得出一套以模塊化、插件化、數據 Key-Value 化爲特色的頁面架構框架。
1)模塊化 – 業務實體進行模塊化,模塊與模塊呈現必定的組織形式;
2)插件化 – 功能單元插件化,知足功能單元可組合、可拆解、可替換;
3)數據 Key-Value 化 – 極簡數據組織形式,減除因數據模型引入的依賴。
3、從業務模塊梳理到架構概述
咱們結合優酷 APP 業務將 UI 元素從大到小進行模塊的劃分,依次是頁面、抽屜、組件和坑位。組件由數個相同的坑位組合而成,同理,若干個組件組合成抽屜,若干個抽屜組成頁面。
添加描述
不一樣層級的模塊都各自的功能單元,以下表:
模塊層級
功能單元
父頁面
頁卡容器、埋點統計(PV)
頁面
NavigationBar列表容器(CollectionView/TableView)上下拉刷新提示面板(空數據、網絡異常)頁面級網絡數據請求頁面級數據緩存埋點統計(PV)
抽屜
列表容器抽屜級佈局管理(平鋪、多 Tab 翻頁抽屜級網絡數據請求
組件
列表容器組件級佈局管理(多行多列平鋪、瀑布流、橫滑、輪播)組件級網絡數據請求
坑位
UI 單元(即具體的、局部的 UI 實現)手勢響應(單擊、雙擊、長按)路由跳轉埋點統計(點擊、曝光、播放)
大模塊由若干個小模塊組合而成,將這些大大小小模塊用線段來連成一體,則能夠獲得一個龐大的樹狀結構,每一個模塊至關於樹裏面的個節點。功能單元則是跟這裏的每一個節點有着聯繫,將一個功能單元對應一個或多個插件。模塊的功能單元代碼由插件承載,模塊內外的功能單元經過事件傳遞消息和數據,再加上 Key-Value 化數據存儲,這樣咱們就能夠得出這個架構的雛形,綜合整理後得出四大核心 Manager:
1)ModuleManager 負責模塊的生命週期和關係管理;
2)PluginManager 負責模塊與插件的關係管理;
3)EventManager 負責模塊內外,插件與插件之間的消息通訊;
4)DataManager 負責模塊的數據管理。
在此基礎上,咱們將經常使用的列表容器、UI 佈局邏輯、埋點統計邏輯、網絡請求邏輯、用戶交互手勢邏輯、路由跳轉邏輯等通用邏輯進行抽象插件化改造,最終造成 4+N 的架構組成。
添加描述
4、模塊表示與管理
如何表示一個模塊,是咱們首要解決的問題。在現實世界中,咱們用身份證 ID 來區分每個人,一樣地每一個模塊都應有惟一標識的 ID。模塊 ID 在整個架構體系中屬於核心中的核心,使用上也很是頻繁,如數據的讀取、消息的傳遞、實體之間的關聯和綁定。咱們用 Context 類的對象來表示一個模塊,最簡單的 Context 類有且僅有一個 ID 屬性。在這裏咱們特別地定義和引入了 ModuleProtocol,若是其餘通常類也遵照這個協議,那麼咱們就能夠把這樣的實例對象看做與該同一模塊 ID 所表示的模塊有所關聯。
複製代碼
@protocol SCModuleProtocol <NSObject> // 注:SC 爲代碼的統一前綴,下同 @property (nonatomic, strong) NSString *scModule; /// 模塊 Id,全局惟一 @end @interface SCContext : NSObject <SCModuleProtocol> @end
咱們根據業務模塊頁面、抽屜、組件、坑位四級劃分,分別制定 PageContext/CardContext/ComponentContext/ItemContext,同時在 Context 類內創建弱引用屬性來方便各層級下不一樣模塊之間的使用。概括起來 Context 類兩大做用:一是表示模塊自己,二是模塊關係的語法糖。
ModuleManager 負責模塊的生命週期管理和模塊的關係管理,包含註冊模塊、註銷模塊、查詢模塊的上下級模塊等接口。
複製代碼
@interface SCModuleManager : NSObject + (instancetype)sharedInstance; - (void)registerModule:(NSString )module supermodule:(NSString )supermodule;/// 註冊模塊 - (void)unregisterModule:(NSString )module; /// 註銷模塊 - (NSString )querySupermodule:(NSString )module; /// 查詢父模塊 - (NSArray<NSString *> )querySubmodules:(NSString *)module; /// 查詢子模塊 @end
5、Key-Value 化數據存儲
爲了減除數據模型引入的依賴,採用了 Key-Value 存儲方案,用字符串做 Key,並約定 Value 只使用基本數據類型( int/double/bool 等)、字符串( NSString )、集合類型( NSArray/NSMutableArray/NSDictionary/NSMutableDictionary )和其餘系統提供的數據類型(NSValue 等),在數據的使用上弱化自定義數據模型(協議)的使用。
複製代碼
// 寫入數據[[SCDataManager sharedInstance] setdata:propertyValue forKey:propertyKeymoduleId:moduleId]; // 讀取數據[[SCDataManager sharedInstance] dataForKey:propertyKey moduleId:moduleId];
每一個模塊的數據都存放在數據中心內。數據中心爲每一個模塊開闢一塊獨立的空間存放數據,這是保證不一樣模塊數據不串擾又同時保證同一模塊內數據共享。同一模塊下只需字段名參數即可讀寫數據;不一樣模塊下也只是多增長一項目標模塊 ID 參數即可讀取數據。即:
在數據中心使用上,必須注意的是:563513413,無論你是大牛仍是小白都歡迎入駐
1)Key-Value 化存儲目的是減除數據模型的依賴,應避免 Value 使用自定義類型,不然失去了 Key-Value 化自己的價值;
2)不是全部的數據都須要存放在數據中心,只將公開化數據放入數據中心,而私有化數據(如臨時變量等)則不建議放入數據中心。
在數據中心的能力設計上,咱們提供了:
1)提供強引用和弱引用兩種存儲方案,開發者按需使用;
2)安全的讀寫接口,對數據進行常規易錯的類型檢查、合法性檢查等。
6、功能單元插件化
用 ViewController 來舉例,在野蠻生長 iOS 開發時代,把列表邏輯、網絡請求邏輯、 Navigationbar 邏輯等諸多功能單元都攤開在 ViewController 來實現。ViewController 實現個各式各樣的協議,以致於 ViewController 的代碼愈來愈臃腫。到了後來爲這個問題,明確劃定功能單元的邊界,加入了各類 Manager,各功能單元邏輯實如今 Manager 內部,ViewController 只負責諸多 Manager 之間來回調度,臃腫的問題得以緩解。
日益豐富和複雜的業務邏輯下,只解決代碼臃腫是不夠的,還需解決靈活調用、代碼複用的問題。在實際實踐中,經常遇到下列問題:
1)功能單元接口設計變形,之間不時出現相互調用形成「你中有我,我中有你」的高度耦合,維護成本愈來愈高;
2)功能單元個性化定製引出繼承鏈的問題:不一樣業務的子類太多,父類牽一髮動全身,很差改也不敢改,補丁補上補;
3)功能單元複用成本高,複用一小塊,依賴一大片,形成代碼複用意願低。接入方寧願重寫一遍或將相關代碼 Copy&Rename 一遍。
功能單元插件化目標是進一步下降功能單元之間的耦合。插件化思路和原則須要保證上述問題獲得有效解決。
1)輕量化接入。減小甚至消滅類與類,類與協議引用依賴;
2)插件可組合、可拆解、可替換,業務邏輯上下游相關方能作到無感知;
3)插件邊界清晰,明確輸入輸出。
事件機制採用「發佈 - 訂閱」設計模式,功能單元經過發佈事件來驅動信息的流轉,經過訂閱事件來接收並處理信息。信息收發雙方按事前約定的事件名進行通訊,事件處理中樞負責事件的派發,所以收發雙方不存在直接依賴。值得留意的是事件機制中的信息接收方能夠是多個。
EventManager 擔當起事件處理中樞的角色,發佈者經過 EventManager 發佈事件, EventManger 以訂閱優先級從高到低把事件分發到訂閱者。高優先級訂閱者處理完事件後將返回值(若有)交給 EventManager,EventManager 將上一訂閱者返回值(若有)和發佈者入參一同分發到下一訂閱者,如此往復直到全部訂閱者處理完畢,此時 EventManager 將最終返回值(若有)輸出給發佈者。圖示以下:
添加描述
事件發佈與事件訂閱及處理的代碼示例:
複製代碼
// 事件發佈NSString eventName = @"demoEvent";NSString moduleId = ...;NSDictionary params = @{...}; NSDictionary response = [[SCEventManager sharedInstance] fireEvent:eventName module:moduleId params:params]; // 事件訂閱、處理+ (NSArray )scEventHandlerInfo{ return @[@{@「event": @"demoEvent", @"selector": @"receiveDemoEvent:", @"priority": @500}, ];}{1}- (void)receiveDemoEvent:(SCEvent )event{ //do something ... event.responseInfo = @{...}; // 返回值 (可選);}{1}
咱們把插件看成是事件機制用訂閱者,同時容許在處理事件的實現中,發起一個新的事件。這樣就可使得插件與插件之間經過事件串聯起來,協力地完成一項完整的業務邏輯。
在插件間的通訊上,除了事件機制協議外,就只有事件名的依賴(事件參數中不推薦使用自定義數據類型,不然將從新引入顯式依賴),事件名自己是一串字符串,這能夠減小因調用引發的各類功能單元間頭文件依賴。
用插件來承載業務邏輯的實現上具備很是靈活的特性,開發者可根據本身的判斷來決定插件的規模,插件的粒度可大可小,插件內部實現也可隨時停止使用事件機制並轉回其餘通常的類與類、類與協議機制來實現具體的業務邏輯。
在插件的使用上具備很是靈活的特性,所以咱們約定插件邊界必須清晰,必須作到單一職責原則,輸入輸出明確並足夠簡單,若是不知足以上條件,則表示該插件有拆解細分的可能性和必要。
插件、功能單元和模塊的關係有如下 4 點:
1)一個模塊實例關聯多個插件實例,但一個插件實例僅對應一個模塊實例;
2)模塊初始化時,完成所有所屬插件的掛載,插件的生命週期與模塊的生命週期基本同步,不容許中途某一時刻外掛或卸載某一插件;
3)單一模塊內的一項業務功能,即一個功能單元,由一個或多個插件組成承載;
4)跨模塊的一項業務功能,即一個跨模塊功能單元,由分屬多個模塊的多個插件協同承載。
插件與模塊之間的聯繫經過配置文件聲明,每一個模塊在初始化之時,經過配置文件的記載,把與之關聯的插件進行初始化和綁定,插件訂閱具體事件並開始運做事件機制,直到模塊被註銷,插件取消訂閱全部事件並結束生命週期。
7、架構實踐
本章節用圖來講明如何使用插件化來編寫一個按鈕功能。一個頁面上有一個按鈕並支持點擊跳轉。
咱們將這個功能看做一個單元總體簡單地用一個插件實現:
1)在 ViewController 初始化的時候進行模塊註冊,經過一系列 Manager 初始化 ButtonPlugin;
2)在 ButtonPlugin 內收斂全部 Button 相關邏輯,ViewController 不會直接出現與 Button 有關的代碼;
3)ViewController 發送 ViewDIDLoad 事件來驅動其餘插件工做;
4)ButtonPlugin 接收 ViewDIDLoad 事件,進行初始化、添加到 ViewController 等操做,當用戶點擊屏幕時,自行處理 Tap 操做。
添加描述
按鈕的點擊會涉及到統計和跳轉兩部分邏輯,因此 ButtonPlugin 實際上可拆出爲另外 2 個插件來分別實現其邏輯。
添加描述
咱們能夠看見點擊行爲拆分爲跳轉和統計 2 個插件後,插件的職責更加單一,可複用性大大獲得了提高。若遇到產品提出新的點擊需求,如跳轉前必須檢查是否登陸狀態,未登陸者須要先登陸再繼續後續的操做。那麼咱們在現有基礎上只須要多增長一個 LoginCheckPlugin 來處理這些邏輯而且不須要修改原有 plugin 代碼,這也是插件化其中的一個優點。
結語;
只有合適的架構,沒有最好的架構。插件化頁面架構有利也有弊,它顛覆了 MVC 架構的開發體驗,增長了開發者學習成本,編譯器也沒法幫助開發者編譯時(事件名錯配等)校驗。所以,咱們充分發揮它的面向切面編程能力,在開發過程當中,咱們經過插件的形式加入調試類和監控類邏輯來緩解架構的不足,另外一方面則創建標準化插件管理平臺對全部插件進行系統化管理。與此同時,標準化事件的開發方式使得存在統一的邏輯收口,極大地方便了代碼調試、線上問題定位等工具的建設。
優酷 APP 主要場景已接入插件化頁面架構,包括首頁、熱點、會員、我的中心、搜索、播放頁等六大板塊。沉澱了 CollectionView、網絡請求、手勢處理、路由跳轉、埋點統計等各系列系統性插件。
在搭建新頁面時,將上述各系列插件經過以配置加調參的形式便可快速接入和實現已有功能。同時也得益於愈來愈完善的列表佈局插件,使得在開發如橫滑、瀑布流、輪播等複雜佈局組件與開發平鋪組件時效一致。據粗略的測算,組件的開發效率提高了 30% 以上。同時經過統一的配置格式使得客戶端具有組件跨頁面、跨板塊投放能力,打破了 framework 間的依賴界限。插件化頁面架構是一個很好的起點,咱們將會持續地完善和深挖它的能力,最終讓其更穩定且高效地支撐業務發展。