0%

HTTP

HTTP 即超文本传输协议(HyperText Transfer Protocol)

HTTP 连接流程

  • 域名解析
  • 发起TCP的3次握手
  • Web浏览器向Web服务器发送http请求命令
  • Web浏览器发送http请求头信息
  • Web服务器应答
  • Web服务器发送应答头信息
  • Web服务器向浏览器发送数据
  • Web服务器关闭TCP连接

TCP连接在发送后将仍然保持打开状态,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

HTTP存在的问题

  • 窃听风险

  • 篡改风险

  • 冒充风险

HTTP各版本区别

HTTP1.x 版本数据传输是通过多个TCP实现的,同时最多开启的TCP可以达到6~8个

1.0

  • 无法长连接,请求完数据立即断开TCP

1.1

  • 引入了长连接,TCP可以被多个请求复用
  • 引入了管道机制,同一个TCP里面可以发送多个请求,但服务器还是顺序执行,可能会出现“队头阻塞”
1
2
3
4
5
HTTP1.1 遗留问题:  
1、头部没有压缩就发送,数据量大。多个请求的头部是一样的
2、服务器是按照请求的顺序响应的,会出现“队头阻塞”
3、没有请求优先级控制
4、服务器无法主动下发数据

2.0

  • 二进制格式
    头信息和数据体都是二进制,并且统称为帧(frame):头信息帧和数据帧。
  • 数据流
    每个数据流都标记着一个独一无二的编号,其中规定客户端发出的数据流编号为奇数, 服务器发出的数据流编号为偶数。
  • 优先级
    客户端还可以指定数据流的优先级。优先级高的请求,服务器就先响应该请求。
  • 多路复用
    一个连接中并发多个请求或回应,而不用按照顺序一一对应。
  • 头部压缩
    同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。
  • 服务器推送
    请求 HTML 的时候,就提前把可能会用到的 JS、CSS 文件等静态资源主动发给客户端,减少延时的等待
1
2
遗留问题:
1.x 是通过多个TCP传输数据的,2.0 改为单个TCP传输数据,当一个TCP丢包需要重发时,会阻塞HTTP请求

3.0

  • 将 TCP 改为 UDP

HTTPS

HTTPS = HTTP + SSL/TLS

改善

  • 信息加密
    混合加密的方式实现信息的机密性,解决了窃听的风险。
  • 校验机制
    摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。
  • 身份证书
    将服务器公钥放入到数字证书中,解决了冒充的风险。

概念

HTTS 采用混合加密方式(非对称加密 + 对称加密),建立连接时使用非对称加密,建立连接后使用对称加密。

非对称加密

拥有两个密钥,公钥和私钥。公钥加密,私钥解密,反过来,私钥加密,公钥解密。
特性:加解密慢(使用了大量的乘除法)。适用于一对多通信。安全性高,私钥是保密的。

对称加密

加密和解密使用同一个秘钥
特性:加解密快(只使用了位移操作)。适用于一对一通信。秘钥难分发,难管理。

数字摘要(摘要算法、哈希算法、散列算法)

采用单向Hash函数生成一个不可逆的“摘要”。
常用的数字摘要算法:MD5、SHA-1、SHA-2
特点:单向,不可逆

1
数字摘要保证了数据的唯一性,相同的文本得到的“摘要”一定相等,不相等的文本得到的“摘要”极小概率相等(哈希碰撞)

数字签名

明文 + 数字摘要 进行加密,得到的就是数字签名

1
数字签名保证了数据的完整性

数字证书

数字签名用CA(权威的认证机构)的私钥加密,得到数字证书

1
数字证书保证和数据的安全性

证书认证流程

证书解析流程

连接流程

参考文章

https://www.toutiao.com/a6802216564595622408

TCP

TCP 全称传输控制协议(Transmission Control Protocol),是面向连接的、可靠的、基于字节流的传输层通信协议

概念

停止等待协议

A 每发一个包给 B,都必须收到 B 的确认(ACK) ,在规定的时间内 A 没有收到 ACK 包,则重传。

1
停止等待协议有一个问题,如果 B 收到了 A 的包,但是返回的 ACK 包丢失了,此时依然会触发 A 重传

