【iOS 客户端专场 学习资料四】第四届字节跳动青训营

998 阅读1小时+

第四届字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~

第十节:iOS 网络与存储编程原理

课程大纲

image.png

了解App存储与网络的作用

互联网App一览

  1. 移动支付强调安全,核心数据都存放在服务器,与服务器交互使用https加密
  1. QQ是一个基于TCP/UDP协议的通讯软件, 网络交互采用点对点的传输。而由于隐私安全原因,这部分数据往往都是存储在用户本地的,需要用户自行决定如何备份。
  1. 王者荣耀、原神这类的游戏,游戏资源非常大,很多资源数据会放在App包中,下载后即可使用,而后续的资源通过网络下载存到本地,以后打开就非常快速了, 且这类moba游戏,大都用udp进行游戏数据同步,并在本地进行UI的渲染。
  1. 短视频和直播,这类多媒体的数据,大都直接来自于网络,且针对这种一对多的网络数据传输,往往也有特殊的广播策略。 而为了应对弱网、断网等情况,App也会对 注入首屏广告、首页视频等内容进行本地缓存,或是预下载。(这里补充b站的) 而为了应对弱网、断网等情况,App也会对 注入首屏广告、首页视频等内容进行本地缓存,或是预下载。(这里补充b站的)

存储与网络涉及哪些知识

推荐阅读

系统IO

计算机组成指的是系统结构的逻辑实现,包括机器机内的数据流和控制流的组成及逻辑设计等。 主要分为五个部分:控制器,运算器,存储器,输入设备,输出设备。

输入/输出(I/O)是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。输入操作是从 I/O 设备复制数据到主存,而输出操作是从主存复制数据到 I/O 设备。

网络编程

网络应用依赖于很多在系统研究中已经学习过的概念。例如,进程、信号、字节顺序、内存映射以及动态内存分配,都扮演着重要的角色。

还有一些新概念要掌握。我们需要理解基本的客户端 - 服务器(C/S)编程模型,以及如何编写使用因特网提供的服务的客户端程序。最后,我们将把所有这些概念结合起来,开发一个虽小但功能齐全的 待办事项Demo。

iOS文件管理与序列化

iOS文件管理

developer.apple.com/library/arc…

www.yuukizoom.top/2021/03/15/…

了解MacOS(类unix)系统的文件系统

DirectoryDescription
/ApplicationsSelf explanatory, this is where your Mac’s applications are kept
/DeveloperThe Developer directory appears only if you have installed Apple’s Developer Tools, and no surprise, contains developer related tools, documentation, and files.
/LibraryShared libraries, files necessary for the operating system to function properly, including settings, preferences, and other necessities (note: you also have a Libraries folder in your home directory, which holds files specific to that user).
/Networklargely self explanatory, network related devices, servers, libraries, etc
/SystemSystem related files, libraries, preferences, critical for the proper function of Mac OS X
/UsersAll user accounts on the machine and their accompanying unique files, settings, etc. Much like /home in Linux
/VolumesMounted devices and volumes, either virtual or real, such as hard disks, CD’s, DVD’s, DMG mounts, etc
/Root directory, present on virtually all UNIX based file systems. Parent directory of all other files
/binEssential common binaries, holds files and programs needed to boot the operating system and run properly
/etcMachine local system configuration, holds administrative, configuration, and other system files
/devDevice files, all files that represent peripheral devices including keyboards, mice, trackpad, etc
/usrSecond major hierarchy, includes sub-directories that contain information, configuration files, and other essentials used by the operating system
/sbinEssential system binaries, contains utilities for system administration
/tmpTemporary files, caches, etc
/varVariable data, contains files whose contents change as the operating system runs

osxdaily.com/2007/03/30/…

\

iOS沙盒机制

wizardforcel.gitbooks.io/ios-sec-wik…

iPhone手机里的文件管理器

\

\

\

\

\

模拟器中的沙盒目录

\

\

\

\

\

\

\

\

\

\

\

NSFileManager

juejin.cn/post/684490…

中文翻译过来就是,文件管理器,(参考你的finder, 能做的事情和你在桌面系统上能做的事情基本一致)

  • 创建、移动、复制、删除文件或文件夹
  • 修改文件内容
  • 修改文件属性
  • 等等

\

获取沙盒根目录路径

NSString*homeDir = NSHomeDirectory();

\

其他文件、目录操作

 //获取Documents目录路径 NSString*docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) firstObject];
//获取Library的目录路径 NSString*libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES) lastObject];
//获取cache目录路径(/Library/Caches) NSString*cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) firstObject];
//获取tmp目录路径 NSString*tmpDir =NSTemporaryDirectory();


// 获取应用程序程序包中资源文件路径的方法:
NSLog(@"%@",[[NSBundle mainBundle] bundlePath]);
NSString*imagePath = [[NSBundle mainBundle] pathForResource:@"apple"ofType:@"png"];
UIImage*appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];

//由于文件管理相关API非常简单,这些API同学课下自行完成熟悉。最常用的的还是掌握NSData到文件的读写,也就是我们常说的磁盘IO。
//创建一个文件并写入数据
- (BOOL)createFileAtPath:(NSString *)path contents:(NSData *)data attributes:(NSDictionary *)attr;
//从一个文件中读取数据
- (NSData *)contentsAtPath:(NSString *)path;
//scrPath路径上的文件移动到dstPath路径上,注意这里的路径是文件路径而不是目录
- (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **) error;
//scrPath路径上的文件复制到dstPath路径上
- (BOOL)copyItemAtPath:(NSString *)scrPath toPath:(NSString *)dstPath error:(NSError **) error;
//比较两个文件的内容是否一样
- (BOOL)contentsEqualAtPath:(NSString *)path1 andPath:(NSString *)path2;
//文件是否存在
- (BOOL)fileExistsAtPath:(NSString *)path;
//移除文件
- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **) error;

\

NSBundle

什么是Bundle

  1. Apple 使用Bundle(捆绑包)来表示应用程序、框架、插件和许多其他特定类型的内容。
  1. 捆绑包将其包含的资源组织到定义明确的子目录中,捆绑包结构因平台和捆绑包类型而异。通过使用包对象,您可以在不知道包的结构的情况下访问包的资源。
  1. 捆绑对象提供了一个用于定位项目的单一界面,同时考虑了捆绑结构、用户偏好、可用的本地化和其他相关因素。
  1. 任何可执行文件都可以使用 bundle 对象来定位资源,无论是在应用程序的 bundle 中还是在位于其他位置的已知 bundle 中。您不使用捆绑对象来定位容器目录或文件系统的其他部分中的文件。

Bundle能做什么

  1. 为预期的捆绑目录创建捆绑对象。
  1. 使用捆绑对象的方法来定位或加载所需的资源。
  1. 使用其他系统 API 与资源进行交互。

某些类型的常用资源无需捆绑即可定位和打开。例如,在加载图像时,您将图像存储在资产目录中,并使用或的方法加载它们。同样,对于字符串资源,您使用加载单个字符串而不是自己加载整个文件。imageNamed:``UIImage``NSImage``NSLocalizedString``.strings

\

Bundle是如何编译打包的

\

  • bundle存放的文件类型

  • bundle用文件管理器打开

\

  • bundle打包过程

\

如何获取Bundle的文件路径

// 获取应用程序程序包中资源文件路径的方法:
NSLog(@"%@",[[NSBundle mainBundle] bundlePath]);
NSString*imagePath = [[NSBundle mainBundle] pathForResource:@"apple"ofType:@"png"];
UIImage*appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];

\

\

序列化/反序列化

序列化(serialization)在电脑科学的资料处理中,是指将数据结构对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台电脑环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。

序列化在电脑科学中通常有以下定义:

  • 对同步控制而言,表示强制在同一时间内进行单一访问。

1、序列化和反序列的概念

