一、前言
随着业务不停地迭代,优酷 APP 用于散发视频资源的 UI 控件越写越多,也越来越简单,并且同时类似相近的代码也十分多。认真钻研之后,发现是很多耦合导致的问题:
1)布局代码耦合数据模型,类似布局组件各自一套布局代码;
2)数据模型、UIView 继承关系太长,改变时牵一发而动全身,为保险计不得不自立门户;
3)依赖引入,一个组件在另一 bundle 下应用时将引入连串依赖。
有鉴于此,咱们须要寻找一种可能进一步升高通用能力接入门槛,晋升单个组件的开发效率;进一步升高组件与页面的耦合,建设各类组件的在不同页面的通用投放能力的架构。
二、插件化页面架构的摸索
咱们先来看一份 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 化 – 极简数据组织模式,减除因数据模型引入的依赖。
三、从业务模块梳理到架构概述
咱们联合优酷 APP 业务将 UI 元素从大到小进行模块的划分,顺次是页面、抽屉、组件和坑位。组件由数个雷同的坑位组合而成,同理,若干个组件组合成抽屉,若干个抽屉组成页面。
增加形容
不同层级的模块都各自的性能单元,如下表:
模块层级
性能单元
父页面
页卡容器、埋点统计(PV)
页面
NavigationBar 列表容器(CollectionView/TableView)高低拉刷新提醒面板(空数据、网络异样)页面级网络数据申请页面级数据缓存埋点统计(PV)
抽屉
列表容器抽屉级布局治理(平铺、多 Tab 翻页抽屉级网络数据申请
组件
列表容器组件级布局治理(多行多列平铺、瀑布流、横滑、轮播)组件级网络数据申请
坑位
UI 单元(即具体的、部分的 UI 实现)手势响应(单击、双击、长按)路由跳转埋点统计(点击、曝光、播放)
大模块由若干个小模块组合而成,将这些大大小小模块用线段来连成一体,则能够失去一个宏大的树状构造,每个模块相当于树外面的个节点。性能单元则是跟这里的每个节点有着分割,将一个性能单元对应一个或多个插件。模块的性能单元代码由插件承载,模块内外的性能单元通过事件传递音讯和数据,再加上 Key-Value 化数据存储,这样咱们就能够得出这个架构的雏形,综合整顿后得出四大外围 Manager:
1)ModuleManager 负责模块的生命周期和关系治理;
2)PluginManager 负责模块与插件的关系治理;
3)EventManager 负责模块内外,插件与插件之间的音讯通信;
4)DataManager 负责模块的数据管理。
在此基础上,咱们将罕用的列表容器、UI 布局逻辑、埋点统计逻辑、网络申请逻辑、用户交互手势逻辑、路由跳转逻辑等通用逻辑进行形象插件化革新,最终造成 4+N 的架构组成。
增加形容
四、模块示意与治理
如何示意一个模块,是咱们首要解决的问题。在事实世界中,咱们用身份证 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
五、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)平安的读写接口,对数据进行惯例易错的类型查看、合法性检查等。
六、性能单元插件化
用 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)跨模块的一项业务性能,即一个跨模块性能单元,由分属多个模块的多个插件协同承载。
插件与模块之间的分割通过配置文件申明,每个模块在初始化之时,通过配置文件的记录,把与之关联的插件进行初始化和绑定,插件订阅具体事件并开始运作事件机制,直到模块被登记,插件勾销订阅所有事件并完结生命周期。
七、架构实际
本章节用图来阐明如何应用插件化来编写一个按钮性能。一个页面上有一个按钮并反对点击跳转。
咱们将这个性能看作一个单元整体简略地用一个插件实现:
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 间的依赖界线。插件化页面架构是一个很好的终点,咱们将会继续地欠缺和深挖它的能力,最终让其更稳固且高效地撑持业务倒退。