累计确认(累计应答)

A同时发送5个数据包,并给这5个数据包序号(seq)(1,2,3,4,5),B再收到数据包后再返回的 ACK 包中返回确认号(ack),表示当前未收到的最小编号。通过这种方式就可以处理停止等待协议带来的问题。

1
2
3
4
5
6
如果B顺序收到1,2,3,4,5编号的包,那么返回的ack号分别为2,3,4,5,6  
如果B顺序收到3,5,1,4,2编号的包,那么返回的ack号分别为1,1,2,2,6

假如3数据包发送时丢包了
如果B顺序收到1,2,4,5编号的包,那么返回的ack号分别为2,3,3,3
如果B顺序收到4,5,1,2编号的包,那么返回的ack号分别为1,1,2,3

流量控制

每个计算机处理能力不一样,如果发送太快,接受太慢怎么办?
A、B 在每个数据包中加上一个值,叫窗口大小(win),表示接收能力

拥塞控制

网络很差时,造成了网络拥塞,假设拥塞窗口的大小为cwnd,流量控制的滑动窗口的大小为rwnd,
那么窗口大小 = min(cwnd, rwnd)。

慢启动

如何知道拥塞窗口的大小呢?可以采用试探法,先发窗口大小为1的包,如果不丢包,就发送窗口为2、4、8的包,直到出现丢包,从而得到最终的拥塞窗口。

滑动窗口

发送一个数据包过去,不需要等待数据包回来再发送

拥塞机制

一旦出现丢包,就会触发拥塞机制(慢启动、拥塞规避、快速启动、快速恢复)

TCP三次握手

  • 客户端向服务器发出连接请求报文,同部位SYN=1,初始序列号seq=x,此时客户端进程进入了SYN-SENT(同步已发送状态)状态。
  • 服务器收到请求报文后,发出确认报文,确认报文中 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时服务器进入了SYN-RCVD(同步收到)状态。
  • 客户端收到报文后,发出确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。
  • 当服务器收到客户端的确认后也进入 ESTABLISHED 状态,此后双方就可以开始通信了。

为什么是三次

1、防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误,两次握手会使得客户端和服务端再次建立连接,导致不必要的资源浪费。
2、其实也可以说是四次,只是中间两次合并成一次发送了。理由是TCP不允许半连接状态下传输数据。

SYN攻击

客户端在短时间内伪造了大量的IP进行连接,服务器回复响应包,但是源地址是不存在的,所以服务端会不断的重发,直到重发超时。
这些伪造的SYN包将长时间占用未连接队列,影响了正常的SYN,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。

1
2
3
4
部分解决方案:
1、延迟TCB分配方法,缓存半连接状态信息,建立连接后在分配
2、增加最大半连接数
3、缩短超时时间

四次挥手

  • 客户端发出断开连接报文,并且停止发送数据。FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),客户端进入FIN-WAIT-1(终止等待1)状态。
  • 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,服务端就进入了CLOSE-WAIT(关闭等待)状态。这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
  • 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
  • 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w。服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
  • 客户端收到服务器的连接释放报文后,发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。必须经过2*MSL(最长报文段寿命)的时间后,才进入CLOSED状态。
  • 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。服务器结束TCP连接的时间要比客户端早一些。

为什么要 TIME_WAIT,等待2个MSL后才关闭TCP (2个MSL(Max Segment Lifetime,约240秒))

  • 防止上一次连接中的包,重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)
  • 发送的最后一个ack(fin) ,有可能丢失,这时被动方会重新发fin。

为什么是三次握手,四次挥手?关闭一定是四次吗?

  • 连接三次是因为中间两次合并成一次了
  • TCP是全双工模式,客户端第一次发送FIN报文,只表示不会再请求数据,但此时服务器还可能需要继续发送数据,等服务器数据发送完,服务器才会发送FIN报文。
  • 如果一端发送FIN报文后,另一端不再需要传输数据,那么第二次挥手的ACK报文会和第三次挥手的FIN报文合并发送过去,此时关闭连接只需要三次挥手。

客户端突然出现故障怎么处理