面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的编程典范,同时也是一种程序开发的抽象方针。它可能包含数据特性代码方法。对象则指的是(class)的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象([1])([2])。

那么这里呢

对象封装了, 数据特性代码方法。 一般情况下我们序列化的都是数据的部分(代码是否也能够序列化? 理论上也可以, 因为代码本身已经编译成二进制指令了,但是由于代码之间的调用存在循环的关系,)。

\

  • 序列化:将对象转换为字节序列的过程称为对象的序列化;
  • 反序列化:将字节序列恢复为对象的过程称为对象的反序列化;

\

2、什么情况下序列化?

\

  • 当你想要把内存中的对象状态保存到一个文件中或者数据库中的时候;
  • 当你想要用套接字在网络上传送对象的时候;

\

3、如何实现序列化?

主流序列化协议:xml、json、 protobuf

\

\

文本文件编码

  1. 阅读文本编码那些事
  1. 理解计算机是如何表示文本这个自然界中的数据,(二进制和编码)
  1. 理解编码和解码,

文件读写

\

C语言-open

文件读写分为三步

  1. 打开文件
  1. 把文件的二进制流写入到内存(缓冲区)/把内存的二进制数据写入到文件中
  1. 关闭文件

读写文本

\

读写二进制

\

iOS-NSData

  • 我们来看看NSData长什么样

    • 二进制格式,因为大多数情况下一个数据的大小是超过一个字节的,一个字节8位,正好是两个16进制字母可以表示,为了方便我们一般用16进制编辑器来查看。
  • 我们其实只要把这些二进制数据直接写到磁盘就可以了。
  • 文件读写相关API

那其实当我们第一次接触一些API的时候都可以可以通过这种方法,阅读API文档,找到满足我们需求实现的API。如果API文档里的内容不足,则可以继续求助搜索引擎。

\

序列化协议

理解序列化过程

我们知道了对于现实世界中的数据(比如文本、图片、音频、视频),我们需要通过一种编码方式来编码成二进制,而特别的在OOP中,对象的编码和解码也就对应着序列化和反序列化,因此这里的序列化协议其实也就是编码的协议。

对于对象的编码主流有两种方式

  1. 转换成标准文本后用文本编码

    1. 对象转换成k-v对象比如字典
    2. 然后将字典转换成标准的KV文本
    3. 最后文本编码成二进制
  1. 特定对象使用特定的编码形式,直接编码成二进制

    1. 定义对象
    2. 定义对象特定编码函数
    3. 定义对象特定解码函数
    4. 执行序列化和反序列化

\

尝试实现一个序列化过程

  • 基本数据类型的二进制表示

很多编程语言的基本类型比如String,Array, 其实都是由C语言的几种基本数据类型组合成的数据结构 所以我们直接来看C语言中的几种基本数据类型的二进制表示 一般有:整型(byte / short / int / long)、浮点型(float / double)、布尔型(boolean)和字符型(char)。

这些基本类型的特点就是,数据就是以二进制的形式存在内存中的。

  • 尝试实现

\

\

\

\

\

\

\

定义序列化/编码函数

\

\

\

\

定义解码函数

\

\

\

\

\

定义main函数来调试序列化和反序列化

\

\

\

\

\

\

主流序列化协议的比较

主流序列化协议:xml、json、 protobuf

XML

\

\

JSON

\

protobuf

\

(1)二进制结构存储,效率高,序列化体积比Json和xml更小、更加灵活; (2)格式规范,支持RPC; (3)易于使用,开发人员可以按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持JAVA、C++、Go等语言环境。通过这类类包含在项目当中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列工作;

\

\

\

\

\

\

\

NSJSONSerialization

JSON序列化的输入需要是一个合法的JSON对象,比如NSDictionary(NSArray,其实可以看成key是0...n的NSDictionary) 。所以我们先要自己手动地把OC对象塞入一个NSDictionary

接着我们可以用NSJSONSerialization 把这个NSDictionary转换成JSON字符串,然后通过UTF8StringEncoding编码转换成NSData

iOS里其实直接是返回NSData, 跳过了转NSString这一步。

\

开源JSON序列化组件

  • YYModel
  • JSONModel
  • mantle

YYModel源码阅读

OC-runtime实现任意对象自动转NSDictionary

核心逻辑,通过objc/runtime.h 头文件里的runtime函数可以获取到任一类的 属性字符串(propertyMappedKey),以及对应的value(是一个OC对象,比如NSString, NSNumber,或者是一个NSObject)。

如果value是一个NSObject的话,会一直递归下去。最终生成一个NSDictionary(严谨来说说validJSONObject)

递归避免死循环问题

由于自动转换是一个递归的逻辑,所以我们要避免存在环导致死循环。

\

总结与其他数据可持久化方法简介

详细的大家可以课下阅读掘金文章(juejin.cn/post/684490…

课上学习了

  1. NSFileManager
  1. NSBundle

\

课下自学

  1. Preference偏好设置(NSUserDefault)

    1. plist, xml格式, (可以是文本,也可以是其他二进制)PLIST文件是macOS应用程序使用的设置文件,也称为“属性文件”。 它包含各种程序的属性和配置设置。 PLIST文件采用XML格式,并基于Apple的Core Foundation DTD。
    2. 相关API, 存储在沙盒的目录
  1. NSKeyedArchiver归档 / NSKeyedUnarchiver解档

\

了解使用

  1. SQLite3的使用
  1. FMDB
  1. Core Data
  1. Realm

\

iOS网络编程

C/S模式

C是指Client,S是指Server,C/S模式就是指客户端/服务器模式。是计算机软件协同工作的一种模式,通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。

在一次Http请求的行为中, 发起请求的是客户端、做出响应的是服务器。其实一些场景下客户端也可能作为服务器(比如消息推送,长链接指令,点对点通信)

Http请求

Http协议的特点是, 基于TCP/IP、 无状态、无连接、CS模式。 支持keep-alive、Https等特性,支持的方法有Post、Get、Put等。在手机上Http请求数据的流程如下图。

一次Http请求过程中发生的事情

  1. DNS域名解析
  1. 建立TCP链接
  1. 发送HTTP请求(Headers, Body)
  1. 服务器处理请求,并响应
  1. 客户端收到响应并处理。

HTTP请求的数据报文如下

Http报文

\

Http报文由三部分组成,分别是请求行、请求头部、请求的数据, 大致上分别对应了我们之后会学习到的,NSURL、NSURLRequest.headers、NSURLRequest.HTTPBody

iOS网络库-NSURLSession

大部分语言都会封装一个网络库(或者工具)来让用户更快速地使用, 比如shell里的curl, python里的requests, js里的axios。

比如Python

\

NSURLSession 如何发起一次POST请求

\

\

NSURL

NSURL, 其实就是帮我们解析了URL字符串各个部分的对象,

zh.wikipedia.org/wiki/%E7%BB… 统一资源定位符(英语:Uniform Resource Locator,缩写:URL,或称统一资源定位器、定位地址、URL地址([1]))俗称网页地址,简称网址。 URL其实就是一个有特定格式的字符串, 在HTTP请求中,URL由这几个部分组成

其API文档如下,比较简单。

NSURLRequest

在NSURLRequest 里我们会设置一些Http报文中的内容,比如方法、URL、http头,

属性web开发的同学知道我们可以在chrome里用F12打开调试器,查看浏览器发出的网络请求,这里可以看到请求的标头,也就是Headers,以及请求体(在预览标签页)

这个请求中我们定义了接收和返回数据的格式(json类型的数据)

\

NSURLSession

NSURLSessioniOS7中推出,NSURLSession的推出旨在替换之前的NSURLConnectionNSURLSession的使用相对于之前的NSURLConnection更简单,而且不用处理Runloop相关的东西。

2015年RFC 7540标准发布了http 2.0版本,http 2.0版本中包含很多新的特性,在传输速度上也有很明显的提升。NSURLSessioniOS9.0开始,对http 2.0提供了支持。

NSURLSession由三部分构成:

  • NSURLSession:请求会话对象,可以用系统提供的单例对象,也可以自己创建。
  • NSURLSessionConfiguration:对session会话进行配置,一般都采用default
  • NSURLSessionTask:负责执行具体请求的task,由session创建。

NSURLSession有三种方式创建:

作者:刘小壮 链接:juejin.cn/post/684700… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

以下同一章节引用同出处

\

NSURLSessionConfiguration

NSURLSessionConfiguration负责对NSURLSession初始化时进行配置,通过NSURLSessionConfiguration可以设置请求的Cookie、密钥、缓存、请求头等参数,将网络请求的一些配置参数从NSURLSession中分离出来。

NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                      delegate:self
                                                 delegateQueue:[NSOperationQueue mainQueue]];

