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

一、前言

随着业务不停地迭代,优酷 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 元素从大到小进行模块的划分,依次是页面、抽屉、组件和坑位。组件由数个相同的坑位组合而成,同理,若干个组件组合成抽屉,若干个抽屉组成页面。

不同层级的模块都各自的功能单元,如下表:

大模块由若干个小模块组合而成,将这些大大小小模块用线段来连成一体,则可以得到一个庞大的树状结构,每个模块相当于树里面的个节点。功能单元则是跟这里的每个节点有着联系,将一个功能单元对应一个或多个插件。模块的功能单元代码由插件承载,模块内外的功能单元通过事件传递消息和数据,再加上 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 间的依赖界限。插件化页面架构是一个很好的起点,我们将会持续地完善和深挖它的能力,最终让其更稳定且高效地支撑业务发展。


https://mp.weixin.qq.com/s/jNnGfWz1n1dNnwp97gUb1g

iOS中的内嵌汇编

写一篇在iOS上使用汇编的文章的想法在脑袋里面停留了很久了,但是迟迟没有动手。虽然早前在做启动耗时优化的工作中,也做过通过拦截objc_msgSend并插入汇编指令来统计方法调用耗时的工作,但也只仅此而已。刚好最近的时间项目在做安全加固,需要写更多的汇编来提高安全性(文章内汇编使用指令集为ARM64),也就有了本文

发布于:10天以前  |  54次阅读  |  详细内容 »

不会吧,这也行?iOS后台锁屏监听摇一摇

一般情况下,出于省电、权限、合理性等因素考虑,给人的感觉是很多奇怪的需求安卓可以实现,但是iOS就无法实现!今天要介绍的需求也有这种感觉,就是“当 APP 处于后台或锁屏状态时,依旧可以监听到摇一摇,进而触发某些功能,比如:语音播报”。

发布于:1月以前  |  121次阅读  |  详细内容 »

iOS 稳定性:App 被终止的原因

本次 session 主要内容如下: 介绍了后台应用终止的常见原因,并提供了一些优化建议 介绍了 MetricsKit 提供的在代码中获取诊断和性能数据的方法 介绍了 Xcode Metrics Ogranizer 提供的关于线上用户性能数据的可视化报告

发布于:1月以前  |  150次阅读  |  详细内容 »

Vue中Axios的封装和API接口的管理

在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。如果还对axios不了解的,可以移步axios文档。

发布于:1月以前  |  153次阅读  |  详细内容 »

iOS 持续集成:更完备的 App Store Connect API

时隔两年 App Store Connect API 有了更新,WWDC 2018 推出了 App Store Connect API ,用于自动化一些 App Store Connect 后台操作。这次更新包含了 app 元数据相关的API,补上了原来缺失的重要一环, 使得几乎可以通过 App Store Connect API 完成 App Store Connect 上的所有操作。今后开发、证书配置、用户管理、测试、发布全流程都可以通过 API 完成。

发布于:1月以前  |  190次阅读  |  详细内容 »

iOS 性能优化:优化 App 启动速度

苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote[1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%

发布于:1月以前  |  178次阅读  |  详细内容 »

让你的应用远离越狱:iOS 14 App Attest 防护功能

当越狱在 iOS 设备第一次流行起来时,iOS 开发人员会尝试各种方法来保护自己的应用程序,以让应用免受盗版等不确定因素的困扰。有许多方法可以做到这一点,包括检查 Cydia 是否存在、检测应用程序是否可读取自身沙箱之外的文件、在检测到调试器时让应用程序崩溃等等。

发布于:1月以前  |  195次阅读  |  详细内容 »

探秘 iOS 14 的 WidgetKit

Widget Extension 提供了 small, medium, large 三个尺寸,不同尺寸可以展示不同的数据、不同的界面,开发者也可以锁定自己APP的 Widget 只有某类尺寸,相同的widget也能重复添加。作为添加在主屏幕上的控件,苹果用了 “At a glance” 来形容 widget ,所以 widget extension 是无法交互的,它能做的只有展示一些信息与点击两个作用,点击后就会引导至app,同时为了性能与耗电量的考虑,Widget extension 也不能展示视频和动态图像。

发布于:1月以前  |  211次阅读  |  详细内容 »

iOS14 Widget 万字指北,先人一步获得顶级流量

2020 年 6 月 22 日,苹果召开了第一次线上的开发者大会 - WWDC20。这次发布会上宣布了ARM架构Mac芯片(拳打Intel)、iOS 14 ATT(脚踢Facebook),可谓是一次载入史册(我是爸爸)的发布会了,当然还发布了被称为下一个顶级流量入口的Widget。踩着八月的尾巴,本次我们就来探究一下Widget。本文会从Widget初窥和Widget开发两个维度和章节来探究一下Widget, 其中初窥章节会带您简单的了解一下Widget,适合应用决策者阅读; 开发章节会带着您一步一步的完成设计开发Widget,适合程序员阅读。

发布于:1月以前  |  264次阅读  |  详细内容 »

OCRunner:完全体的iOS热修复方案

为了能够实现一篇文章的思路:Objective-C源码 -> 二进制补丁文件 ->热更新(具体是哪篇我忘了)。当时刚好开始了oc2mango翻译器的漫漫长路(顺带为了学习编译原理,嘻嘻),等基本完成以后,就开始肝OCRunner:完全兼容struct,enum,系统C函数调用,魔改libffi,生成补丁文件等,尽可能兼容Objective-C,为了做一个直接运行OC的快乐人。

发布于:1月以前  |  219次阅读  |  详细内容 »

iOS APP图标版本化

在我们的项目开发过程中,需要频繁打包给测试人员去测试,有时候我们都不知道测试机上安装的版本是否是最新的,这样会造成很多不必要的麻烦和成本。因此我们需要将buildNumber以水印的方式打在APPIcon上,可以很直观的知道当前是哪一个版本。

发布于:1月以前  |  206次阅读  |  详细内容 »

百度App iOS工程化实践: EasyBox破冰之旅

百度App从单一的搜索工具发展到今天以搜索和Feed流为双引擎的综合性内容消费服务平台,其复杂程度已然不可同日而语矣。 作为一个日活过亿的超级App,业务规模庞大,相关技术人员超过千人,客户端支持主流的移动技术,涉及近百业务方,技术形态复杂,各种组件近三百个,代码百万量级,由此带来的工程化问题是技术团队的一个极大挑战。

发布于:1月以前  |  227次阅读  |  详细内容 »

最多阅读

快速配置 Sign In with Apple 1年以前  |  3443次阅读
开篇 关于iOS越狱开发 1年以前  |  2454次阅读
给数组NSMutableArray排序 1年以前  |  2404次阅读
APP适配iOS11 1年以前  |  2376次阅读
在越狱的iPhone设置上使用lldb调试 1年以前  |  2325次阅读
UITableViewCell高亮效果实现 1年以前  |  2229次阅读
使用 GPUImage 实现一个简单相机 1年以前  |  2220次阅读
App Store 审核指南[2017年最新版本] 1年以前  |  2198次阅读
所有iPhone设备尺寸汇总 1年以前  |  2122次阅读
使用ssh访问越狱iPhone的两种方式 1年以前  |  2040次阅读
关于Xcode不能打印崩溃日志 1年以前  |  2013次阅读
使用ssh 访问越狱iPhone的两种方式 1年以前  |  1894次阅读
UIDevice的简单使用 1年以前  |  1737次阅读
为对象添加一个释放时触发的block 1年以前  |  1699次阅读
使用最高权限操作iPhone手机 1年以前  |  1663次阅读

Whoops, looks like something went wrong.