服务器每收到一次客户端的请求后都会重新复位一个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

TCP 报文格式

TCP是怎么保证数据可靠性传输的

  • 序列号和确认应答信号
  • 超时重发控制
  • 数据校验
  • 连接管理

粘包、拆包

报文太短,需要合并后发送,报文太长,需要分开发送

解决方案

  • 指明数据包长度
  • 结尾加入特殊字符’\n’之类的

参考文章

https://www.cnblogs.com/xiaolincoding/p/12732052.html
https://www.toutiao.com/i6862639863104012814
https://www.toutiao.com/i6893802302663033355
https://mp.weixin.qq.com/s/Uf42QEL6WUSHOwJ403FwOA

两种方法
1、machO文件中 通过 __objc_classlist __objc_classrefs 对比
2、运行时获取到未使用的类(需要大量测试,或者线上测试)
两种方法都不能绝对识别准确,但合并起来准确率很高了,最终再手动确定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <dlfcn.h>
#include <mach-o/dyld.h>
#include <mach-o/getsect.h>

/* 获取 runtime 未使用的类 */
NSMutableSet<NSString *> *runtimeUnusedClass() {
Dl_info info;
dladdr((const void *)&runtimeUnusedClass, &info);

const uint64_t mach_header = (uint64_t)info.dli_fbase;

const struct section_64 *classlist = getsectbynamefromheader_64((const struct mach_header_64 *)mach_header, "__DATA", "__objc_classlist");

if (classlist) {
NSMutableSet *classlistUnused = [[NSMutableSet alloc] init];
// 遍历拿到所有的类
for (UInt64 addr = classlist->offset; addr < classlist->offset + classlist->size; addr += sizeof(const char **)) {
uint64_t baseArrr = mach_header + addr;

//获取类对象指针
uint64_t object_class_addr = *(uint64_t *)(baseArrr);
//
uint64_t object_class_isa = *(uint64_t *)(object_class_addr);
//获取元类对象
uint64_t object_meta_class_addr;
if (object_class_isa & (1 << 0)) {
object_meta_class_addr = object_class_addr;
} else {
object_meta_class_addr = object_class_isa & 0x00007ffffffffff8ULL;
}
//偏移32个字节,拿到 class_data_bits 的地址
uint64_t class_data_bits_addr = object_meta_class_addr + 32;
//取 class_data_bits 的值
uint64_t class_data_bits = *(uint64_t *)(class_data_bits_addr);
//得到 class_rw_t 的指针 (class_rw_t *)(bits & FAST_DATA_MASK);
uint64_t class_rw_addr = (class_data_bits & 0x00007ffffffffff8UL);
//取指针的值,得到的第一个数据就是 flags
uint64_t flags = *(uint64_t *)(class_rw_addr);
//
bool isInitialized = flags & (1 << 29);

if (!isInitialized) {
//打印类会导致类初始化,所以此方法只能使用一次
NSString *class = [NSString stringWithFormat:@"%@", (void *)object_class_addr];
[classlistUnused addObject:class];
}
}
return classlistUnused;
}
return nil;
}

/* 获取 MachO 未使用的类 */
NSMutableSet<NSString *> *machOUnusedClass() {
Dl_info info;
dladdr((const void *)&machOUnusedClass, &info);

const uint64_t mach_header = (uint64_t)info.dli_fbase;

const struct section_64 *classlist = getsectbynamefromheader_64((const struct mach_header_64 *)mach_header, "__DATA", "__objc_classlist");
const struct section_64 *selfrefs = getsectbynamefromheader_64((const struct mach_header_64 *)mach_header, "__DATA", "__objc_classrefs");

if (classlist && selfrefs) {
NSMutableSet *classlistSet = [[NSMutableSet alloc] init];
for (UInt64 addr = classlist->offset; addr < classlist->offset + classlist->size; addr += sizeof(const char **)) {
uint64_t baseArrr = mach_header + addr;
Class cls = (__bridge Class)(*(void **)(baseArrr));
NSString *clsString = [NSString stringWithFormat:@"%@",cls];
[classlistSet addObject:clsString];
}

NSMutableSet *selfrefsSet = [[NSMutableSet alloc] init];
for (UInt64 addr = selfrefs->offset; addr < selfrefs->offset + selfrefs->size; addr += sizeof(const char **)) {
uint64_t baseArrr = mach_header + addr;

Class cls = (__bridge Class)(*(void **)(baseArrr));
while (cls) {
[selfrefsSet addObject:[NSString stringWithFormat:@"%@",cls]];
cls = [cls superclass];
}
}
[classlistSet minusSet:selfrefsSet];

return classlistSet;
}

return nil;
}