NSURLSessionTask

通过NSURLSession发起的每个请求,都会被封装为一个NSURLSessionTask任务,但一般不会直接是NSURLSessionTask类,而是基于不同任务类型,被封装为其对应的子类。

  • NSURLSessionDataTask:处理普通的GetPost请求。
  • NSURLSessionUploadTask:处理上传请求,可以传入对应的上传文件或路径。
  • NSURLSessionDownloadTask:处理下载地址,提供断点续传功能的cancel方法。

\

NSURLSessionDelegate

对于NSURLSession的代理方法这里就不详细列举了,方法命名遵循苹果一贯见名知义的原则,用起来很简单。这里介绍一下NSURLSession的代理继承结构。

NSURLSession中定义了一系列代理,并遵循上面的继承关系。根据继承关系和代理方法的声明,如果执行某项任务,只需要遵守其中的某个代理即可。

例如执行上传或普通Post请求,则遵守NSURLSessionDataDelegate,执行下载任务则遵循NSURLSessionDownloadDelegate,父级代理定义的都是公共方法。

\

创建一个Post请求

  1. 创建request,传入NSURL
  1. 设置Http方法为POST
  1. 设置Headers请求的body和response的body都需要是json格式的HTTPBody
  1. 设置HTTPBody
  1. 创建task并resume(恢复执行), task刚创建出来是suspend(挂起)的

\

\

实战作业 Demo.app

  1. 用tableView编写一个TODO List, 支持增加、删除、改变完成状态 2. TODOlist的数据实时同步到本地的文件中,启动时默认加载本地文件里的数据 3. 用户能够自行选择从本地同步数据到远端,还是从远端同步数据到本地。

Model与序列化

可以直接使用YYModel进行序列化。

本地存储

  1. 可以尝试NSFileManager或者其他数据库进行数据存储
  1. 注意我们每一次改动内存里的数据后都应该同步到磁盘
  1. 同步的过程注意放到子线程(最好是一个专门的写入串行队列来保证写入顺序)异步执行

\

网络数据同步

  1. 网络数据同步到内存后如果触发了内存对象的更新,也会触发数据写到磁盘

第十一节:探索 iOS 多媒体技术

  1. 相册权限

  1. 工程配置

info.plist 中需要添加对应的提示文案。

好的文案可以让用户更容易接受,并开启对应的权限。

其他的权限页可以自己尝试玩一玩。

  1. 权限获取

权限状态

typedef NS_ENUM(NSInteger, PHAuthorizationStatus) {
    // 暂未获取权限
    PHAuthorizationStatusNotDetermined = 0,
    // 受限状态,没有授权访问相册,用户也无法修改状态(例如家长模式下)
    PHAuthorizationStatusRestricted,
    // 用户拒绝了相册授权
    PHAuthorizationStatusDenied,
    // 用户同意了相册授权
    PHAuthorizationStatusAuthorized,
    // 受限的相册访问,用户会选择部分程序可以访问的相册数据
    PHAuthorizationStatusLimited API_AVAILABLE(ios(14)),
};

权限获取方式1

选择 PHAuthorizationStatusLimited 或者 PHAuthorizationStatusAuthorized 都会返回 PHAuthorizationStatusAuthorized 状态。

权限获取方式2

如果希望判断用户是否选择了 PHAuthorizationStatusLimited 状态,可以使用下面的这种方法。

\

  1. 相册的加载

  1. UIImagePickerController

  1. PHPickerViewController

  1. 自定义选择器

不主动请求权限

系统会返回空结果,并在另一个进程弹出权限请求弹窗。无论选择哪种权限,代码已经执行过了。需要重新进入才能获取到数据。

主动请求权限

主动请求权限,可以控制一定选择过权限类型后,再加载数据。

与【不主动请求权限】不同的是,可以在选择权限后,立刻将数据上屏。

  1. 图片的加载

\

  1. 视频播放

  1. 初始化

  1. 通过 URL 创建 AVPlayerItem 供播放器使用。

可以认为 AVPlayerItem 就是多媒体数据的管理器。

  1. 播放时长监听

可能是网络资源,播放时长可能后期设置

  1. 加载状态监听

出错时可以进行错误处理

  1. 播放速率监听
  • 播放和暂停都等价于设置 rate 值。
  • 由于播放可能被打断,例如有电话打过来了,系统会将播放器暂停;挂断后也不会自动恢复。UI 展示可能出现问题。

  1. 播放进度监听

定时更新进度条

  1. 播放控制

  • 点击播放暂停,调用 play/pause
  • 拖拽进度条

    • 通过seekToTime:toleranceBefore:toleranceAfter:来调整播放位置。
    • 松手时,调用 play

  1. 音频控制

为了防止其他 App 的播放影响我们的播放,将 AVAudioSession 设置为 AVAudioSessionCategoryPlayback

类别当按“静音”或者锁屏时是否静音是否引起不支持混音的App中断是否支持录音和播放
AVAudioSessionCategoryAmbient只支持播放
AVAudioSessionCategoryAudioProcessing///
AVAudioSessionCategoryMultiRoute既可以录音也可以播放
AVAudioSessionCategoryPlayAndRecord默认不引起既可以录音也可以播放
AVAudioSessionCategoryPlayback默认引起只用于播放
AVAudioSessionCategoryRecord只用于录音
AVAudioSessionCategorySoloAmbient只用于播放
  1. 画中画模式

  • 需要判断设备是否支持。
  • 监听 pictureInPicturePossible 是否更新状态,更新后可尝试触发画中画效果。
  • 监听应用进入活跃状态。(从主屏幕返回时,停止画中画模式)

  1. 远端视频播放

网络视频的播放和本地视频的播放并没有太大区别。直接使用本地视频的播放逻辑,有 URL 就能实现播放。

\

  1. 滤镜

  1. 基础知识

  • OpenGL(Open Graphics Library)

    • ⼀个跨编程语⾔、跨平台的编程图形程序接⼝,它将计算机的资源抽象称为⼀个 OpenGL 的对象,对这些资源的操作抽象为⼀个的 OpenGL 指令。
  • OpenGL ES (OpenGL for Embedded Systems)

    • OpenGL三维图形 API 的⼦集,针对手机、 PDA和游戏主机等嵌入式设备而设计,去除了许多不必要和性能较低的API接⼝。
  • DirectX

    • DirectX 是 Windows 上⼀个多媒体处理API,并不支持 Windows 以外的平台,所以不是跨平台框架。DirectX 不是⼀个单纯的图形API,按照性质分类,可以分为四⼤部分:显示部分、声⾳部分、输入部分和⽹络部分。
  • Metal

    • Apple 为游戏开发者推出了新的平台技术,该技术能够为 3D 图像提⾼ 10 倍的渲染性能。Metal 是 Apple 为了解决 3D 渲染而推出的框架。

OpenGL状态机

  • 状态机可以理解为一台可以保存状态,并根据当前状态进行相应输出的机器。
  • 当进⼊特殊状态(停机状态)时,不再接收输⼊,停⽌工作。

渲染管线 Render Pipeline

