优酷iOS插件化页面架构方法

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)插件边界清晰,明确输入输出。

  1. 事件机制 - 更灵活的通讯方式

事件机制采用“发布 - 订阅”设计模式,功能单元经过发布事件来驱动信息的流转,经过订阅事件来接收并处理信息。信息收发双方按事前约定的事件名进行通讯,事件处理中枢负责事件的派发,所以收发双方不存在直接依赖。值得留意的是事件机制中的信息接收方能够是多个。

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}

  1. 在插件中使用事件机制

咱们把插件看成是事件机制用订阅者,同时容许在处理事件的实现中,发起一个新的事件。这样就可使得插件与插件之间经过事件串联起来,协力地完成一项完整的业务逻辑。

在插件间的通讯上,除了事件机制协议外,就只有事件名的依赖(事件参数中不推荐使用自定义数据类型,不然将从新引入显式依赖),事件名自己是一串字符串,这能够减小因调用引发的各类功能单元间头文件依赖。

用插件来承载业务逻辑的实现上具备很是灵活的特性,开发者可根据本身的判断来决定插件的规模,插件的粒度可大可小,插件内部实现也可随时停止使用事件机制并转回其余通常的类与类、类与协议机制来实现具体的业务逻辑。

在插件的使用上具备很是灵活的特性,所以咱们约定插件边界必须清晰,必须作到单一职责原则,输入输出明确并足够简单,若是不知足以上条件,则表示该插件有拆解细分的可能性和必要。

  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 间的依赖界限。插件化页面架构是一个很好的起点,咱们将会持续地完善和深挖它的能力,最终让其更稳定且高效地支撑业务发展。

相关文章
相关标签/搜索