缺点

1、runtimeUnusedClass 方法只有在第一次调用有效,而且必须手动浏览所有的页面(可以考虑放到线上收集)
2、两种方法都是不准确的,但未使用的类一定在其中,最终需要手动再次确认。

线程&进程

进程

程序执行的一个实例,表示一个正在运行的程序,是系统进行资源分配的基本单元,拥有一个完整的虚拟地址空间。

线程

轻量级进程,程序执行的最小单元,是进程里面的一个实体,线程与资源分配无关,线程自己不拥有资源(只有少量寄存器、栈、线程控制表TCB),线程依赖进程,并与进程内其他线程共享资源。

区别

调度:线程是程序调度、分配的基本单位,而进程拥有资源,各司其职,显著的提高系统的并发运行。同一个进程中,线程切换,进程不需要切换,而进程切换,线程也必须切换。线程切换只需要保存自己的寄存器以及堆栈数据,线程切换还需要分配新的资源。
并发性:进程之间可以并发,一个进程内的多个线程亦可并发,利用好线程的并发性可以更好地利用资源
资源:线程只拥有少量资源(寄存器、栈),但可以使用进程的资源
系统开销:进程切换开销大于线程切换的开销(进程:切换虚拟地址空间、切换CPU上下文、切换内核栈 线程:切换CPU上下文、切换内核栈)
通信方式:进程间通信需要通过IPC(本身也是一个程序),而线程间通信只是简单的读写数据段。进程资源互不影响,而线程资源则要考虑同步、互斥的问题

1
2
3
4
页表可以将虚拟地址转换为物理内存地址,页表查找很慢,通常使用缓存来加快查找,切换进程,意味着虚拟内存切换、页表切换,从而导致缓存命中率低,查找变慢 

页表: 类似于字典一个虚拟地址对应一个物理地址,真实机制没搞懂
虚实地址的映射关系是通过页表来描述的,而mmu正是通过页表来查找虚地址所对应的物理地址。

进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

我自己理解的线程&进程

进程就是正在执行的程序,线程就是这个程序的最小执行单元,一个进程通常拥有多个线程。进程拥有独立的资源以及虚拟地址空间,而线程只拥有寄存器、栈等少量资源,但进程内部的线程可以共享进程的资源,包括虚拟内存地址。

为什么进程切换比线程切换耗资源

  • 线程切换就是栈、寄存器的切换。
  • 进程的切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

WebSocket

Websocket 处于应用层协议,他必须依赖 HTTP 协议进行一次握手,握手成功后直接通过单个 TCP 传输数据。
Websocket 是为了解决 HTTP 轮询、长轮询的问题

  • 轮询:每隔一定时间发出一个请求,耗资源
  • 长轮询:客户端发送一个超长时间的请求,服务器hold住这个请求,直到有新数据时返回

WebSocket & Socket

Socket 本身并不是一个协议。它工作在 OSI 模型会话层(第5层),是为了方便大家直接使用更底层协议(一般是 TCP 或 UDP )而存在的一个抽象层。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API)。
Socket 可以指定不同的传输协议(TCP 、UDP)

1
WebSocket 和 Socket 本质没有什么关系。WebSocket 基于 TCP ,Socket 可以基于 TCP、UDP

WebSocket & HTTP

  • HTTP 是基于请求-应答的方式,WebSocket是双向通信的
  • WebSocket 连接使用的是HTTP协议进行连接的,发送了一个标记了 Upgrade 字段的请求,定义了一系列新的header域,标明是 WebSocket 连接。
  • 都是基于TCP的应用层协议。

Socket