在OpenGL下渲染图形,就像一个固定顺序的流⽔线,任务严格按照既定的先后顺序依次执行。

Pipeline 也有“流水线”的意思,但目前常见翻译为“管线”

OpenGL上下文(context)

  • ⾮常庞⼤的状态机

    • 在应⽤程序调⽤任何 OpenGL 的指令之前,首先需要创建⼀个 OpenGL 的上下⽂。这个上下⽂是⼀个⾮常庞⼤的状态机,保存了 OpenGL 中的各种状态,这也是 OpenGL 指令执⾏的基础。
  • 面向过程,可封装为面向对象

    • OpenGL 的函数不管在哪个语⾔中,都是类似C语⾔一样的面向过程的函数。本质上都是对 OpenGL 上下⽂这个庞⼤的状态机中的某个状态或者对象进行操作。通过对 OpenGL 指令的封装,可以将 OpenGL 的相关调⽤封装成为⼀个⾯向对象的图形API。
  • 切换上下文开销大,可创建多个上下文独立使用

    • 由于 OpenGL 上下⽂是⼀个巨大的状态机,切换上下文往往会产生较⼤的开销,但是不同的绘制模块,可能需要使⽤完全独立的状态管理。因此,可以分别创建多个不同的上下文,在不同线程中使⽤不同的上下文,上下⽂之间共享纹理、缓冲区等资源。⽐反复切换上下⽂,或者⼤量修改渲染状态,更加合理高效。
  1. GPUImage

github.com/BradLarson/… (Objective-C 版本,使用 OpenGL ES)

github.com/BradLarson/… 版本,使用 OpenGL ES)

github.com/BradLarson/… 版本,使用 Metal)

  • 成熟的三方框架,可以用于学习原理。
  • GPUImage 已经停止维护了,但不妨碍大家学习。

    • 也可以看 GPUImage2 或 GPUImage3 进行学习。

\

使用非常简单。

  1. 准备最终渲染的视图。
  1. 初始化视频播放类和滤镜类。
  1. 将视频、滤镜、渲染视图串联起来。

由于三方框架没有定时回调播放进度的逻辑,所以写了一个定时器,不断去获取播放时间,更新到滤镜中。

  1. 参考资料

  1. Demo 代码

bytedance.feishu.cn/file/boxcnp…

第十二节:苹果开发者设计模式与开发范式

image.png

设计模式

\

设计模式简史

在介绍设计模式的起源之前,我们先要了解一下模式的诞生与发展。

与很多软件工程技术一样,模式起源于建筑领域;毕竟与只有几十年历史的软件工程相比,已经拥有几千年沉淀的建筑工程有太多值得学习和借鉴的地方。

\

那么模式是如何诞生的?让我们先来认识一个人——Christopher Alexander(克里斯托弗.亚历山大),哈佛大学建筑学博士、美国加州大学伯克利分校建筑学教授,他还有一个“昵称”——模式之父(The father of patterns)。Christopher Alexander博士及其研究团队用了约20年的时间,对住宅和周边环境进行了大量的调查研究和资料收集工作,发现舒适住宅和城市环境存在一些共同的规律,Christopher Alexander在著作中把这些规律归纳为253个模式,对每一个模式(Pattern)都从Context(前提条件)、Theme或Problem(目标问题)、 Solution(解决方案)三个方面进行了描述,并给出了从用户需求分析到建筑环境结构设计直至经典实例的过程模型。

\

在Christopher Alexander的另一部经典著作《建筑的永恒之道》中,他给出了关于模式的定义:

每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心,通过这种方式,我们可以无数次地重用那些已有的成功的解决方案,无需再重复相同的工作。这个定义可以简单地用一句话表示:模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。【A pattern is a successful or efficient solution to a recurring problem within a context】

1990年,软件工程界开始关注ChristopherAlexander等在这一住宅、公共建筑与城市规划领域的重大突破。最早将模式的思想引入软件工程方法学的是1991-1992年以“四人组(Gang of Four,简称GoF,分别是Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides)”自称的四位著名软件工程学者,他们在1994年归纳发表了23种在软件开发中使用频率较高的设计模式,旨在用模式来统一沟通面向对象方法在分析、设计和实现间的鸿沟。

\

GoF将模式的概念引入软件工程领域,这标志着软件模式的诞生。软件模式(Software Patterns)是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件开发生命周期的每一个阶段都存在着一些被认同的模式。软件模式与具体的应用领域无关,也就是说无论你从事的是移动应用开发、桌面应用开发、Web应用开发还是嵌入式软件的开发,都可以使用软件模式。

\

在软件模式中,设计模式是研究最为深入的分支,设计模式用于在特定的条件下为一些重复出现的软件设计问题提供合理的、有效的解决方案,它融合了众多专家的设计经验,已经在成千上万的软件中得以应用。 1995年, GoF将收集和整理好的23种设计模式汇编成Design Patterns: Elements of Reusable Object-Oriented Software【《设计模式:可复用面向对象软件的基础》】一书,该书的出版也标志着设计模式正式成为面向对象(Object Oriented)软件工程的一个重要研究分支。

\

设计模式是什么

设计模式的一般定义如下:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。

狭义的设计模式是指GoF在《设计模式:可复用面向对象软件的基础》一书中所介绍的23种经典设计模式,不过设计模式并不仅仅只有这23种,随着软件开发技术的发展,越来越多的新模式不断诞生并得以应用。

\

设计模式一般包含模式名称、问题、目的、解决方案、效果等组成要素,其中关键要素是模式名称、问题、解决方案和效果。模式名称(Pattern Name)通过一两个词来描述模式的问题、解决方案和效果,以便更好地理解模式并方便开发人员之间的交流,绝大多数模式都是根据其功能或模式结构来命名的(GoF设计模式中没有一个模式用人名命名,);问题(Problem)描述了应该在何时使用模式,它包含了设计中存在的问题以及问题存在的原因;解决方案(Solution)描述了一个设计模式的组成成分,以及这些组成成分之间的相互关系,各自的职责和协作方式,通常解决方案通过UML类图和核心代码来进行描述;效果(Consequences)描述了模式的优缺点以及在使用模式时应权衡的问题。

\

虽然GoF设计模式只有23个,但是它们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。根据它们的用途,设计模式可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种,其中创建型模式主要用于描述如何创建对象,结构型模式主要用于描述如何实现类或对象的组合,行为型模式主要用于描述类或对象怎样交互以及怎样分配职责,在GoF 23种设计模式中包含5种创建型设计模式、7种结构型设计模式和11种行为型设计模式。此外,根据某个模式主要是用于处理类之间的关系还是对象之间的关系,设计模式还可以分为类模式和对象模式。我们经常将两种分类方式结合使用,如单例模式是对象创建型模式,模板方法模式是类行为型模式。

\

设计模式有什么用

下面我们来回答最后一个问题:设计模式到底有什么用?

  1. 设计模式来源众多专家的经验和智慧,它们是从许多优秀的软件系统中总结出的成功的、能够实现可维护性复用的设计方案,使用这些方案将可以让我们避免做一些重复性的工作,也许我们冥思苦想得到的一个“自以为很了不起”的设计方案其实就是某一个设计模式。在时间就是金钱的今天,设计模式无疑会为有助于我们提高开发和设计效率,但它不保证一定会提高;
  1. 设计模式提供了一套通用的设计词汇和一种通用的形式来方便开发人员之间沟通和交流,使得设计方案更加通俗易懂。交流通常很耗时,任何有助于提高交流效率的东西都可以为我们节省不少时间。无论你使用哪种编程语言,做什么类型的项目,甚至你处于一个国际化的开发团队,当面对同一个设计模式时,你和别人的理解并无二异,因为设计模式是跨语言、跨平台、跨应用、跨国界的;
  1. 大部分设计模式都兼顾了系统的可重用性和可扩展性,这使得我们可以更好地重用一些已有的设计方案、功能模块甚至一个完整的软件系统,避免我们经常做一些重复的设计、编写一些重复的代码。此外,随着软件规模的日益增大,软件寿命的日益变长,系统的可维护性和可扩展性也越来越重要,许多设计模式将有助于提高系统的灵活性和可扩展性,让我们在不修改或者少修改现有系统的基础上增加、删除或者替换功能模块。如果一点设计模式都不懂,我想要做到这一点恐怕还是很困难的;
  1. 合理使用设计模式并对设计模式的使用情况进行文档化,将有助于别人更快地理解系统。
  1. 最后一点对初学者很重要,学习设计模式将有助于初学者更加深入地理解面向对象思想,让你知道:如何将代码分散在几个不同的类中?为什么要有“接口”?何谓针对抽象编程?何时不应该使用继承?如何不修改源代码增加新功能?同时还让你能够更好地阅读和理解现有类库(如JDK)与其他系统中的源代码;

\

常用设计模式

Delegate

  1. 什么是代理模式?

代理模式是在 IOS 中经常遇到的一种设计模式,那什么叫做代理模式呢? A 完成一件事,但是自己不能完成,或是自己完成这件事情要花费非常大的精力,于是他找个代理人 B 替他完成这个事情,他们之间便有个协议 (protocol),B 继承该协议来完成 A 代理给他的事情。

  1. 为什么使用代理模式?

在Cocoa框架内,Delegate使用的场景非常多,如常用的UITableView:

\

3.这里推导一下为什么要用Delegate

  • UITableView,核心功能是展示列表;
  • 那么TableView目前有两个依赖:

    • 获取数据源,用户展示列表;
    • 列表cell点击之后的事件交互;
  • 如果不用Delegate,UITableView只能把这个逻辑封装到组件内;

    • UI组件耦合业务逻辑,无法复用;

\

  • 换Delegate的思路:

    • UITableView声明自己的依赖:UITableViewDelegate/UITableViewDataSource;
    • 业务实现UITableView依赖的协议,作为代理方;实现自己的业务逻辑;
    • 这样才获得了一个和业务无关的,可服用/高度封装的组件;

暂时无法在飞书文档外展示此内容

4.总结

  • 模式名称(Pattern Name):

    • Delegate
  • 问题(Problem):

    • 解耦当前功能不关心的逻辑;
  • 解决方案(Solution):

    • 依赖协议, 代理方实现;
  • 效果(Consequences):

    • 可以复用的基础组件,解耦;
    • Cocoa,UIKit框架内,常用的UI组件,都是通过代理模式来实现;

\

类簇

抽象工厂

在解释类簇之前,先理解下抽象工厂;

在抽象工厂模式结构图中包含如下几个角色:

● AbstractFactory(抽象工厂):它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。

● ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。

● AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。

● ConcreteProduct(具体产品):它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法

抽象工厂和工厂方法不同的地方在于,生产产品的工厂是抽象的。举例,可口可乐公司生产可乐的同时,也需要生产装可乐的瓶子和箱子,瓶子和箱子也是可口可乐专属定制的,同样百事可乐公司也会有这个需求。这个时候我们的工厂不仅仅是生产可乐饮料的工厂,还必须同时生产同一主题的瓶子和箱子,所以它是一个抽象的主题工厂,专门生产同一主题的不同商品

\

\

Sunny公司使用抽象工厂模式设计了界面皮肤库,该皮肤库可以较为方便地增加新的皮肤,但是现在遇到一个非常严重的问题:由于设计时考虑不全面,忘记为单选按钮(RadioButton)提供不同皮肤的风格化显示,导致无论选择哪种皮肤,单选按钮都显得那么“格格不入”。Sunny公司的设计人员决定向系统中增加单选按钮,但是发现原有系统居然不能够在符合“开闭原则”的前提下增加新的组件,原因是抽象工厂SkinFactory中根本没有提供创建单选按钮的方法,如果需要增加单选按钮,首先需要修改抽象工厂接口SkinFactory,在其中新增声明创建单选按钮的方法,然后逐个修改具体工厂类,增加相应方法以实现在不同的皮肤中创建单选按钮,此外还需要修改客户端,否则单选按钮无法应用于现有系统。

怎么办?答案是抽象工厂模式无法解决该问题,这也是抽象工厂模式最大的缺点。在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为“开闭原则”的倾斜性。“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的,对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:

(1) 增加产品族:对于增加新的产品族,抽象工厂模式很好地支持了“开闭原则”,只需要增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改。

(2) 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。

正因为抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。

主要优点

抽象工厂模式的主要优点如下:

(1) 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。

(2) 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。

(3) 增加新的产品族很方便,无需修改已有系统,符合“开闭原则”。

  1. 主要缺点

抽象工厂模式的主要缺点如下:

增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。

  1. 适用场景

在以下情况下可以考虑使用抽象工厂模式:

(1) 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和使用解耦。

(2) 系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。

(3) 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的,此时具有一个共同的约束条件:操作系统的类型。

(4) 产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。

\

类簇(class clusters)

类簇是Foundation framework框架下广泛使用的一种设计模式。它管理了一组隐藏在公共抽象父类下的具体私有子类

为了说明类簇的结构体系和好处,我们先思考一个问题:如何构建一个类的结构体系用它来定义一个对象存储不同数据类型的数字(char,int, float, double)。因为不同数据类型的数字有很多共同点(例如:它们都能从一种类型转换成另一种类型,都能用字符串表示),所以可以用一个类来表示它们。然而,不同的数据类型的数字的存储空间是不同的,所以用一个类来表示它们是很低效的。考虑到这个问题,我们设计了如下图结构解决这个问题。

\

Number是一个抽象父类,在其方法声明中声明了子类的共有操作。但是,Number不会声明一个实例变量存储不同类型的数据,而是由其子类创建对应类型的实例变量并将调用接口共享给抽象父类Number。 到目前为止,这个类结构的设计十分简单。然而,如果C语言的基本数据类型被修改了(例如:加入了些新的数据类型),那么我们Number类结构如下图所示:

\

这种创建一个类保存一种类型数据的概念很容易扩展成十几个类。展示了一种概念简洁性的设计。

\

使用类簇(Simple Concept and Simple Interface)

使用类簇的设计模式来解决这个问题,类结构设计如图所示:

使用类簇我们只能看到一个公共父类Number,它是如何创建正确子类的实例的呢?解决方式是利用抽象父类来处理实例化。

\

优点:

  • 可以将抽象基类背后的复杂细节隐藏起来。
  • 程序员不会需要记住各种创建对象的具体类实现,简 化了开发成本,提高了开发效率。
  • 便于进行封装和组件化。
  • 减少了 if-else 这样缺乏扩展性的代码。
  • 增加新功能支持不影响其他代码。

缺点

  • 已有的类簇非常不好扩展。

观察者模式

观察者模式的4个角色

1)Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。

2)ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无需扩展目标类,则具体目标类可以省略。

3)Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。

4)ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。 观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。

\

观察者模式是一种使用频率非常高的设计模式,无论是移动应用、Web应用或者桌面应用,观察者模式几乎无处不在,它为实现对象之间的联动提供了一套完整的解决方案,凡是涉及到一对一或者一对多的对象交互场景都可以使用观察者模式。观察者模式广泛应用于各种编程语言的GUI事件处理的实现,在基于事件的XML解析技术(如SAX2)以及Web事件处理中也都使用了观察者模式。

1.主要优点

观察者模式的主要优点如下:

(1) 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。

(2) 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。

(3) 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。

(4) 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。

2.主要缺点

观察者模式的主要缺点如下:

(1) 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。

(2) 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

(3) 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

3.适用场景

在以下情况下可以考虑使用观察者模式:

(1) 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。

(2) 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。

(3) 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。

\

Cocoa 观察者应用

NSNotificationCenter

\

KVO

\

\

开发范式

传统MVC