网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
Socket 本身并不是一个协议,而是一个调用接口。它工作在 OSI 模型会话层,是为了方便大家直接使用更底层协议(一般是 TCP 或 UDP )而存在的一个抽象层。Socket 是对 TCP/IP 协议的封装。

Socket 连接流程

创建 Socket 的时候,可以指定网络层使用的是 IPv4 还是 IPv6,传输层使用的是 TCP 还是 UDP。

TCP方式 服务器

  • 创建套接字(socket)
  • 将套接字绑定到一个本地地址和端口上(bind)
    当内核收到TCP报文,会通过端口号找到对应的Socket
  • 将套接字设为监听模式,准备接收客户端请求(listen)
  • 等待客户请求到来,当请求到来后,接收连接请求,返回一个新的对应于此次连接的套接字(accept)
    监听 Socket 和真正用来传数据的 Socket 是两个,监听 Socket 以及 已连接 Socket
  • 用返回的套接字和客户端进行通信(send/recv)
  • 返回,等待另一客户请求
  • 关闭套接字

在 TCP 连接时,内核为每个 Socket 维护两个队列

  • TCP 半连接队列,还没完全建立连接的队列
  • TCP 全连接队列,已经建立连接的队列

TCP方式 客户端

  • 创建套接字(socket)
  • 向服务器发出连接请求(connect)
  • 和服务器端进行通信(send/recv)
  • 关闭套接字

UDP方式 服务器端

  • 创建套接字(socket)
  • 将套接字绑定到一个本地地址和端口上(bind)
  • 等待接收数据(recvfrom)
  • 关闭套接字

UDP方式 客户端

  • 创建套接字(socket)
  • 向服务器发送数据(sendto)
  • 关闭套接字

如何保证 Socket 的长连接

应用层自己实现心跳包

MVC

苹果的 UIVieController 就是根据 MVC 框架来设计的,UIVieController 持有 View 跟 Model(自己实现),对于一个简单的页面而言在 UIVieController 负责处理业务逻辑,View 负责显示 UI 以及接受用户事件,并将事件传递给 UIVieController,而 Model 负责存储数据。

庞大的 C

苹果的 MVC 中, C 包含了大量的代码,包括:设置 View 的代码,监听 Model 的代码,网络相关业务逻辑,页面跳转逻辑,这些都是在 C 中实现的。Model 中仅仅包含一些数据,简简单单的一个瘦Model,或者可以将部分数据组装、格式化的逻辑放在 Model 中,让这个 Model 慢慢变成胖Model。
而在传统的 MVC 中,Model 是负责网络相关的业务逻辑的,数据通过 Model 提供的接口异步获取,所有跟数据相关的逻辑都应该放在 Model 中。这样做似乎更加合理,但是我在 iOS 开发中却很少见到这样的代码

分离 C 中的代码

  1. 设置 View 的代码 可以放在 View 里面(或者 View 分类),事实上我见到的代码很多都是这样做的,甚至不可避免的在 View 中 还会包含一些简单的展示逻辑。此时也可以将监听的代码放在 View 中实现。考虑到苹果原生的 KVO 好多坑,这里可以用 Facebook 的 KVOController 这个库。
  2. 网络逻辑本应该放在 Model 中的,但是没放,所以可以将网络逻辑抽出来放在一个单独的文件中,将返回的数据处理好后再交给 Model
  3. 分模块,很多 MVC 之所以会“肿”,是因为将 UIVieController 完全当成 C 了,对于一些业务分开的界面,完全可以单独写成一个 MVC 。这样更加合理,也方便修改。对于很复杂的页面,例如包含 UITableView 的页面,就应该将 UITableView 相关的逻辑写成一个 MVC ,每种不同的类型的 UITableViewCell 写成一个 MVC ,最终变成 UITableView(MVC)+ N种类型的 UITableViewCell(MVC)。(PS:实际上项目中这里我都写成 MVC + MVVM 了,不过思想是一样的)

优点

  1. 简单,很容易上手
  2. 相比于其他框架,MVC代码量最少,非常适用于一些业务简单的页面