Model-View-Controller 是一种设计模式,它由几个更基本的设计模式组成。这些基本模式共同定义了 MVC 应用程序所特有的功能分离和通信路径。然而,传统的 MVC 概念分配了一组与 Cocoa 分配的基本模式不同的基本模式。区别主要在于赋予应用程序的控制器和视图对象的角色。

在最初的概念(Smalltalk)中,MVC由Composite(组合)、Strategy(策略)和Observer(观察者)模式组成。

  • Composite——应用程序中的View对象实际上是以协调方式(即视图层次结构)一起工作的嵌套视图的组合。这些显示组件范围从窗口到复合视图(如表视图),再到单个视图(如按钮)。用户输入和显示可以发生在复合结构的任何级别。
  • Strategy——一个Controller对象为一个或多个View对象实现策略。View将自己限制在维护其可视化方面,并将所有关于接口行为的应用程序的特定含义的决议委托给控制器。
  • Observer ——模型对象保持应用程序中感兴趣的对象(通常是视图对象),并通知其状态的变化。

Composite、Strategy和Observer模式一起工作的传统方式如图所示:用户在复合结构的某个层次上操作一个视图,结果生成一个事件。Controller对象接收事件并以特定于应用程序的方式解释它——也就是说,它应用一个策略。这个策略可以是请求(通过消息)Model对象更改其状态,或者请求View对象(在复合结构的某个级别上)更改其行为或外观。反过来,当Model对象的状态发生变化时,它会通知所有已注册为观察者的对象;如果观察者是一个View对象,它可能会相应地更新它的外观。

然而,这种设计存在理论上的问题。视图对象和模型对象应该是应用程序中最可重用的对象。视图对象代表操作系统和系统支持的应用程序的“外观和感觉”;外观和行为的一致性至关重要,这需要高度可重用的对象。根据定义,模型对象封装了与问题域相关的数据并对这些数据执行操作。在设计方面,最好将模型和视图对象彼此分开,因为这样可以提高它们的可重用性。

View并没有任何界限,仅仅是简单的在Controller中呈现出Model的变化。想象一下,就像网页一样,在点击了跳转到某个其他页面的连接之后就会完全的重新加载页面。尽管在iOS平台上实现这这种MVC模式是没有任何难度的,但是它并不会为我们解决架构问题带来任何裨益。因为它本身也是,三个实体间相互都有通信,而且是紧密耦合的。这很显然会大大降低了三者的复用性,而这正是我们不愿意看到的 ;

\

传统MVC,在服务端有较多应用:

\

唯一区别在于,View的容器在服务端,是由Browser负责,在整个网站的流程中,这个容器放在Browser是非常合理的。在iOS客户端,View的容器是由UIViewController中的view负责;

因为浏览器和服务端之间的关系非常松散,而且他们分属于两个不同阵营,服务端将对View的描述生成之后,交给浏览器去负责展示,然而一旦view上有什么事件产生,基本上是很少传递到服务器(也就是所谓的Controller)的(要传也可以:AJAX),都是在浏览器这边把事情都做掉,所以在这种情况下,View容器就适合放在浏览器(V)这边。

但是在iOS开发领域,虽然也有让View去监听事件的做法,但这种做法非常少,都是把事件回传给Controller,然后Controller再另行调度。所以这时候,View的容器放在Controller就非常合适。Controller可以因为不同事件的产生去很方便地更改容器内容,比如加载失败时,把容器内容换成失败页面的View,无网络时,把容器页面换成无网络的View等等。

所以服务端只管生成对View的描述,至于对View的长相,UI事件监听和处理,都是浏览器负责生成和维护的。但是在Native这边来看,原本属于浏览器的任务也逃不掉要自己做。那么这件事情由谁来做最合适?苹果给出的答案是:UIViewController

\

Cocoa MVC

\

1、 Controller和View之间可以通信,Controllor通过outlet(输出口)控制View,View可以通过target-action、delegate或者data source(想想UITableVeiwDatasource)来和Controller通信;

2、 Controller在接收到View传过来的交互事件之后,经过一些判断和处理,把需要Model处理的事件递交给Model处理,Controller对Model使用的是API;

3、 Model在处理完数据之后,如果有需要,会通过Notification或者KVO的方式告知Controller,事件已经处理完,Controller再经过判断和处理之后,考虑下一步要怎么办。这里的无线天线很有意思,Model只负责发送通知,具体谁接收这个通知并处理它,Model并不关心,这一点非常重要,是理解Notification模式的关键。

4、 Model和View之间不直接通信!

就开发速度而言,Cocoa MVC是最好的架构选择方案

\

MVC 自身存在着很多不足。

1.愈发笨重的 Controller:

  • 在 iOS 中经常能看到成千上万行代码的view controller。这些超重 app 的突出情况包括:厚重的 View Controller 很难维护(由于其庞大的规模);包含几十个属性,使他们的状态难以管理;遵循许多协议(protocol),导致协议的响应代码和 controller 的逻辑代码混淆在一起。
  • 控制器 Controller 是 app 的 “胶水代码”,协调模型和视图之间的所有交互。控制器负责管理他们所拥有的视图的视图层次结构,还要响应视图的 loading、appearing、disappearing 等等,同时往往也会充满我们不愿暴露的 Model 的模型逻辑以及不愿暴露给视图的业务逻辑。
  • 传统的 Model 数据大多来源于网络数据,拿到网络数据后客户端要做的事情就是将数据直接按照顺序画在界面上。随着业务的越来越来的深入,移动端经常自行处理一部分逻辑计算操作。这个时间一惯的做法是在控制器中处理,最终导致了控制器成了垃圾箱,越来越不可维护。
  • 厚重的 view controller 很难测试,不管是手动测试或是使用单元测试,因为有太多可能的状态。

2.太过于轻量级的 Model:

  • 早期的 Model 层,其实就是如果数据有几个属性,就定义几个属性,ARC 普及以后我们在 Model 层的实现文件中基本上看不到代码(无需再手动管理释放变量,Model 既没有复杂的业务处理,也没有对象的构造,基本上 .m 文件中的代码普遍是空的);同时与控制器的代码越来厚重形成强烈的反差,这一度让人不禁对现有的开发设计构思有所怀疑。

3.遗失的网络逻辑:

  • 苹果使用的 MVC 的定义是这么说的:所有的对象都可以被归类为一个 Model,一个 view,或是一个控制器。就这些,那么把网络代码放哪里?和一个 API 通信的代码应该放在哪儿?
  • 你可能试着把它放在 Model 对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的 Model 生命周期更长,事情将变的复杂。显然也不应该把网络代码放在 view 里,因此只剩下控制器了。这同样是个坏主意,因为这加剧了厚重控制器的问题。那么应该放在哪里呢?显然 MVC 的 3 大组件根本没有适合放这些代码的地方。

4.较差的可测试性

  • MVC 的另一个大问题是,它不鼓励开发人员编写单元测试。由于控制器混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。大多数人选择忽略这个任务,那就是不做任何测试。

\

为什么会这样呢?主要还是因为我们的UIViewController它本身就拥有一个VIew,这个View是所有视图的根视图,而且View的生命周期也都由Controoler负责管理,所以View和Controller是很难做到相互独立的。虽然你可以把控制器里的一些业务逻辑和数据转换工作交给Model,但是你却没有办法将一些工作让View来分摊,因为View的主要职责只是将用户的操作行为交给Controller去处理而已。于是Controller最终就变成了所有东西的代理和数据源,甚至还有网络请求.....还有......所以我们写的Controller代码量一般都是非常大的,随着当业务需求的增加,Controller的代码量会一直增长,而相对来说View和Model的代码量就比较稳定,所以也有人把MVC叫做Massive View Controller,因为Controller确实显得有些臃肿。

在iOS开发领域中,怎样才算是MVC划分的正确姿势?