缺点

  1. 不太适用于较复杂的页面
  2. 业务逻辑都写在 C 中,但 C 和 V 紧密联系在一起,边界缺失,很难对 C 进行单元测试。想象一下,假如现在需要测试 C ,那么 C 的接口在哪里?

MVP

MVP 改进了 MVC,将原来 C 中的业务移到 P 中,V 和 P 通过接口通信 ,感觉好像跟 MVC 没什么区别啊!??其实是有的
最明显的区别就是 MVP 中可以对 P 跟 M 进行单元测试,V 持有 P ,P 持有 M,这样我们很容易模拟 V , 从而对 P 跟 M 进行单元测试。

优点

  1. 易测试,可以对 P 跟 M 进行单元测试

缺点

  1. V 和 P 之间引入了大量的接口

MVVM

MVVM 改进了 MVP,通过双向绑定机制解决了 MVP 接口很多的问题,MVVM 中的 VM 对应 MVP 的 P,负责处理业务逻辑。通过绑定机制,当 M 发生改变时更新 VM ,VM 发生改变时更新 V ,这些数据更新都不需要通过接口实现。在 iOS 中,这种绑定机制可以用 ReactiveCocoa 或者用 Facebook 的 KVOController + KVC 实现。

1
PS:使用 Swift 自身的特性应该可以更好地实现 MVVM,但由于本人目前不太熟悉 Swift ,Demo 中并没有使用 Swift 自身的特性

优点

  1. 易测试,可以对 VM 进行单元测试
  2. 双向绑定简化代码

缺点

  1. 双向绑定使得查找 bug 变难

MVVM vs MVC-VM

在实际开发中不使用绑定机制也可以将数据相关的业务放在 VM 中,这样整个框架看起来就像是 MVC-VM,同样也可以对 VM 进行单元测试。在不使用 ReactiveCocoa 的情况下,这种代码框架在我接触的项目中占比很大,其实很多文章把这种代码框架也叫做 MVVM

总结

  1. MVC 简单易上手,但很难进行单元测试(其实,项目开发本来就很少单元测试,甚至没有)
  2. MVP 改进了 MVC ,使得 MVP 很容易进行单元测试,但同时也使得 V 和 P 之间出现一堆交互接口
  3. MVVM 改进了 MVP,使用双向绑定简化了 V 和 P 之间的交互接口
  4. 在没有使用 ReactiveCocoa 等框架的情况下,使用没有绑定机制的 MVVM(MVC-VM) 也是个不错的选择

Demo

Demo

目前公司项目中用到的 Swift 比较少,所以对 Swift 的理解也很局限。这里把一些放在笔记中的内容整理下分享出来。

as

编译时检测,有两个意思
1、指定文字表达类型
2、upcast(向上转型,转换成其父类类型)

1
2
3
4
5
6
7
8
9
10
//指定 1 的类型为 CGFloat 类型,既变量 num 为 CGFloat 类型
let num = 1 as CGFloat
//
class Animal {}
class Dog: Animal {}
let dog = Dog()
dog as Animal //把 dog 转换为 Animal 类型,向上转型成功,编译器不会报错
//
let dog: Animal = Dog()
dog as Dog //编译错误,此时的变量 dog 在编译时是 Animal 类型,只能向上转换,无法向下转换。

as! as?

运行时检测,downcast(向下转型,转换成其子类类型)
只不过前者是强制解包,解包失败就崩溃
后者是可选类型

1
2
3
4
5
//下面代码编译时均不会报错,因为 as! 和 as? 都是运行时检查的
let a: Animal = Animal()
a as! Dog
1 as! Dog
1 as? Dog
1
2
3
4
5
6
7
8
9
10
class Dog: Animal {
var name = "Spot"
}

let dog: Animal = Dog()
let dog1 = dog as? Dog //可选值
let dog2 = dog as! Dog //强制解压

dog1?.name //可选调用
dog2.name //直接调用

总结

1、as 在编译时检测,as! as? 在运行时检测
2、as 可以用来指定文字表达类型以及向上转型
3、as! as? 用来向下转型,as? 转型后为可选值,as! 相当于在这个可选值上强制解压(可能会导致崩溃)