M应该做的事:

  1. 给ViewController提供数据
  1. 给ViewController存储数据提供接口
  1. 提供经过抽象的业务基本组件,供Controller调度

C应该做的事:

  1. 管理View Container的生命周期
  1. 负责生成所有的View实例,并放入View Container
  1. 监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。

V应该做的事:

  1. 响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
  1. 界面元素表达

MVC演进

MVC的演进方向有很多,各种开发范式都是为了解决MVC目前存在的问题;万变不离其宗的还是三个角色:数据管理者数据加工者数据展示者。这些五花八门的思想,不外乎就是制定了一个规范,规定了这三个角色应当如何进行数据交换

暂时无法在飞书文档外展示此内容

\

胖/瘦Model

  • 什么叫胖Model?

胖Model包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。举个例子:

把timestamp转换成具体业务上所需要的字符串,这属于业务代码,算是弱业务。FatModel做了这些弱业务之后,Controller就能变得非常skinny,Controller只需要关注强业务代码就行了。众所周知,强业务变动的可能性要比弱业务大得多,弱业务相对稳定,所以弱业务塞进Model里面是没问题的。另一方面,弱业务重复出现的频率要大于强业务,对复用性的要求更高,如果这部分业务写在Controller,类似的代码会洒得到处都是,一旦弱业务有修改(弱业务修改频率低不代表就没有修改),这个事情就是一个灾难。如果塞到Model里面去,改一处很多地方就能跟着改,就能避免这场灾难。

然而其缺点就在于,胖Model相对比较难移植,虽然只是包含弱业务,但好歹也是业务,迁移的时候很容易拔出萝卜带出泥。另外一点,MVC的架构思想更加倾向于Model是一个Layer,而不是一个Object,不应该把一个Layer应该做的事情交给一个Object去做。最后一点,软件是会成长的,FatModel很有可能随着软件的成长越来越Fat,最终难以维护。

\

  • 什么叫瘦Model?

瘦Model只负责业务数据的表达,所有业务无论强弱一律扔到Controller。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或方法来对弱业务做抽象,强业务依旧交给Controller。举个例子:

由于SlimModel跟业务完全无关,它的数据可以交给任何一个能处理它数据的Helper或其他的对象,来完成业务。在代码迁移的时候独立性很强,很少会出现拔出萝卜带出泥的情况。另外,由于SlimModel只是数据表达,对它进行维护基本上是0成本,软件膨胀得再厉害,SlimModel也不会大到哪儿去。

\

MVCS

苹果自身就采用的是这种架构思路,从名字也能看出,也是基于MVC衍生出来的一套架构。从概念上来说,它拆分的部分是Model部分,拆出来一个Store。这个Store专门负责数据存取。但从实际操作的角度上讲,它拆开的是Controller。

MVCS如何分工

这算是瘦Model的一种方案,瘦Model只是专门用于表达数据,然后存储、数据处理都交给外面的来做。MVCS使用的前提是,它假设了你是瘦Model,同时数据的存储和处理都在Controller去做。所以对应到MVCS,它在一开始就是拆分的Controller。因为Controller做了数据存储的事情,就会变得非常庞大,那么就把Controller专门负责存取数据的那部分抽离出来,交给另一个对象去做,这个对象就是Store。这么调整之后,整个结构也就变成了真正意义上的MVCS。

分工总结: 视图(View):用户界面 控制器(Controller):业务逻辑及处理 模型(Model):数据存储 存储器(Store):数据处理逻辑

MVCS是基于瘦Model的一种架构思路,把原本Model要做的很多事情中的其中一部分关于数据存储的代码抽象成了Store,在一定程度上降低了Controller的压力。

\

\

暂时无法在飞书文档外展示此内容

\

MVP

\

MVC的缺点在于并没有区分业务逻辑和展示逻辑, 这对单元测试很不友好. MVP针对以上缺点做了优化, 它将业务逻辑和业务展示也做了一层隔离; M和V功能不变, 原来的C现在只负责布局, 而所有的业务逻辑全都转移到了P层。 P层处理完了业务逻辑,如果要更改view的显示,那么可以通过回调来实现,这样可以减轻耦合,同时可以单独测试P层的业务逻辑。 在 MVP 中,Presenter 可以理解为松散的控制器,其中包含了视图的 UI 业务逻辑, 所有从视图发出的事件,都会通过代理给 Presenter 进行处理; 同时,Presenter 也通过视图暴露的接口与其进行通信。而这一般是在 Controller中处理的。

\

View负责界面展示和布局管理,向Presenter暴露视图更新和数据获取的接口- Presenter负责接收来自View的事件,通过View提供的接口更新视图,并管理Model- Model和MVC中的一样,提供数据模型;

\

这个流程看起来确实很像 Apple 的 理想化的MVC,它的名字是 MVP。 Apple 的 MVC 实际上是 MVP 吗?不是的,

\

区别就是IOS中: (1)苹果的理想MVC中UIView相当于View,UIController是Controller,而在MVP中,UIView和UIController都相当于View,所以在 Presenter 里面基本没什么布局相关的代码,它的职责只是通过数据和状态更新 View。 (2)持有关系也不一样,MVC中 C 持有 M和V,但是在MVP中 V 持有 P,P 持有M 。

\

在 MVP 架构里面,UIViewController 的那些子类其实是属于 View 的,而不是 Presenter。 这种区别提供了极好的可测性,但是这是用开发速度的代价换来的,因为你必须要手动的去创建数据和绑定事件

MVVM

实际上MVVM模式就是在MVP的基础上将presenter改为与View双向绑定的ViewModel即可演变而成。MVVM和MVP的最大区别就是MVVM采用了双向绑定机制,View的变动,自动反映在ViewModel上。

\

在MVVM模式中ViewModel扮演两个重要的角色

视图层的真正数据提供者:同MVP一致,ViewModel也会进行视图数据处理。一般来说视图层展示的数据经常是一个或者多个模型的属性组合,比如用户个人页的用户模型有firstName、lastName、status等多个属性,viewModel就会将这些数据整合在一起,使得视图直接调用单个数据即可展示所要的效果,简单的来说ViewModel就是为了视图展示而对Model层的数据进行包装,可以认为ViewModel才是真正给View提供数据的数据源。

视图层的交互响应者:所有用户的交互都会传递给ViewModel,ViewModel则会依次更新视图层所需要的属性,同时相应修改Model层的数据,这里依靠的是属性观察或响应式架构。

\

由示意图可以看出View是持有ViewModel的,在ViewModel中不能包含视图层的任何类或者结构体

绑定是一种响应式的通信方式。当被绑定对象某个值的变化时,绑定对象会自动感知,无需被绑定对象主动通知绑定对象。可以使用KVO和RAC实现。例如在Label中显示倒计时,是V绑定了包含定时器的VM。

\

归根结底就是一句话:在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情,就是MVVM。然后,为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa,因为苹果本身并没有提供一个比较适合这种情况的绑定方法。iOS领域里KVO,Notification,block,delegate和target-action都可以用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM;

\

那么ReactiveCocoa应该扮演什么角色?

不用ReactiveCocoa也能MVVM,用ReactiveCocoa能更好地体现MVVM的精髓。前面我举到的例子只是数据从API到View的方向,View的操作也会产生"数据",只不过这里的"数据"更多的是体现在表达用户的操作上,比如输入了什么内容,那么数据就是text、选择了哪个cell,那么数据就是indexPath。那么在数据从view走向API或者Controller的方向上,就是ReactiveCocoa发挥的地方。

我们知道,ViewModel本质上算是Model层(因为是胖Model里面分出来的一部分),所以View并不适合直接持有ViewModel,那么View一旦产生数据了怎么办?扔信号扔给ViewModel,用谁扔?ReactiveCocoa。

在MVVM中使用ReactiveCocoa的第一个目的就是如上所说,View并不适合直接持有ViewModel。第二个目的就在于,ViewModel有可能并不是只服务于特定的一个View,使用更加松散的绑定关系能够降低ViewModel和View之间的耦合度。

\

那么在MVVM中,Controller扮演什么角色?

大部分国内外资料阐述MVVM的时候都是这样排布的:View <-> ViewModel <-> Model,造成了MVVM不需要Controller的错觉,现在似乎发展成业界开始出现MVVM是不需要Controller的。的声音了。其实MVVM是一定需要Controller的参与的,虽然MVVM在一定程度上弱化了Controller的存在感,并且给Controller做了减负瘦身(这也是MVVM的主要目的)。但是,这并不代表MVVM中不需要Controller,MMVC和MVVM他们之间的关系应该是这样:

\

View <-> C <-> ViewModel <-> Model,所以使用MVVM之后,就不需要Controller的说法是不正确的。严格来说MVVM其实是MVCVM。从图中可以得知,Controller夹在View和ViewModel之间做的其中一个主要事情就是将View和ViewModel进行绑定。在逻辑上,Controller知道应当展示哪个View,Controller也知道应当使用哪个ViewModel,然而View和ViewModel它们之间是互相不知道的,所以Controller就负责控制他们的绑定关系,所以叫Controller/控制器就是这个原因。

\

MVC、MVP以及MVVM的异同

首先,这三种模式都是由Model层、View层以及中间层(C/P/VM)构成,这三者的模型层没有太大区别,从理论来说都是数据来源;视图层在理论上都被设计为被动,但在实际上略有不同。在实际开发中,MVC中的View和Controller高度耦合,几乎所有的操作都统一由ViewController管理,但是从理论来说MVC希望视图层就是单纯的View/ViewController,只需要负责UI的更新和交互,不涉及业务逻辑和模型更新。在实际开发中MVP以及MVVM实现了MVC的理论期望,即使得视图和中间层分离。但是MVP的视图层是完全被动的,单纯的把交互传递给中间层;而MVVM的视图层并不是完全被动的,他可以监视中间层的变化,一旦产生变化可以自动进行相应的变化。

\

针对于中间层的设计是三种架构的核心差异。从逻辑上来说,中间层的左右就是链接视图层和模型层,用于处理交互、接受通知和完成数据更新。对于MVC的中间层会持有V和M,主要起到组装和连接的作用,通过传递参数和实例变量来完成所有的操作。MVP的Presenter中间层持有M,在更新Model上与MVC的Controller作用一致,但是他不持有V,相反V持有中间层Presenter。中间层的工作流程即:从V接受交互传递--->响应--->向V传递响应指令--->V进行更新。这些全部的操作都需要手动书写代码完成。MVVM的中间层ViewModel持有M,在更新模型上与以上两种相同。它完全独立于视图层,V拥有中间层ViewModel,通过绑定属性自动进行更新。全部操作由响应式逻辑框架自动完成。

\

总的来说MVC耦合度比较高,代码分配不合理,维护和扩展成本比较高,但是不需要进行层级传递,代码总量比较少,便于理解和应用。MVP和MVVM相似,耦合度和代码分配比较合理,便于测试,但是MVP的视图层需要把所有的交互传递给中间层,且需要手动实现响应和更新,代码总量远超MVVM。MVVM在响应和更新上,通过响应式框架自动进行操作,精简了代码量,但是需要引入第三方响应式框架,同时因为属性观察环环相扣,调用栈比较大,不便于debug。

\

VIPER

到目前为止,你可能觉得我们把职责划分成三层,这个颗粒度已经很不错了。 现在 VIPER 从另一个角度对职责进行了划分,这次划分了五层。 VIPER并不复杂,它是将原来MVC中的Controller中的各种任务进行了清晰的分解,在写代码时,你会很清楚你正在做什么。 事实上,它比使用了数据绑定技术的MVVM更加简单,就是因为它职责明确。从MVC转到VIPER的过程同样是很清晰的, 它甚至把重构的思路都体现出来了。而MVVM则留下了许多尚未明确的责任,导致不同的人会在某些地方有不同的实现。 * View 提供完整的视图,负责视图的组合、布局、更新 向Presenter提供更新视图的接口 将View相关的事件发送给Presenter * Interactor(交互器) 维护主要的业务逻辑功能,向Presenter提供现有的业务用例 维护、获取、更新Entity 当有业务相关的事件发生时,处理事件,并通知Presenter * Presenter(展示器) 接收并处理来自View的事件 向Interactor请求调用业务逻辑 向Interactor提供View中的数据 接收并处理来自Interactor的数据回调事件 通知View进行更新操作 通过Router跳转到其他View * Entities(实体) 纯粹的数据对象。不包括数据访问层,因为这是 Interactor 的职责。 * Router(路由) 负责 VIPER 模块之间的转场 提供View之间的跳转功能,减少了模块间的耦合 初始化VIPER的各个模块

\

MVX

在具体做View层架构的设计时,不需要拘泥于MVC、MVVM、VIPER等规矩。这些都是招式,告诉你你就知道了,然后怎么玩都可以

核心思想都是为了解决cocoa MVC的臃肿问题,而对不同的元素进行拆分;

用以达到解耦和复用的目标;

让框架的易用性和可测性得到保障;

\

MVC其实是非常高Level的抽象,意思也就是,在MVC体系下还可以再衍生无数的架构方式,但万变不离其宗的是,它一定符合MVC的规范根据前面几节的洗礼,相信各位也明白了这样的道理:拆分方式的不同诞生了各种不同的衍生架构方案(MVCS拆胖Controller,MVVM拆胖Model,VIPER什么都拆),但即便拆分方式再怎么多样,那都只是招式。而拆分的规范,就是心法。

\

开发范式的本质

  • 数据管理
  • 数据加工
  • 数据展示

不管多么复杂的App,围绕软件生命周期,总是在处理这三个问题;

我们无论选择哪一种开发范式,都是为了更好的处理这三个角色;

当然你可以不用上面的开发范式,如果能做到设计的范式,符合六大设计原则,可测性和易用性都达标,也是合格的开发范式;

\

架构演进解决的核心问题

架构的目标是为了更好的支撑业务,控制复杂度,提升研发效率

复杂度

而什么是复杂度呢?复杂度简单理解就是量变产生的质变 1行的函数易理解,100行的就要费点脑筋,上千行的函数就很难全面掌控了。为此就需要拆分成一个个小函数,给于易于理解的名字,进行抽象和分层。那函数多了呢?一样不好理解和掌控,就开始拆分为多个文件。文件多了就开始拆分为多个组件。组件也多了就开始分层分模块。这些都是由量变导致的质变,都是因为人能在同一时间理解和掌控的量有限,因此通过不断的抽象,聚合,隔离,形成一个个抽象模块,降低需要理解和掌控的量,按需深入到相应的子模块。

模块化

那最终都要进行组件化模块化架构分层,是否可以一开始就这么做,一步到位呢?这样就有可能产生大量的组件,和相应繁重的组件管理和环境适配,但实际需求没有这么高的复杂度,也不需要组件拆分带来的灵活度和复用性,但付出了组件拆分隔离的成本。也就是把简单的需求复杂化,过度设计了。所以,在什么阶段,就有什么样的架构。架构设计,就是需要控制好相应的平衡点,在满足需求的前提下,既能控制好复杂度,也能保障研发效率,不做过度设计,让整个研发系统能不断演进,即时重构,整体发展稳定可控。

MVX

Cocoa开发推荐的是MVC,对于小型应用没问题,随业务开发逐步遇到了Controller臃肿的问题,因此业界产生了拆分Controller的方法,MVP,MVVM,MVCS,VIPER也随之而来,不管怎么变化,心法一定是「拆分」;本文不计划讨论这些开发范式,因为只要能控制一个模块的复杂度,甚至都不会遇到臃肿的Controller;只是为了统一编程风格,防止框架的腐化,本文后续推荐用MVVM开发范式;