docs: 汇编研究
1
Chapter1 - iOS/1.107.md
Normal file
@@ -0,0 +1 @@
|
||||
# IM技术
|
||||
5
Chapter1 - iOS/1.108.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 精准测试
|
||||
|
||||
- https://blog.csdn.net/diy534/article/details/7099049
|
||||
- https://sq.sf.163.com/blog/article/180397339783520256
|
||||
- https://www.jianshu.com/p/a9679f47711c
|
||||
640
Chapter1 - iOS/1.109.md
Normal file
@@ -0,0 +1,640 @@
|
||||
# 汇编学习
|
||||
|
||||
## 基础知识回顾
|
||||
|
||||
地址总线:它的宽度决定了 CPU 的寻址能力。比如8086地址总线宽度为20,则寻址能力为1M(2的20次方)
|
||||
数据总线:它的宽度决定了 CPU 单次数据传送量,也就是数据传输速度。比如8086的数据总线宽度为16,所以单次最大传递2个字节的数据/
|
||||
控制总线:它的宽度决定了 CPU 对其他其间的控制能力,能有多少种控制。
|
||||
|
||||
内存的分段管理:起始地址+偏移地址=物理地址,计算出物理地址再去访问内存。
|
||||
|
||||
偏移地址为16位,16位地址的寻址能力位64kb,所以一个段的长度最大为64kb。
|
||||
|
||||
## CPU 的典型构成
|
||||
|
||||
- 寄存器:信息存储
|
||||
|
||||
- 运算器:信息处理
|
||||
|
||||
- 控制器:控制其他器件进行工作
|
||||
|
||||
对开发同学来说,CPU 中最主要的部件就是寄存器,“可以通过改变寄存器的内容来实现对 CPU 的控制”。
|
||||
|
||||
不同的 CPU,寄存器个数、结构是不同的(比如8086是16为结构的 CPU,8086有14个寄存器)
|
||||
|
||||
## 通用寄存器
|
||||
|
||||
AX、BX、CX、DX 这4个寄存器通常用来存放一般性的数据,成为通用寄存器(有时候也有特定用途)
|
||||
|
||||
通常 CPU 会把内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
|
||||
|
||||
例如:在内存中有一个内存空间a值为3,需要将它的值加1,然后将结果存储到内存空间b上。
|
||||
|
||||
- `mov ax, a`。CPU 首先会将内存空间 a 的值放到寄存器 ax 中
|
||||
|
||||
- `add ax, 1`。调用 add 指令,将 ax + 1
|
||||
|
||||
- `mov b, ax`。调用 mov 将 ax 中的值赋值给内存空间 b
|
||||
|
||||
### CS和IP
|
||||
|
||||
CS 为代码段,IP 为指令指针寄存器,它们代表 CPU 当前要读取指令的地址
|
||||
|
||||
任意时刻,8086 CPU 都会将 `CS:IP` 指向的指令作为下一条需要取出执行的指令。
|
||||
|
||||
IP 只为 CS 提供服务。
|
||||
|
||||
8086 CPU 工作过程如下:
|
||||
|
||||
- 从 `CS:IP` 指向的内存单元读取指令,读取的指令进入指令缓冲器
|
||||
|
||||
- IP = IP + 当前所读取指令的长度。从而指向 IP 指向下一条指令
|
||||
|
||||
- 执行指令,转到步骤1,重复执行
|
||||
|
||||
在8086CPU 加电启动或者复位后(即 CPU 刚开始工作时)CS 被设置位 CS=FFFFH,IP 被设置为 IP=0000H,即在8086PC 机刚启动时,CPU 从内存 FFFF0H 单元中读取指令执行,FFFF0H 单元中的指令是 8086PC 机开机后执行的第一条指令。
|
||||
|
||||
注意:在内存或者磁盘上,指令和数据其实没有差别,都是二进制信息。CPU 在工作时把有的信息看成指令,有些信息看数据,为同样的信息赋予了不同意义。
|
||||
|
||||
那么 CPU 根据什么来判断这块内存上的信息是指令还是数据?
|
||||
|
||||
- CPU 将 `CS:IP` 所指向的内存单元的内容看作指令
|
||||
|
||||
- 如果内存中的某段内容曾被 CPU 执行过,那么它所在的内存单元肯定被 `CS:IP` 指向过
|
||||
|
||||
### jmp 指令
|
||||
|
||||
mov 指令不能用于设置 CS、IP 的值,8086没有提供该功能。可以通过 jmp 指令来实现修改 CS、IP 的值,这些指令被成为转移指令。
|
||||
|
||||
`jmp 段地址:偏移地址` 可以实现同时修改 CS、IP 的值,表示用指令中给出的段地址修改 CS,偏移地址 IP。
|
||||
|
||||
`jmp 2AE3:3` 执行后表示:CS=2AE3H,IP=0003H,CPU 将从2AE33H处读取指令。
|
||||
|
||||
QA:下面3条指令执行完毕后,CPU 修改了几次 IP 寄存器?
|
||||
|
||||
```shell
|
||||
mov ax, bx
|
||||
sub ax, ax
|
||||
jmp ax
|
||||
```
|
||||
|
||||
修改了4次。每执行一条指令,IP 都会被修改1次(IP=IP+该条指令的长度),最后一条指令执行后,IP 寄存器的值也会被修改1次,共3+1=4次。
|
||||
|
||||
### ds 寄存器
|
||||
|
||||
CPU 要读写一个内存单元时,必须要给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成。
|
||||
|
||||
8086中有一个 DS 段寄存器,通常用来存放要访问数据的段地址
|
||||
|
||||
```shell
|
||||
mov bx, 1000H
|
||||
mov ds, bx
|
||||
mov al, [0]
|
||||
```
|
||||
|
||||
上面3条指令的意思是将 10000H (1000:0)中的内存数据,赋值到 al 寄存器中。
|
||||
|
||||
`mov al, [address]` 的意思是将 DS:address 该地址中的内存数据赋值到 al 寄存器中。
|
||||
|
||||
由于 al 是8位寄存器,所以上述命令是将一个字节的数据赋值给 al 寄存器。
|
||||
|
||||
tips:8086 不支持将数据直接送入段寄存器,所以 `mov ds, 1000H` 是错误的。
|
||||
|
||||
QA:写指令来实现将 al 中的数据写入到内存单元 10000H 中
|
||||
|
||||
```shell
|
||||
mov ax, 1000
|
||||
mov ds, ax
|
||||
mov [0], al
|
||||
```
|
||||
|
||||
QA:内存中有如下数据,写出下面指令执行后寄存器 ax 的值?
|
||||
|
||||
| 10000H | 23 |
|
||||
| ------ | --- |
|
||||
| 10001H | 11 |
|
||||
| 10002H | 22 |
|
||||
| 10003H | 66 |
|
||||
|
||||
```shell
|
||||
mov ax, 1000H
|
||||
mov ds, ax
|
||||
mov ax, [2]
|
||||
```
|
||||
|
||||
代码分析:
|
||||
|
||||
- 第一条指令,ax 寄存器存放了 1000H 这个地址
|
||||
|
||||
- 第二条指令,将访问 ds 数据段,在 1000H 这个地址出访问
|
||||
|
||||
- 第三条指令,将数据段中 1000H 这个地址处,偏移 2,也就是内存中 10002H 这个的值22写入到 ax 中,由于 ax 寄存器是16位,所以会取2个单位的数据,22和66。所以 ax 的值为2266。
|
||||
|
||||
8086 CPU 下,AX、BX、CX、DX 等通用寄存器均被分为高位和低位,AX = AH + AL,其中高位寄存器和低位寄存器。高位和低位都是16位。所以会从10002H 开始取2个16位的数据赋值给 ax。
|
||||
|
||||
思考:如果代码改变下呢,如下
|
||||
|
||||
```shell
|
||||
mov ax, 1000H
|
||||
mov ds, ax
|
||||
mov al, [2]
|
||||
```
|
||||
|
||||
此时 al 的值为多少?al 和 ax 的区别在于 ax = ah + al,所以 al 的情况下直接从 10002H 开始取1个16位的数据,所以 al 为 0022。
|
||||
|
||||
### 大小端序
|
||||
|
||||
小端序,指的是数据的高字节保存在内存的高地址中,数据的低字节保存在内存的低地址中
|
||||
|
||||
大端序,指的是数据的高字节保存在内存的低地址中,数据的低字节保存在内存的高地址中。
|
||||
|
||||
注意:这里的大小端序还存在网络大小端序 NBO 和主机大小端序 HBO,详细可以查看我[这篇文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.3.md)
|
||||
|
||||
Big Endian:PowerPC、IBM、Sun
|
||||
|
||||
Little Endian:x86、DEC
|
||||
|
||||
ARM:大小端序模式下均可工作。
|
||||
|
||||
16bit 宽的数0x1234 分别在大小端序模式的 CPU 内存存放形式为
|
||||
|
||||
| 内存地址 | 小端序 | 大端序 |
|
||||
| ------ | ---- | ---- |
|
||||
| 0x4000 | 0x34 | 0x12 |
|
||||
| 0x4001 | 0x12 | 0x34 |
|
||||
|
||||
32bit宽的 0x12345678 分别在大小端序模式的 CPU 内存存放形式为
|
||||
|
||||
| 内存地址 | 小端序 | 大端序 |
|
||||
| ------ | ---- | ---- |
|
||||
| 0x4000 | 0x78 | 0x12 |
|
||||
| 0x4001 | 0x56 | 0x34 |
|
||||
| 0x4002 | 0x34 | 0x56 |
|
||||
| 0x4003 | 0x12 | 0x78 |
|
||||
|
||||
QA:将 0x1122 存放在 0x40002 中,如何存储?
|
||||
|
||||
分析 0x1122需要2个字节,0x40002 是1个字节,所以肯定需要在 0x40002和向后的一个字节中存储。然后考虑主机序的大小端情况。假设小端模式下:
|
||||
|
||||
0x40000
|
||||
|
||||
0x40001
|
||||
|
||||
0x40002 0x22
|
||||
|
||||
0x40003 0x11
|
||||
|
||||
### 指令操作明确 CPU 操作的内存
|
||||
|
||||
```shell
|
||||
mov ax, 1000H
|
||||
mov ds, ax
|
||||
mov word ptr [0], 66h
|
||||
```
|
||||
|
||||
上述代码先把 1000H 写入 ax 寄存器,然后访问数据段的 1000H 内存,然后将66h写入到数据段的0位置,但是 word 告诉了 CPU 需要操作2个字节,也就是 00 66
|
||||
|
||||
指令执行前:1000: 0000 11 22 00 00 00 00 00 00
|
||||
|
||||
指令执行后:1000: 0000 00 66 00 00 00 00 00 00
|
||||
|
||||
如果将第三行代码改为 `mov byte ptr [0], 66h`,意味着明确告诉计算机需要操作1个字节,也就是66 。
|
||||
|
||||
指令执行前:1000: 0000 11 22 00 00 00 00 00 00
|
||||
|
||||
指令执行后:1000: 0000 66 22 00 00 00 00 00 00
|
||||
|
||||
## 栈
|
||||
|
||||
栈是一种后进先出特点的数据存储空间(LIFO)
|
||||
|
||||

|
||||
|
||||
- 8086 会将 CS 作为代码段的段地址,将 `CS:IP` 指向的指令作为下一条需要取出执行的指令
|
||||
|
||||
- 8060会将 DS 作为数据段的段地址,`mov ax, [address]` 就是取出 `DS:address` 内存区域上的数据放到 ax 寄存器中
|
||||
|
||||
- 8086会将 SS 作为栈段的段地址,`SS:SP` 指向栈顶元素
|
||||
|
||||
- 8086提供了 PUSH 指令用来入栈,POP 出栈。PUSH ax 是将 ax 的数据入栈,pop ax 是将栈顶的数据送入 ax
|
||||
|
||||
SS: 栈的段地址
|
||||
|
||||
SP:堆栈寄存器存放栈的偏移地址
|
||||
|
||||
#### push
|
||||
|
||||

|
||||
|
||||
`push ax` 指令执行,会拆解为:
|
||||
|
||||
- `SP = SP-2` ,`SS:SP` 指向当前栈顶前面的单元,更新栈顶指针
|
||||
|
||||
- 将 ax 中的数据送入到 `SS:SP` 所指向的内存单元处
|
||||
|
||||
ax = ah + al,所以 ax 中的数据入栈需要占据2个单位(sp = sp - 2)
|
||||
|
||||
#### pop
|
||||
|
||||

|
||||
|
||||
`pop ax` 指令执行,会拆解为:
|
||||
|
||||
- 将 `SS:SP` 栈顶指向的内存单元中的数据(2个单位)写入到 ax 寄存器中
|
||||
|
||||
- `SP = SP + 2`,更新 `SS:SP` 栈顶的地址
|
||||
|
||||
注意:
|
||||
|
||||
当一个栈空间是空的时候,`SS:SP` 指向栈空间最高地址单元的下一个单元。
|
||||
|
||||

|
||||
|
||||
当一个栈空或者满的时候,执行 PUSH、POP 指令需要注意,因为 `SP = SP + 2`、`SP = SP - 2` 都会导致将错误的数据入栈或者错误的数据出栈,导致发生不可预期的事情。
|
||||
|
||||
QA:将10000H~1000FH 这段空间当作栈,初始栈为空,AX = 001AH,BX=001BH,利用栈,交换 AX、BX 中的数据
|
||||
|
||||
```shell
|
||||
push ax
|
||||
push bx
|
||||
pop ax
|
||||
pop bx
|
||||
```
|
||||
|
||||
### 段总结
|
||||
|
||||
数据段:存放数据的段
|
||||
|
||||
代码段:存放代码的段
|
||||
|
||||
栈段:将一个段当作栈
|
||||
|
||||
对于数据段,将它的段地址放在 DS 中,用 mov、add、sub 等访问内存单元的指令时,CPU 则认为数据段中的内容是数据来访问
|
||||
|
||||
对于代码段,将它的段地址存放在 CS 中,将段中的第一条指令的偏移地址放在 IP 中,这样 CPU 就将执行我们定义的代码段的指令(每执行一条指令之前,就会将 IP 的值更新,规则为 IP = IP + 当前指令的长度,以保证该条指令执行完可以根据 段地址 + 偏移地址获取到下条指令的地址)
|
||||
|
||||
对于栈段,将它的段地址存放在 SS 中,将栈顶单元的偏移地址存放在 SP 中,这样 CPU 在进行栈操作(LIFO)的时候比如 push、pop 指令,就可以操作 SP,将我们定义的栈段当作栈空间来使用
|
||||
|
||||
### 中断
|
||||
|
||||
中断是由于软件或者硬件的信号,使得 CPU 暂停当前的任务,转而去执行另一段子程序。
|
||||
|
||||
在程序运行过程中,系统出现了一个必须由 CPU 立即处理的情况,此时,CPU 暂时终止当前程序的执行转而处理这个新情况的过程就叫中断。
|
||||
|
||||
中断分为:
|
||||
|
||||
- 硬中断(外中断):由外部设备(网卡、硬盘)随机引发的,比如当网卡收到数据包的时候,就会发出一个中断
|
||||
|
||||
- 软中断(内中断):由执行中断指令产生,可以通过程序控制触发
|
||||
|
||||
汇编中主要指的是软中断,可以通过指令 `int n` 产生中断,其中 `n` 表示中断码,内存中有一张中断向量表,用来存放中断码对应中断处理程序的入口地址。
|
||||
|
||||
Demo:写一个打印 Hello World 的汇编代码
|
||||
|
||||
```powershell
|
||||
; 提醒开发者每个段的定义(增加可读性)
|
||||
assume cs:code, ds:data
|
||||
;------ 数据段 begin --------
|
||||
data segment
|
||||
age db 20h
|
||||
no dw 30h
|
||||
db 10 dup(6) ; 生成连续10个6
|
||||
string db 'Hello world!$'
|
||||
data ends
|
||||
;------ 数据段 end --------
|
||||
|
||||
;------ 代码段 begin --------
|
||||
code sgement
|
||||
start:
|
||||
; 设置 ds 的值
|
||||
mov ax, data
|
||||
mov ds, ax
|
||||
|
||||
mov ax, no
|
||||
mov bl, age
|
||||
|
||||
; 打印字符串
|
||||
mov dx, offset string ; offset string 代表 string 的偏移地址(将地址赋值给 dx)
|
||||
mov ah, 9h
|
||||
int 21h ; 打印字符串其实也是一次中断
|
||||
|
||||
; 退出程序
|
||||
mov ax, 4c00h
|
||||
int 21h
|
||||
code ends
|
||||
end start
|
||||
;------ 代码段 end --------
|
||||
```
|
||||
|
||||
- start 代表汇编程序的入口
|
||||
|
||||
- `mov ax, 4c00h` 和 `int 21h` 代表程序正常中断
|
||||
|
||||
QA:“全局变量的地址在编译那一刻就确定好了”怎么理解?
|
||||
|
||||
全局变量存放在数据段,我们开发者写的代码存放在代码段,位置不一样,编译期就可以确定全局变量的地址。
|
||||
|
||||
### call 和 ret 指令
|
||||
|
||||
实现打印3次 "Hello"
|
||||
|
||||
方法1
|
||||
|
||||
```powershell
|
||||
assume ds:data, ss: stack, cs: code
|
||||
; 栈段
|
||||
stack segment
|
||||
|
||||
ends stack
|
||||
|
||||
; 数据段
|
||||
data segment
|
||||
|
||||
|
||||
ends data
|
||||
|
||||
; 代码段
|
||||
code segment
|
||||
start:
|
||||
; 设置 ds、ss
|
||||
mov ax, data
|
||||
mov ds, ax
|
||||
mov ax, stack
|
||||
mov ss, ax
|
||||
|
||||
; 业务逻辑
|
||||
; 打印
|
||||
; ds:dx 告诉字符串地址
|
||||
mov dx, offset string
|
||||
mov ah, 9h
|
||||
int 21h
|
||||
|
||||
mov dx, offset string
|
||||
mov ah, 9h
|
||||
int 21h
|
||||
|
||||
mov dx, offset string
|
||||
mov ah, 9h
|
||||
int 21h
|
||||
|
||||
; 程序正常退出
|
||||
mov ax, 4c00h
|
||||
int 21h
|
||||
code ends
|
||||
end start
|
||||
```
|
||||
|
||||
有没有问题?重复出现2次以及以上,需要封装为函数,汇编也遵循这个原则
|
||||
|
||||
方法2
|
||||
|
||||
```shell
|
||||
assume ds:data, ss: stack, cs: code
|
||||
; 栈段
|
||||
stack segment
|
||||
|
||||
ends stack
|
||||
|
||||
; 数据段
|
||||
data segment
|
||||
|
||||
|
||||
ends data
|
||||
|
||||
; 代码段
|
||||
code segment
|
||||
start:
|
||||
; 设置 ds、ss
|
||||
mov ax, data
|
||||
mov ds, ax
|
||||
mov ax, stack
|
||||
mov ss, ax
|
||||
|
||||
; 业务逻辑
|
||||
call print
|
||||
call print
|
||||
call print
|
||||
|
||||
; 程序正常退出
|
||||
mov ax, 4c00h
|
||||
int 21h
|
||||
print:
|
||||
; 打印
|
||||
; ds:dx 告诉字符串地址
|
||||
mov dx, offset string
|
||||
mov ah, 9h
|
||||
int 21h
|
||||
; 函数正常退出
|
||||
ret
|
||||
|
||||
code ends
|
||||
end start
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
call 会将下一条指令的偏移地址入栈;会转到标号(print:) 处执行指令
|
||||
|
||||
ret 会将栈顶的值出栈,赋值给 `CS:IP` ,ret 即 return
|
||||
|
||||
## 函数调用的本质
|
||||
|
||||
函数的3要素:参数、返回值、局部变量
|
||||
|
||||
#### 返回值
|
||||
|
||||
函数运算的结果,一般是放在 ax 通用寄存器中。可以拿 Xcode 将下面的代码执行下,断点开启在 test 方法内的 return 处(Debug - Debug WorkFlow - Always show Disassembly)
|
||||
|
||||
```objectivec
|
||||
#import <Foundation/Foundation.h>
|
||||
int test (void) {
|
||||
return 9;
|
||||
}
|
||||
int main(int argc, const char * argv[]) {
|
||||
int res = test();
|
||||
printf("%d", res);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
可以看到 return 的值是保存在 eax 寄存器中。为什么是 e,e是32位的意思(环境:老款 MBP 电脑运行)。
|
||||
|
||||
#### 参数
|
||||
|
||||
需要用的时候 push,最后不用则 pop,所以用栈来传参。
|
||||
|
||||
注意利用栈传递参数,在函数内部计算之后(return出来),需要保证**栈平衡**,也就是函数调用前后的栈顶指针要一致。
|
||||
|
||||
栈不平衡会导致栈空间迟早溢出,发生不可预期的错误。
|
||||
|
||||
Demo
|
||||
|
||||
```shell
|
||||
push 1122h
|
||||
push 2233h
|
||||
call sum
|
||||
add sp, 4
|
||||
|
||||
sum:
|
||||
; 访问栈中的参数
|
||||
mov bp, sp
|
||||
mov ax, ss:[bp + 2]
|
||||
add ax, ss:[bp + 4]
|
||||
ret
|
||||
```
|
||||
|
||||
- 上面代码调用2次 push 将方法参数入栈,调用 sum 方法(call sum),call 本质会将下一行指令的地址压入栈,所以目前栈有3个元素。
|
||||
|
||||
- 函数执行完毕,调用 ret 指令会将栈顶的指令 pop 出来,更改 `CS:IP` 然后马上执行
|
||||
|
||||
- 访问栈中的参数的时候,由于 call 指令会将下一条指令地址也入栈,所以访问需要 +2
|
||||
|
||||
- 访问栈中参数 +2 的时候不能直接 `sp + 2 `,需要用 bp
|
||||
|
||||
但目前函数调用结束了,栈里面还存在2个局部变量,导致空间浪费了,会存在“栈平衡”问题。所以在函数调用完毕,需要告诉内存,将栈顶指针恢复原位 `sp = sp + 4`(类比程序员告诉计算机,这块内存我不用了,后续其他人的代码可以用这块内存存某个值)
|
||||
|
||||
QA:stack overflow?
|
||||
|
||||
清楚函数调用原理 call、ret、stack 就知道函数调用函数,常见的递归或者循环,其实函数都在 stack 上进行操作,比如函数参数、函数下一条指令也会入栈,在递归或者函数内不断调用函数的过程中,stack 不及时”栈平衡“,很容易出现栈溢出的情况,也就是 stack overflow。
|
||||
|
||||
### 内平栈/外平栈
|
||||
|
||||
外平栈
|
||||
|
||||
```shell
|
||||
push 1122h
|
||||
push 2233h
|
||||
call sum
|
||||
add sp, 4
|
||||
|
||||
sum:
|
||||
; 访问栈中的参数
|
||||
mov bp, sp
|
||||
mov ax, ss:[bp + 2]
|
||||
add ax, ss:[bp + 4]
|
||||
ret
|
||||
```
|
||||
|
||||
内平栈
|
||||
|
||||
```shell
|
||||
push 1122h
|
||||
push 2233h
|
||||
call sum
|
||||
|
||||
sum:
|
||||
; 访问栈中的参数
|
||||
mov bp, sp
|
||||
mov ax, ss:[bp + 2]
|
||||
add ax, ss:[bp + 4]
|
||||
ret 4
|
||||
```
|
||||
|
||||
内平栈的好处是函数调用者不用去处理“栈平衡”
|
||||
|
||||
### 函数调用的约定
|
||||
|
||||
`__cdecl` 外平栈,参数从右到左入栈
|
||||
|
||||
`_stdcall` 内平栈,参数从右到左入栈
|
||||
|
||||
`_fastcall` 内平栈,ecx、edx 分别传递前面2个参数,其他参数从右到左入栈
|
||||
|
||||
寄存器传递参数效率更高,速度更快,iOS 平台函数采用6到8个 寄存器传参,剩余的从右到左入栈。
|
||||
|
||||
### c 代码可与汇编混合开发
|
||||
|
||||
验证函数的返回值是存放在 eax 寄存器中(eax 和 ax 区别在于位数)
|
||||
|
||||
```c
|
||||
#import <Foundation/Foundation.h>
|
||||
int test (int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
int main(int argc, const char * argv[]) {
|
||||
test(2, 8);
|
||||
int c = 0;
|
||||
__asm {
|
||||
mov c, eax
|
||||
}
|
||||
printf("%d", c);
|
||||
return 0;
|
||||
}
|
||||
// 10
|
||||
```
|
||||
|
||||
## 函数局部变量
|
||||
|
||||
大多数情况下函数内部会存在局部变量,但是不知道局部变量到底有多少,如何保证局部变量不会被污染呢?
|
||||
|
||||
CPU 会在栈内部,将局部变量的地方,临时分配10字节大小空间用来存储局部变量。这个怎么实现呢?`SP = SP - 10` 这条指令用来将栈顶指针改变,留出10字节大小空间。但是留出的空间是空的,万一 `CS:IP` 指向这块区域会把里面的数据当作指令去执行,则可能发生一些不可预知的错误。Windows 平台,针对预留的局部变量空间,会走动填充 cc,也就是 `int 3 ` 断点中断,只要 `CS:IP` 去执行就会断点中断,更安全。
|
||||
|
||||

|
||||
|
||||
关键代码如下:
|
||||
|
||||
```powershell
|
||||
; 返回值放在 ax 寄存器中
|
||||
; 传递2个参数(放入栈中)
|
||||
sum:
|
||||
; 保护 bp
|
||||
push bp
|
||||
; 保存 sp 之前的值;指向 bp 以前的值
|
||||
mov bp, sp
|
||||
; 预留10个字节的空间用来存放局部变量(栈,内部是高地址向)
|
||||
sub sp, 10
|
||||
|
||||
; 保护可能用到的寄存器
|
||||
push si
|
||||
push di
|
||||
push bx
|
||||
|
||||
; 给局部变量空间填充 int 3(cccc):调试中断,以增加 `CS:IP` 安全性
|
||||
; stosw 的作用:将 ax 的值拷贝到 es:di 中
|
||||
mov ax, 0cccch
|
||||
; 让 es 等于 ss
|
||||
mov es, ss
|
||||
mov es, bx
|
||||
; 让 di = bp - 10(局部变量地址的最小处)
|
||||
mov di, bp
|
||||
sub di, 10
|
||||
; cx 的值决定了 rep 的执行次数
|
||||
mov cx, 5
|
||||
; rep 重复执行某条指令(次数由 cx 的值决定)
|
||||
rep stosw
|
||||
|
||||
; 业务逻辑
|
||||
; 定义2个局部变量
|
||||
mov word ptr ss:[bp-2], 3
|
||||
mov word ptr ss:[bp-4], 4
|
||||
mov ax, ss:[bp-2]
|
||||
add ax, ss:[bp-4]
|
||||
mov ss:[bp-6], ax
|
||||
|
||||
; 访问栈中的参数
|
||||
mov ax, ss:[bp+4]
|
||||
add ax, ss:[bp+6]
|
||||
add ax, ss:[bp-6]
|
||||
|
||||
; 恢复寄存器中的值
|
||||
pop bx
|
||||
pop di
|
||||
pop si
|
||||
|
||||
; 恢复 sp
|
||||
mov sp, bp
|
||||
|
||||
; 恢复 bp
|
||||
|
||||
pop
|
||||
```
|
||||
|
||||
## 栈帧
|
||||
|
||||
Stack Frame Layout,代表一个函数的执行环境。包括:参数、返回地址、局部变量和包括在本函数内部执行的所有内存操作等
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -1320,7 +1320,7 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
|
||||
|
||||
- 性能优化
|
||||
|
||||
## NSTimer 经常会不准确,原因是什么?
|
||||
### NSTimer 经常会不准确,原因是什么?
|
||||
|
||||
NSTimer 在创建的时候经常会指派到特定的 NSRunLoopMode 中去,举个例子,默认创建的NSTimer 是被添加到 NSRunLoopDefaultMode 中去,当你的页面上有 UIScrollView 或者子类的时如果被拖动了,当前 RunLoop 的 NSRunloopMode 会从 NSDefaultRunLoopMode 转变为 UITrackingRunLoopMode 。遇到这种情况你需要精确的 NSTimer 的话,在创建好 NSTimer 之后,设置 RunLoopMod 为 NSRunLoopCommonModes。
|
||||
|
||||
@@ -1407,19 +1407,39 @@ UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示
|
||||
|
||||
### 自动释放池
|
||||
|
||||
自动释放池什么时候创建和释放
|
||||
自动释放池什么时候创建和释放?
|
||||
|
||||
创建时间:第一次进入 RunLoop 的时候
|
||||
App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_wrapRunLoopWithAutoReleasePoolHandler`。
|
||||
|
||||
释放时间:RunLoop 退出的时候
|
||||
第一个 Observer 监听 RunLoop 的 `kCFRunLoopEntry` 状态(即将进入 RunLoop),回调为 `_objc_autoreleasePoolPush()`,会创建自动释放池,其 order 为 `-2147483647`,优先级最高,保证创建自动释放池一定是发生在其他回调之前。
|
||||
|
||||
其他情况:当 RunLoop 将要休眠的时候释放,然后创建一个新的
|
||||
第二个 Observer 监听 RunLoop 2个事件:
|
||||
|
||||
**_wrapRunLoopWithAutoreleasePoolHandler** **0x1**
|
||||
- 监听 `kCFRunLoopBeforeWaiting` (将要休眠),回调为 `_objc_autoreleasePoolPop()` 和 `_objc_autoreleasePoolPush()`,用来释放旧的自动释放池,创建新的自动释放池。
|
||||
|
||||
**_wrapRunLoopWithAutoreleasePoolHandler** **0xa0**
|
||||
- 监听 `kCFRunLoopExit`(RunLoop 即将退出),回调为 `_objc_autoreleasePoolPop()`,Observer 优先级为 2147483647优先级最低,保证自动释放池的释放在其他所有的回调之后进行。
|
||||
|
||||
0x1 和 0xa0 是十六进制的数,对应十进制为1和160。
|
||||
总结版:在主线程执行的代码,通常是写在事件回调、Timer 回调内的,这些回调都会被 RunLoop 自身状态相关的 AutoreleasePool 所包裹,所以会自动管理内存,开发者不需要手动创建 AutoreleasePool。
|
||||
|
||||
### 事件响应
|
||||
|
||||
系统注册了 Source1(基于 Mach port)用来接收系统事件,其回调函数为 `__IOHIDEventSystemClientQueueCallback`
|
||||
|
||||
当一个硬件事件(触摸/锁屏/摇晃等)发生时,首先 `IOKit.Framework` 会生成一个 `IOHIDEvent` 事件并由 SpringBoard 接收。SpringBoard 只接收按键、触摸、传感器等几种 Event,然后通过 Mach Port 转发给需要处理的 App 进程。随后苹果注册的 Source1 就会触发回调,并调用 `_UIApplicationHandleEventQueue()` 进行应用内部的分发。
|
||||
|
||||
`_UIApplicationHandleEventQueue` 会把 `IOHIDEvent` 处理并包装成 UIEvent 进行处理和分发(其中包括 UIGesture、屏幕旋转等)。
|
||||
|
||||
### 手势识别
|
||||
|
||||
`_UIApplicationHandleEventQueue` 识别到一个手势时,首先会调用 cancel 将当前的 touchBegin/End/Move 系统回调打断,然后系统会将对应的 `UIGestureRecognizer ` 标记为待处理。
|
||||
|
||||
苹果注册了一个 Observer 监控 RunLoop 的 `kCFRunLoopBeforeWaiting`(将要休眠)状态,回调为 `_UIGestureRecognizerUpdateObserver`,其内部会获取所有刚被标记为待处理的 UIGestureRecognizer,并执行对应的回调。
|
||||
|
||||
### UI 刷新
|
||||
|
||||
当界面的 Frame 改变,或者更改 UIView、CALayer 的层次时,或者调用了 UIView、CALayer 的 setNeedsLayout、setNeedsDisplay 方法后,这个 UIView、CALayer 会被标记为待处理(类比前端的 Virtual Dom Diff,标记为 dirty),并被提交到一个全局容器中。
|
||||
|
||||
苹果设计 UI 更新也是 RunLoop 的业务方,所以会注册一个 Obserger 监控 `kCFRunLoopBeforeWaiting`(将要休眠)和 `kCFRunLoopExit` (即将退出 RunLoop)状态,然后会执行 `_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()` 回调。内部会遍历所有待处理的 UIView、CALayer 以执行实际的绘制和渲染,更新 UI
|
||||
|
||||
### RunLoop 空闲时做一些任务
|
||||
|
||||
@@ -1445,6 +1465,33 @@ UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示
|
||||
}
|
||||
```
|
||||
|
||||
### Crash 防护
|
||||
|
||||
利用监控手段,比如 C/OC crash、Signal、Mach 异常,当监控到异常之后,正常来说会发生闪退等,体验较差。某些场景下希望 App 从异常中恢复,重新启动,这个可以利用 RunLoop 实现。
|
||||
|
||||
```objectivec
|
||||
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
|
||||
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
|
||||
while (1) {
|
||||
for (NSString *mode in (__bridge NSArray *)allModes) {
|
||||
if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
|
||||
continue;
|
||||
}
|
||||
CFStringRef modeRef = (__bridge CFStringRef)mode;
|
||||
CFRunLoopRunInMode(modeRef, 0.1, false);
|
||||
}
|
||||
}
|
||||
CFRelease(allModes);
|
||||
```
|
||||
|
||||
但该方案存在一些缺点,需要衡量:
|
||||
|
||||
- 因为崩溃发生,将程序的控制权交给新的 RunLoop,所以不会返回原来的 RunLoop,函数调用堆栈将不会释放,造成泄漏
|
||||
|
||||
- 虽然可以保活,但是会增加业务异常风险,所以需要衡量
|
||||
|
||||
|
||||
|
||||
### 线程保活
|
||||
|
||||
应用场景:经常在子线程中处理某些逻辑的场景。如果销毁再创建再销毁再创建效率很低,这个情况下就需要线程保活。
|
||||
|
||||
@@ -2157,4 +2157,10 @@ iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为
|
||||
|
||||
Tips:Code Sign 加密哈希并不少针对于整个文件,而是针对于每一个 Page 的,保证了在 dyld 进行加载的时候,可以对每一个 page 进行独立验证。
|
||||
|
||||
等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。
|
||||
等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。
|
||||
|
||||
|
||||
|
||||
## 检测
|
||||
|
||||
根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)
|
||||
@@ -46,8 +46,6 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval
|
||||
1. 控制器不再强引用定时器
|
||||
2. 定时器不再保留当前的控制器
|
||||
|
||||
|
||||
|
||||
## 解决方案:
|
||||
|
||||
### 替换 NSTimer API
|
||||
@@ -84,14 +82,10 @@ dispatch_resume(timerSource);
|
||||
self.timer = timerSource;
|
||||
```
|
||||
|
||||
|
||||
|
||||
为什么 GCD timer 会更准确?因为普通定时器运行依赖 RunLoop,RunLoop 一个运行周期内的任务繁忙程度是不确定的。当某次任务繁重,那么定时器调度就不准时。
|
||||
|
||||
GCD timer 不依赖 RunLoop,系统底层驱动,所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动,RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。
|
||||
|
||||
|
||||
|
||||
### 打破循环引用,NSTimer target 自定义
|
||||
|
||||
```objectivec
|
||||
@@ -158,45 +152,45 @@ dispatch_semaphore_t semaphore_;
|
||||
+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
|
||||
{
|
||||
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
|
||||
|
||||
|
||||
// 队列
|
||||
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
|
||||
|
||||
|
||||
// 创建定时器
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
|
||||
|
||||
|
||||
// 设置时间
|
||||
dispatch_source_set_timer(timer,
|
||||
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
|
||||
interval * NSEC_PER_SEC, 0);
|
||||
|
||||
|
||||
|
||||
|
||||
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
|
||||
// 定时器的唯一标识
|
||||
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
|
||||
// 存放到字典中
|
||||
timers_[name] = timer;
|
||||
dispatch_semaphore_signal(semaphore_);
|
||||
|
||||
|
||||
// 设置回调
|
||||
dispatch_source_set_event_handler(timer, ^{
|
||||
task();
|
||||
|
||||
|
||||
if (!repeats) { // 不重复的任务
|
||||
[self cancelTask:name];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 启动定时器
|
||||
dispatch_resume(timer);
|
||||
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
|
||||
{
|
||||
if (!target || !selector) return nil;
|
||||
|
||||
|
||||
return [self execTask:^{
|
||||
if ([target respondsToSelector:selector]) {
|
||||
#pragma clang diagnostic push
|
||||
@@ -210,9 +204,9 @@ dispatch_semaphore_t semaphore_;
|
||||
+ (void)cancelTask:(NSString *)name
|
||||
{
|
||||
if (name.length == 0) return;
|
||||
|
||||
|
||||
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
|
||||
|
||||
|
||||
dispatch_source_t timer = timers_[name];
|
||||
if (timer) {
|
||||
dispatch_source_cancel(timer);
|
||||
@@ -245,8 +239,6 @@ dispatch_semaphore_t semaphore_;
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### NSProxy
|
||||
|
||||
```objectivec
|
||||
@@ -276,8 +268,6 @@ QA: 自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理
|
||||
|
||||
NSProxy 效率更高。继承自 NSObject 的代理,内部运行的时候还是存在方法查找(isa、superclass、cache、methods)流程。
|
||||
|
||||
|
||||
|
||||
看一段神奇的代码
|
||||
|
||||
`LBPProxy`
|
||||
@@ -297,7 +287,6 @@ NSProxy 效率更高。继承自 NSObject 的代理,内部运行的时候还
|
||||
return self.target;
|
||||
}
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
`LBPProxy2`
|
||||
@@ -352,9 +341,9 @@ appDelegateClassName = NSStringFromClass([AppDelegate class]);
|
||||
```objectivec
|
||||
- (BOOL) isKindOfClass: (Class)aClass
|
||||
{
|
||||
NSMethodSignature *sig;
|
||||
NSInvocation *inv;
|
||||
BOOL ret;
|
||||
NSMethodSignature *sig;
|
||||
NSInvocation *inv;
|
||||
BOOL ret;
|
||||
sig = [self methodSignatureForSelector: _cmd];
|
||||
inv = [NSInvocation invocationWithMethodSignature: sig];
|
||||
[inv setSelector: _cmd];
|
||||
@@ -367,8 +356,6 @@ appDelegateClassName = NSStringFromClass([AppDelegate class]);
|
||||
|
||||
可以看到内部直接调用了消息转发。
|
||||
|
||||
|
||||
|
||||
### 采用 Block 的形式为 NSTimer 增加分类
|
||||
|
||||
```objectivec
|
||||
@@ -487,4 +474,8 @@ __strong __typeof(&*weakSelf)self = weakSelf;
|
||||
}
|
||||
```
|
||||
|
||||
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
|
||||
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
|
||||
|
||||
## 检测
|
||||
|
||||
根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)
|
||||
@@ -535,4 +535,8 @@ NSLog(@"%zd", malloc_size(temp));
|
||||
|
||||
```c
|
||||
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */
|
||||
```
|
||||
```
|
||||
|
||||
为什么系统是由16字节对齐的?
|
||||
|
||||
成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
|
||||
@@ -2367,7 +2367,7 @@ static void GSLogZombie(id o, SEL sel){
|
||||
|
||||
申请内存 alloc 时在内存上填 `0xAA`,释放内存 dealloc 时在内存上填 `0x55`。此种方案也会让内存泄漏偶现的 crash 变为必现。
|
||||
|
||||
### 4. 野指针监控工具<a name="zombieSniffer"></a>
|
||||
### 5. 野指针监控工具<a name="zombieSniffer"></a>
|
||||
|
||||
##### 类 Zombie Object 方案
|
||||
|
||||
@@ -7778,6 +7778,16 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
7. APM 技术方案本身是随着技术手段、分析需求不断调整升级的。上图的几个结构示意图是早期几个版本的,目前使用的是在此基础上进行了升级和结构调整,提几个关键词:Hermes、Flink SQL、InfluxDB。
|
||||
|
||||
## 十四、未来规划
|
||||
|
||||
- 监控能力继续完善
|
||||
|
||||
- 技术持续研究,提升监控准确度、案发第一现场数据,便于排查问题
|
||||
|
||||
- APM 新方向的研究。UI 自动化结合 iOS Instrucments Server 中的性能数据(DTXMessage 通信)
|
||||
|
||||
- 开源 SDK + 桌面端 App 查看性能数据(产品侧)
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [iOS 保持界面流畅的技巧](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
> 相信在国内一些中小型公司,开发者很少会去写软件测试相关的代码。当然这背后有一些原因在。本文就讲讲 iOS 开发中的软件测试相关的内容。
|
||||
|
||||
|
||||
|
||||
## 一、 测试的重要性
|
||||
|
||||
测试很重要!测试很重要!测试很重要!重要的事情说三遍。
|
||||
@@ -16,10 +14,6 @@
|
||||
|
||||
相信大多数开发者都遇到过上述场景的问题。其实上述这几个问题都有解,那就是“软件测试”。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 二、软件测试
|
||||
|
||||
### 1. 分类
|
||||
@@ -28,8 +22,6 @@
|
||||
|
||||
合理应用软件测试技术,就可以规避掉第一部分的3个场景下的问题。
|
||||
|
||||
|
||||
|
||||
软件测试强调开发、测试同步进行,甚至是测试先行,从需求评审阶段就先考虑好软件测试方案,随后才进行技术方案评审、开发编码、单元测试、集成测试、系统测试、回归测试、验收测试等。
|
||||
|
||||
软件测试从测试范围分为:单元测试、集成测试、系统测试、回归测试、验收测试(有些公司会谈到“冒烟测试“,这个词的精确定义不知道,但是学软件测试课的时候按照范围就只有上述几个分类)。工程师自己负责的是单元测试。测试工程师、QA 负责的是集成测试、系统测试。
|
||||
@@ -38,8 +30,6 @@
|
||||
|
||||
软件测试从开发模式分为:面向测试驱动开发 TDD (Test-driven development)、面向行为驱动开发 BDD (Behavior-driven development)。
|
||||
|
||||
|
||||
|
||||
### 2. TDD
|
||||
|
||||
TDD 的思想是:先编写测试用例,再快速开发代码,然后在测试用例的保证下,可以方便安全地进行代码重构,提升应用程序的质量。一言以蔽之就是通过测试来推动开发的进行。正是由于这个特点,TDD 被广泛使用于敏捷开发。
|
||||
@@ -50,8 +40,6 @@ TDD 的思想是:先编写测试用例,再快速开发代码,然后在测
|
||||
|
||||
缺点:技术方案需要先评审结束、架构需要提前搭建好。假如需求变动,则前面步骤需要重新执行,灵活性较差。
|
||||
|
||||
|
||||
|
||||
### 3. BDD
|
||||
|
||||
BDD 即行为驱动开发,是敏捷开发**技术**之一,通过自然语言定义系统行为,以功能使用者的角度,编写需求场景,且这些行为描述可以直接形成需求文档,同时也是测试标准。
|
||||
@@ -60,22 +48,14 @@ BDD 的思想是跳出单一的函数,针对的是行为而展开的测试。B
|
||||
|
||||
优点:各团队的成员可以集中在一起,设计基于行为的计测试用例。
|
||||
|
||||
|
||||
|
||||
### 4. 对比
|
||||
|
||||
根据特点也就是找到了各自的使用场景,TDD 主要针对开发中的最小单元进行测试,适合单元测试。而 BDD 针对的是行为,所以测试范围可以再大一些,在集成测试、系统测试中都可以使用
|
||||
|
||||
|
||||
|
||||
TDD 编写的测试用例一般针对的是开发中的最小单元(比如某个类、函数、方法)而展开,适合单元测试。
|
||||
|
||||
BDD 编写的测试用例针对的是行为,测试范围更大一些,适合集成测试、系统测试阶段。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 三、 单元测试编码规范<a name="codeRules"></a>
|
||||
|
||||
本文的主要重点是针对日常开发阶段工程师可以做的事情,也就是单元测试而展开。
|
||||
@@ -98,19 +78,18 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
for (NSInteger index = 1; index <= 10000; index++) {
|
||||
HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
|
||||
model.log_id = index;
|
||||
// ...
|
||||
// ...
|
||||
[insertModels addObject:model];
|
||||
}
|
||||
// when
|
||||
[dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
|
||||
// then
|
||||
[dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) {
|
||||
// then
|
||||
[dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) {
|
||||
XCTAssert(count == insertModels.count, @"「数据增加」功能:异常");
|
||||
[exception fulfill];
|
||||
}];
|
||||
[self waitForExpectationsWithCommonTimeout];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
可以看到这个方法的名称为 testInsertDataInOneSpecifiedTable,这段代码做的事情通过函数名可以看出来:测试插入数据到某个特定的表。这个测试用例分为3部分:测试环境所需的先决条件准备;调用所要测试的某个方法、函数;验证输出和行为是否符合预期。
|
||||
@@ -119,8 +98,6 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
|
||||
所以单元测试的代码规范也就出来了。此外单元测试代码规范统一后,每个人的测试代码都按照这个标准展开,那其他人的阅读起来就更加容易、方便。按照这3个步骤去阅读、理解测试代码,就可以清晰明了的知道在做什么。
|
||||
|
||||
|
||||
|
||||
### 2. 一个测试用例只测试一个分支
|
||||
|
||||
我们写的代码有很多语句组成,有各种逻辑判断、分支(if...else、swicth)等等,因此一个程序从一个单一入口进去,过程可能产生 n 个不同的分支,但是程序的出口总是一个。所以由于这样的特性,我们的测试也需要针对这样的现状走完尽可能多的分支。相应的指标叫做「分支覆盖率」。
|
||||
@@ -153,8 +130,6 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3. 明确标识被测试类
|
||||
|
||||
这条主要站在团队合作和代码可读性角度出发来说明。写过单元测试的人都知道,可能某个函数本来就10行代码,可是为了测试它,测试代码写了30行。一个方法这样写问题不大,多看看就看明白是在测试哪个类的哪个方法。可是当这个类本身就很大,测试代码很大的情况下,不管是作者自身还是多年后负责维护的其他同事,看这个代码阅读成本会很大,需要先看测试文件名 `代码类名 + Test` 才知道是测试的是哪个类,看测试方法名 `test + 方法名` 才知道是测试的是哪个方法。
|
||||
@@ -201,8 +176,6 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 4. 使用分类来暴露私有方法、私有变量
|
||||
|
||||
某些场景下写的测试方法内部可能需要调用被测对象的私有方法,也可能需要访问被测对象的某个私有属性。但是测试类里面是访问不到被测类的私有属性和私有方法的,借助于 `Category` 可以实现这样的需求。
|
||||
@@ -221,7 +194,7 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
- (void)hello;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation HermesClientTest
|
||||
|
||||
- (void)testPrivatePropertyAndMethod
|
||||
@@ -232,8 +205,6 @@ BDD 编写的测试用例针对的是行为,测试范围更大一些,适合
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 四、 单元测试下开发模式、技术框架选择
|
||||
|
||||
单元测试是按照测试范围来划分的。TDD、BDD 是按照开发模式来划分的。因此就有各种排列组合,这里我们只关心单元测试下的 TDD、BDD 方案。
|
||||
@@ -254,22 +225,18 @@ TDD 开发过程类似下图:
|
||||
- 然后编写真正的功能实现代码。这时候去跑测试,测试通过,也就是到了绿色的状态
|
||||
- 在测试用例的保证下,可以重构、优化代码
|
||||
|
||||
|
||||
|
||||
**抛出一个问题:TDD 看上去很好,应该用它吗?**
|
||||
|
||||
这个问题不用着急回答,回答了也不会有对错之分。开发中经常是这样一个流程,新的需求出来后,先经过技术评审会议,确定宏观层面的技术方案、确定各个端的技术实现、使用的技术等,整理出开发文档、会议文档。工期评估后开始编码。事情这么简单吗?前期即使想的再充分、再细致,可能还是存在特殊 case 漏掉的情况,导致技术方案或者是技术实现的改变。如果采用 TDD,那么之前新功能给到后,就要考虑测试用例的设计、编写了测试代码,在测试用例的保证下再去实现功能。如果遇到了技术方案的变更,之前的测试用例要改变、测试代码实现要改变。可能新增的某个 case 导致大部分的测试代码和实现代码都要改变。
|
||||
|
||||
|
||||
|
||||
如何开展 TDD**
|
||||
|
||||
1. 新建一个工程,确保 “Include Unit Tests” 选项是选中的状态
|
||||
|
||||
|
||||

|
||||
|
||||
2. 创建后的工程目录如下
|
||||
|
||||
|
||||

|
||||
|
||||
3. 删除 Xcode 创建的测试模版文件 `TDDDemoTests.m`
|
||||
@@ -277,28 +244,28 @@ TDD 开发过程类似下图:
|
||||
4. 假如我们需要设计一个人类,它具有吃饭的功能,且当他吃完后会说一句“好饱啊”。
|
||||
|
||||
5. 那么按照 TDD 我们先设计测试用例。假设有个 Person 类,有个对象方法叫做吃饭,吃完饭后会返回一个“好饱啊”的字符串。那测试用例就是
|
||||
|
||||
| 步骤 | 期望 | 结果 |
|
||||
| --------------------------------------- | ------------------ | ---- |
|
||||
|
||||
| 步骤 | 期望 | 结果 |
|
||||
| -------------------------- | ---------- | --- |
|
||||
| 实例化 Person 对象,调用对象的 eat 方法 | 调用后返回“好饱啊” | ? |
|
||||
|
||||
6. 实现测试用例代码。创建继承自 Unit Test Case class 的测试类,命名为 `工程前缀+测试类名+Test`,也就是 `TDDPersonTest.m`。
|
||||
|
||||
|
||||

|
||||
|
||||
7. 因为要测试 Person 类,所以在主工程中创建 Person 类
|
||||
|
||||
8. 因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在 `TDDPersonTest.m` 中创建一个测试函数 `-(void)testReturnStatusStringWhenPersonAte;`函数内容如下
|
||||
|
||||
|
||||
```objective-c
|
||||
- (void)testReturnStatusStringWhenPersonAte
|
||||
{
|
||||
// Given
|
||||
Person *somebody = [[Person alloc] init];
|
||||
|
||||
|
||||
// When
|
||||
NSString *statusMessage = [somebody performSelector:@selector(eat)];
|
||||
|
||||
|
||||
// Then
|
||||
XCTAssert([statusMessage isEqualToString:@"好饱啊"], @"Person 「吃饭后返回“好饱啊”」功能异常");
|
||||
}
|
||||
@@ -307,7 +274,7 @@ TDD 开发过程类似下图:
|
||||
9. Xcode 下按快捷键 `Command + U`,跑测试代码发现是失败的。因为我们的 Person 类根本没实现相应的方法
|
||||
|
||||
10. 从 [TDD 开发过程](#TDDStructure)可以看到,我们现在是红色的 “Fail” 状态。所以需要去 Person 类中实现功能代码。Person 类如下
|
||||
|
||||
|
||||
```objective-c
|
||||
#import "Person.h"
|
||||
|
||||
@@ -328,8 +295,6 @@ TDD 开发过程类似下图:
|
||||
|
||||
13. 假如 eat 方法实现的不够漂亮。现在在测试用例的保证下,大胆重构,最后确保所有的 Unit Test case 通过即可。
|
||||
|
||||
|
||||
|
||||
### 2. BDD
|
||||
|
||||
相比 TDD,BDD 关注的是行为方式的设计,拿上述“人吃饭”举例说明。
|
||||
@@ -341,7 +306,7 @@ TDD 开发过程类似下图:
|
||||
6. BDD 需要引入好用的框架 `Kiwi`,使用 Pod 的方式引入
|
||||
|
||||
7. 因为要测试人类在吃饭后说一句“好饱啊”。所以设想那个类目前只有一个吃饭的方法。于是在 `TDDPersonTest.m` 中创建一个测试函数 `-(void)testReturnStatusStringWhenPersonAte;`函数内容如下
|
||||
|
||||
|
||||
```objective-c
|
||||
#import "kiwi.h"
|
||||
#import "Person.h"
|
||||
@@ -351,7 +316,7 @@ TDD 开发过程类似下图:
|
||||
describe(@"Person", ^{
|
||||
context(@"when someone ate", ^{
|
||||
it(@"should get a string",^{
|
||||
Person *someone = [[Person alloc] init];
|
||||
Person *someone = [[Person alloc] init];
|
||||
NSString *statusMessage = [someone eat];
|
||||
[[statusMessage shouldNot] beNil];
|
||||
[[statusMessage should] equal:@"好饱啊"];
|
||||
@@ -362,8 +327,6 @@ TDD 开发过程类似下图:
|
||||
SPEC_END
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3. XCTest
|
||||
|
||||
**开发步骤**
|
||||
@@ -386,8 +349,6 @@ Xcode 自带的测试系统是 `XCTest`,使用简单。开发步骤如下
|
||||
|
||||
- 在 `- (void)tearDown` 方法里面写一些释放掉资源或者关闭的代码。比如测试数据库功能的时候,写一些数据库连接池关闭的代码
|
||||
|
||||
|
||||
|
||||
**断言相关宏**
|
||||
|
||||
```c++
|
||||
@@ -607,12 +568,10 @@ Xcode 自带的测试系统是 `XCTest`,使用简单。开发步骤如下
|
||||
_XCTPrimitiveAssertNoThrowSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)
|
||||
```
|
||||
|
||||
|
||||
|
||||
**经验小结**
|
||||
|
||||
1. XCTestCase 类和其他类一样,你可以定义基类,这里面封装一些常用的方法。
|
||||
|
||||
|
||||
```
|
||||
// HCTTestCase.h
|
||||
#import <XCTest/XCTest.h>
|
||||
@@ -622,100 +581,98 @@ Xcode 自带的测试系统是 `XCTest`,使用简单。开发步骤如下
|
||||
@interface HCTTestCase : XCTestCase
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval networkTimeout;
|
||||
|
||||
```
|
||||
|
||||
/**
|
||||
用一个默认时间设置异步测试 XCTestExpectation 的超时处理
|
||||
*/
|
||||
- (void)waitForExpectationsWithCommonTimeout;
|
||||
|
||||
/**
|
||||
用一个默认时间设置异步测试的
|
||||
|
||||
@param handler 超时的处理逻辑
|
||||
*/
|
||||
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler;
|
||||
|
||||
|
||||
- (void)waitForExpectationsWithCommonTimeout;
|
||||
|
||||
/**
|
||||
用一个默认时间设置异步测试的
|
||||
|
||||
@param handler 超时的处理逻辑
|
||||
*/
|
||||
|
||||
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler;
|
||||
|
||||
/**
|
||||
生成 Crash 类型的 meta 数据
|
||||
|
||||
|
||||
@return meta 类型的字典
|
||||
*/
|
||||
- (NSDictionary *)generateCrashMetaDataFromReport;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
// HCTTestCase.m
|
||||
#import "HCTTestCase.h"
|
||||
#import ...
|
||||
|
||||
@implementation HCTTestCase
|
||||
|
||||
#pragma mark - life cycle
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
self.networkTimeout = 20.0;
|
||||
// 1. 设置平台信息
|
||||
[self setupAppProfile];
|
||||
// 2. 设置 Mget 配置
|
||||
[[TITrinityInitManager sharedInstance] setup];
|
||||
// ....
|
||||
// 3. 设置 HermesClient
|
||||
[[HermesClient sharedInstance] setup];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - public Method
|
||||
|
||||
- (void)waitForExpectationsWithCommonTimeout
|
||||
{
|
||||
[self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
|
||||
}
|
||||
|
||||
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler
|
||||
{
|
||||
[self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
|
||||
}
|
||||
|
||||
|
||||
- (NSDictionary *)generateCrashMetaDataFromReport
|
||||
{
|
||||
NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary];
|
||||
NSDate *crashTime = [NSDate date];
|
||||
metaDictionary[@"MONITOR_TYPE"] = @"appCrash";
|
||||
// ...
|
||||
metaDictionary[@"USER_CRASH_DATE"] = @([crashTime timeIntervalSince1970] * 1000);
|
||||
return [metaDictionary copy];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (void)setupAppProfile
|
||||
{
|
||||
[[CMAppProfile sharedInstance] setMPlatform:@"70"];
|
||||
// ...
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
- (NSDictionary *)generateCrashMetaDataFromReport;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
// HCTTestCase.m
|
||||
#import "HCTTestCase.h"
|
||||
#import ...
|
||||
|
||||
@implementation HCTTestCase
|
||||
|
||||
#pragma mark - life cycle
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
self.networkTimeout = 20.0;
|
||||
// 1. 设置平台信息
|
||||
[self setupAppProfile];
|
||||
// 2. 设置 Mget 配置
|
||||
[[TITrinityInitManager sharedInstance] setup];
|
||||
// ....
|
||||
// 3. 设置 HermesClient
|
||||
[[HermesClient sharedInstance] setup];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
#pragma mark - public Method
|
||||
|
||||
- (void)waitForExpectationsWithCommonTimeout
|
||||
{
|
||||
[self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
|
||||
}
|
||||
|
||||
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler
|
||||
{
|
||||
[self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
|
||||
}
|
||||
|
||||
- (NSDictionary *)generateCrashMetaDataFromReport
|
||||
{
|
||||
NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary];
|
||||
NSDate *crashTime = [NSDate date];
|
||||
metaDictionary[@"MONITOR_TYPE"] = @"appCrash";
|
||||
// ...
|
||||
metaDictionary[@"USER_CRASH_DATE"] = @([crashTime timeIntervalSince1970] * 1000);
|
||||
return [metaDictionary copy];
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (void)setupAppProfile
|
||||
{
|
||||
[[CMAppProfile sharedInstance] setMPlatform:@"70"];
|
||||
// ...
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
2. 上述说的基本是开发规范相关。测试方法内部如果调用了其他类的方法,则在测试方法内部必须 Mock 一个外部对象,限制好返回值等。
|
||||
|
||||
3. 在 XCTest 内难以使用 mock 或 stub,这些是测试中非常常见且重要的功能
|
||||
|
||||
|
||||
|
||||
**例子**
|
||||
|
||||
这里举个例子,是测试一个数据库操作类 `HCTDatabase`,代码只放某个方法的测试代码。
|
||||
@@ -729,7 +686,7 @@ Xcode 自带的测试系统是 `XCTest`,使用简单。开发步骤如下
|
||||
// 2. 再插入一批数据
|
||||
NSMutableArray *insertModels = [NSMutableArray array];
|
||||
NSMutableArray *reportIDS = [NSMutableArray array];
|
||||
|
||||
|
||||
for (NSInteger index = 1; index <= 100; index++) {
|
||||
HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
|
||||
model.log_id = index;
|
||||
@@ -740,37 +697,33 @@ Xcode 自带的测试系统是 `XCTest`,使用简单。开发步骤如下
|
||||
[insertModels addObject:model];
|
||||
}
|
||||
[dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
|
||||
|
||||
|
||||
// 3. 将早期的数据删除掉(id > 90 && id <= 100)
|
||||
[dbInstance removeLatestRecordsByCount:10 inTableType:HCTLogTableTypeMeta];
|
||||
|
||||
|
||||
// 4. 拿到当前的前10条数据和之前存起来的前10条 id 做比较。再判断当前表中的总记录条数是否等于 90
|
||||
[dbInstance getLatestRecoreds:10 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
|
||||
NSArray<HCTLogModel *> *latestRTentRecords = records;
|
||||
|
||||
|
||||
[dbInstance getOldestRecoreds:100 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
|
||||
NSArray<HCTLogModel *> *currentRecords = records;
|
||||
|
||||
|
||||
__block BOOL isEarlyData = NO;
|
||||
[latestRTentRecords enumerateObjectsUsingBlock:^(HCTLogModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
if ([reportIDS containsObject:obj.report_id]) {
|
||||
isEarlyData = YES;
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
XCTAssert(!isEarlyData && currentRecords.count == 90, @"***Database「删除最新n条数据」功能:异常");
|
||||
[exception fulfill];
|
||||
}];
|
||||
|
||||
|
||||
}];
|
||||
[self waitForExpectationsWithCommonTimeout];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 3. 测试框架
|
||||
|
||||
#### 1. Kiwi
|
||||
@@ -791,7 +744,6 @@ BDD 框架里的 Kiwi 可圈可点。使用 CocoaPods 引入 `pod 'Kiwi'`。看
|
||||
+(NSArray *)trustList;
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
测试类
|
||||
@@ -799,7 +751,7 @@ BDD 框架里的 Kiwi 可圈可点。使用 CocoaPods 引入 `pod 'Kiwi'`。看
|
||||
```objective-c
|
||||
SPEC_BEGIN(TPKTrustListHelperTest)
|
||||
describe(@"Middleware Wrapper", ^{
|
||||
|
||||
|
||||
context(@"when get trustlist", ^{
|
||||
it(@"should get a array of string",^{
|
||||
NSArray *array = [TPKTrustListHelper trustList];
|
||||
@@ -809,7 +761,7 @@ describe(@"Middleware Wrapper", ^{
|
||||
[[NSStringFromClass([first class]) should] equal:@"__NSCFString"];
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
context(@"when check a string wether contained in trustlist ", ^{
|
||||
it(@"first string should contained in trustlist",^{
|
||||
NSArray *array = [TPKTrustListHelper trustList];
|
||||
@@ -838,7 +790,7 @@ XCTestExpectation *exception = [self expectationWithDescription:@"测试数据
|
||||
for (NSInteger index = 1; index <= 10000; index++) {
|
||||
HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
|
||||
model.log_id = index;
|
||||
// 。。。
|
||||
// 。。。
|
||||
[insertModels addObject:model];
|
||||
}
|
||||
[dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
|
||||
@@ -849,10 +801,6 @@ XCTestExpectation *exception = [self expectationWithDescription:@"测试数据
|
||||
[self waitForExpectationsWithCommonTimeout];
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### 2. expecta、Specta
|
||||
|
||||
expecta 和 Specta 都出自 [orta](https://github.com/orta) 之手,他也是 Cocoapods 的开发者之一。太牛逼了,工程化、质量保证领域的大佬。
|
||||
@@ -864,8 +812,6 @@ Specta 是一个轻量级的 BDD 测试框架,采用 DSL 模式,让测试更
|
||||
- 易于集成到项目中。在 Xcode 中勾选 `Include Unit Tests` ,和 XCTest 搭配使用
|
||||
- 语法很规范,对比 Kiwi 和 Specta 的文档,发现很多东西都是相同的,也就是很规范,所以学习成本低、后期迁移到其他框架很平滑。
|
||||
|
||||
|
||||
|
||||
Expecta 是一个匹配(断言)框架,相比 Xcode 的断言 XCAssert,Excepta 提供更加丰富的断言。
|
||||
|
||||
特点:
|
||||
@@ -875,8 +821,6 @@ Expecta 是一个匹配(断言)框架,相比 Xcode 的断言 XCAssert,Ex
|
||||
- 反向匹配,很灵活。断言匹配用 `except(...).to.equal(...)`,断言不匹配则使用 `.notTo` 或者 `.toNot`
|
||||
- 延时匹配,可以在链式表达式后加入 `.will`、`.willNot`、`.after(interval)` 等
|
||||
|
||||
|
||||
|
||||
### 4. 小结
|
||||
|
||||
Xcode 自带的 `XCTestCase` 比较适合 TDD,不影响源代码,系统独立且不影响 App 包大小。适合简单场景下的测试。且每个函数在最左侧又个测试按钮,点击后可以单独测试某个函数。
|
||||
@@ -889,8 +833,6 @@ Excepta 是一个匹配框架,比 XCTest 的断言则更加全面一些。
|
||||
|
||||
没办法说哪个最好、最合理,根据项目需求选择合适的组合。
|
||||
|
||||
|
||||
|
||||
## 五、网络测试
|
||||
|
||||
我们在测试某个方法的时候可能会遇到方法内部调用了网络通信能力,网络请求成功,可能刷新 UI 或者给出一些成功的提示;网络失败或者网络不可用则给出一些失败的提示。所以需要对网络通信去看进行模拟。
|
||||
@@ -901,8 +843,6 @@ iOS 中很多网络都是基于 NSURL 系统下的类实现的。所以我们可
|
||||
|
||||
> Stub your network requests easily! Test your apps with fake network data and custom response time, response code and headers!
|
||||
|
||||
|
||||
|
||||
几个主要类及其功能:`HTTPStubsProtocol` 拦截网络请求;`HTTPStubs` 单例管理 `HTTPStubsDescriptor` 实例对象;`HTTPStubsResponse` 伪造 HTTP 请求。
|
||||
|
||||
`HTTPStubsProtocol` 继承自 `NSURLProtocol`,可以在 HTTP 请求发送之前对 request 进行过滤处理
|
||||
@@ -947,7 +887,7 @@ describe(@"routerTests", ^{
|
||||
pagePath = [routerManager filePathOfUrl:@"http://***/resource1"];
|
||||
});
|
||||
[[expectFutureValue(pagePath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil];
|
||||
|
||||
|
||||
__block NSString *rescPath = nil;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
rescPath = [routerManager filePathOfUrl:@"http://***/resource1"];
|
||||
@@ -978,8 +918,6 @@ SPEC_END
|
||||
|
||||
😂 插一嘴,我贴的代码已经好几次可以看到不同的测试框架组合了,所以不是说选了框架 A 就完事,根据场景选择最优解。
|
||||
|
||||
|
||||
|
||||
## 六、UI 测试
|
||||
|
||||
上面文章大篇幅的讲了单元测试相关的话题,单元测试十分适合代码质量、逻辑、网络等内容的测试,但是针对最终产物 App 来说单元测试就不太适合了,如果测试 UI 界面的正确性、功能是否正确显然就不太适合了。Apple 在 Xcode 7 开始推出的 `UI Testing` 就是苹果自己的 UI 测试框架。
|
||||
@@ -1005,7 +943,7 @@ Accessibility 通过对 UI 元素进行分类和标记。分类成类似按钮
|
||||
- `XCUIApplication launch` 来启动测试。`XCUIApplication` 是 UIApplication 在测试进程中的代理,用来和 App 进行一些交互。
|
||||
|
||||
- 使用 `staticTexts`来获取当前屏幕上的静态文本(UILabel)元素的代理。等价于 `[app descendantsMatchingType:XCUIElementTypeStaticText]`。XCUIElementTypeStaticText 参数是枚举类型。
|
||||
|
||||
|
||||
```objective-c
|
||||
typedef NS_ENUM(NSUInteger, XCUIElementType) {
|
||||
XCUIElementTypeAny = 0,
|
||||
@@ -1095,32 +1033,25 @@ Accessibility 通过对 UI 元素进行分类和标记。分类成类似按钮
|
||||
```
|
||||
|
||||
- 通过 `XCUIApplication` 实例化对象调用 `descendantsMatchingType:` 方法得到的是 `XCUIElementQuery` 类型。比如 `@property (readonly, copy*) XCUIElementQuery *staticTexts;`
|
||||
|
||||
|
||||
```objective-c
|
||||
/*! Returns a query for all descendants of the element matching the specified type. */
|
||||
- (XCUIElementQuery *)descendantsMatchingType:(XCUIElementType)type;
|
||||
```
|
||||
|
||||
- `descendantsMatchingType` 返回所有后代的类型匹配对象。`childrenMatchingType` 返回当前层级子元素的类型匹配对象
|
||||
|
||||
|
||||
```objective-c
|
||||
/*! Returns a query for direct children of the element matching the specified type. */
|
||||
- (XCUIElementQuery *)childrenMatchingType:(XCUIElementType)type;
|
||||
|
||||
```
|
||||
|
||||
- 拿到 `XCUIElementQuery` 后不能直接拿到 `XCUIElement`。和 `XCUIApplication` 类似,`XCUIElement` 不能直接访问 UI 元素,它是 UI 元素在测试框架中的代理。可以通过 `Accessibility` 中的 `frame`、`identifier` 来获取。
|
||||
|
||||
|
||||
|
||||
对比很多自动化测试框架都需要找出 UI 元素,也就是借助于 `Accessibility` 的 `identifier`。这里的唯一标识生成对比[为 UIAutomation 添加自动化测试标签的探索](http://yulingtianxia.com/blog/2016/03/28/Add-UITest-Label-for-UIAutomation/)]
|
||||
|
||||
|
||||
|
||||
第三方 UI 自动化测试框架挺多的,可以查看下典型的 [appium](https://github.com/appium/appium)、[macaca](https://github.com/alibaba/macaca)。
|
||||
|
||||
|
||||
|
||||
## 七、 测试经验总结
|
||||
|
||||
TDD 写好测试再写业务代码,BDD 先写实现代码,再写基于行为的测试代码。另一种思路是没必要针对每个类的私有方法或者每个方法进行测试,因为等全部功能做完后针对每个类的接口测试,一般会覆盖据大多数的方法。等测试完看如果方法未被覆盖,则针对性的补充 `Unit Test`。
|
||||
@@ -1139,15 +1070,6 @@ UITesting 还是建议在核心逻辑且长时间没有改动的情况下去做
|
||||
|
||||
WWDC 这张图也很清楚,UI 其实需要的占比较小,还是要靠单测驱动。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [维基百科:测试驱动开发](https://zh.wikipedia.org/wiki/测试驱动开发)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,25 +26,37 @@ malloc_size((__bridge const void *)(p)) // 24 isa占8字节 + _name 指针占
|
||||
class_getInstanceSize(p.class) // 32 ,系统内存对齐
|
||||
```
|
||||
|
||||
为什么内存对齐?以空间换时间。系统以16字节对齐。
|
||||
为什么系统是由16字节对齐的?
|
||||
|
||||
x /6gx p.class
|
||||
成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
|
||||
|
||||
类对象有且仅有1个。
|
||||
|
||||
p.class
|
||||
|
||||
class_getClass("Person")
|
||||
|
||||
[Person class]
|
||||
|
||||
p/x (class_data_bits_t *)地址
|
||||
|
||||
## class_rw_t、class_ro_t 区别?
|
||||
## class_rw_t、class_ro_t、class_rw_ext_t 区别?
|
||||
|
||||
class_ro_t 在编译时期生成的,class_rw_t 是在运行时期生成的。
|
||||
|
||||
拷贝带来的问题?当开发者通过 runtime第一次 动态修改类的信息的时候,Apple 会生成 rwe。搜索 class_rw_ext_t
|
||||
那么什么是 `class_rw_ext_t`?首先明确2个概念
|
||||
|
||||
- clean memory:加载后不会被修改。当系统内存紧张时,可以从内存中移除,需要时可以再次加载
|
||||
|
||||
- dirty memory:加载后会被修改,一直处于内存中
|
||||
|
||||
Runtime 初始化的时候,遇到一个类,则会利用类的 `class_ro_t` 中的基础信息(methods、properties、protocols)来创建 `class_rw_t` 对象。`class_rw_t` 设计的目的就是为了 Runtime 所需(Category 增加属性、协议、动态增加方法等),但是实际上那么多类大多数情况只有少部分类才需要 Runtime 能力。所以 Apple 为了内存优化,在 iOS 14 对 `class_rw_t` 拆分出 `class_rw_ext_t`,用来存储 Methods、Protocols、Properties 信息,会在使用的时候才创建,节省更多内存。
|
||||
|
||||
比如访问 method 的过程
|
||||
|
||||
```objectivec
|
||||
// 新版
|
||||
const method_array_t methods() const {
|
||||
auto v = get_ro_or_rwe();
|
||||
if (v.is<class_rw_ext_t *>()) {
|
||||
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
|
||||
} else {
|
||||
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 有类对象、为什么设计元类对象
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# DYLD
|
||||
# DYLD 及 Mach-O
|
||||
|
||||
dynamic loader,动态加载器。在 MacOS/iOS 中,使用 `/usr/lib/dyld` 程序来加载动态库的。
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
# 从 Flutter 和前端角度出发,聊聊单线程模型下如何保证 UI 流畅性
|
||||
|
||||
|
||||
|
||||
> 文章主题是“单线程模型下如何保证 UI 的流畅性”。该话题针对的是 Flutter 性能原理展开的,但是 dart 语言就是 js 的延伸,很多概念和机制都是一样的。具体不细聊。此外 js 也是单线程模型,在界面展示和 IO 等方面和 dart 类似。所以结合对比讲一下,帮助梳理和类比,更加容易掌握本文的主题,和知识的横向拓展。
|
||||
>
|
||||
>
|
||||
> 先从前端角度出发,分析下 event loop 和事件队列模型。再从 Flutter 层出发聊聊 dart 侧的事件队列和同步异步任务之间的关系。
|
||||
|
||||
|
||||
|
||||
## 一、单线程模型的设计
|
||||
|
||||
### 1. 最基础的单线程处理简单任务
|
||||
@@ -27,7 +23,7 @@ void mainThread () {
|
||||
string name = "姓名:" + "杭城小刘";
|
||||
string birthday = "年龄:" + "1995" + "02" + "20"
|
||||
int age = 2021 - 1995 + 1;
|
||||
printf("个人信息为:%s, %s, 大小:%d", name.c_str(), birthday.c_str(), age);
|
||||
printf("个人信息为:%s, %s, 大小:%d", name.c_str(), birthday.c_str(), age);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -35,8 +31,6 @@ void mainThread () {
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 2. 线程运行过程中来了新的任务怎么处理?
|
||||
|
||||
问题1 介绍的线程模型太简单太理想了,不可能从一开始就 n 个任务就确定了,大多数情况下,会接收到新的 m 个任务。那么 section1 中的设计就无法满足该需求。
|
||||
@@ -69,26 +63,18 @@ void mainThread () {
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 3. 处理来自其他线程的任务
|
||||
|
||||
真实环境中的线程模块远远没有这么简单。比如浏览器环境下,线程可能正在绘制,可能会接收到1个来自用户鼠标点击的事件,1个来自网络加载 css 资源完成的事件等等。第二版线程模型虽然引入了事件循环机制,可以接受新的事件任务,但是发现没?这些任务之来自线程内部,该设计是无法接受来自其他线程的任务的。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
从上图可以看出,渲染主线程会频繁接收到来自于 IO 线程的一些事件任务,当接受到的资源加载完成后的消息,则渲染线程会开始 DOM 解析;当接收到来自鼠标点击的消息,渲染主线程则会执行绑定好的鼠标点击事件脚本(js)来处理事件。
|
||||
|
||||
需要一个合理的数据结构,来存放并获取其他线程发送的消息?
|
||||
|
||||
**消息队列**这个词大家都听过,在 GUI 系统中,事件队列是一个通用解决方案。
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
**消息队列(事件队列)是一种合理的数据结构。要执行的任务添加到队列的尾部,需要执行的任务,从队列的头部取出。**
|
||||
@@ -97,8 +83,6 @@ void mainThread () {
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
可以看出改造分为3个步骤:
|
||||
|
||||
- 构建一个消息队列
|
||||
@@ -122,8 +106,8 @@ TaskQueue taskQueue;
|
||||
void processTask ();
|
||||
void mainThread () {
|
||||
while (true) {
|
||||
Task task = taskQueue.fetchTask();
|
||||
processTask(task);
|
||||
Task task = taskQueue.fetchTask();
|
||||
processTask(task);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -139,16 +123,12 @@ void handleIOTask () {
|
||||
|
||||
Tips: 事件队列是存在多线程访问的情况,所以需要加锁。
|
||||
|
||||
|
||||
|
||||
### 4. 处理来自其他线程的任务
|
||||
|
||||

|
||||
|
||||
浏览器环境中, 渲染进程经常接收到来自其他进程的任务,IO 线程专门用来接收来自其他进程传递来的消息。IPC 专门处理跨进程间的通信。
|
||||
|
||||
|
||||
|
||||
### 5. 消息队列中的任务类型
|
||||
|
||||
消息队列中有很多消息类型。内部消息:如鼠标滚动、点击、移动、宏任务、微任务、文件读写、定时器等等。
|
||||
@@ -157,46 +137,36 @@ Tips: 事件队列是存在多线程访问的情况,所以需要加锁。
|
||||
|
||||
上述事件都是在渲染主线程中执行的,因此编码时需注意,尽量减小这些事件所占用的时长。
|
||||
|
||||
|
||||
|
||||
### 6. 如何安全退出
|
||||
|
||||
Chrome 设计上,确定要退出当前页面时,页面主线程会设置一个退出标志的变量,每次执行完1个任务时,判断该标志。如果设置了,则中断任务,退出线程
|
||||
|
||||
|
||||
|
||||
### 7. 单线程的缺点
|
||||
|
||||
事件队列的特点是先进先出,后进后出。那后进的任务也许会被前面的任务因为执行时间过长而阻塞,等待前面的任务执行完毕才可以执行后面的任务。这样存在2个问题。
|
||||
|
||||
- 如何处理高优先级的任务
|
||||
|
||||
|
||||
假如要监控 DOM 节点的变化情况(插入、删除、修改 innerHTML),然后触发对应的逻辑。最基础的做法就是设计一套监听接口,当 DOM 变化时,渲染引擎同步调用这些接口。不过这样子存在很大的问题,就是 DOM 变化会很频繁。如果每次 DOM 变化都触发对应的 JS 接口,则该任务执行会很长,导致**执行效率**的降低
|
||||
|
||||
|
||||
如果将这些 DOM 变化做为异步消息,假如消息队列中。可能会存在因为前面的任务在执行导致当前的 DOM 消息不会被执行的问题,也就是影响了监控的**实时性**。
|
||||
|
||||
|
||||
如何权衡效率和实时性?**微任务** 就是解决该类问题的。
|
||||
|
||||
|
||||
通常,我们把消息队列中的任务成为**宏任务**,每个宏任务中都包含一个**微任务队列**,在执行宏任务的过程中,假如 DOM 有变化,则该变化会被添加到该宏任务的微任务队列中去,这样子效率问题得以解决。
|
||||
|
||||
|
||||
当宏任务中的主要功能执行完毕欧,渲染引擎会执行微任务队列中的微任务。因此实时性问题得以解决
|
||||
|
||||
|
||||
|
||||
- 如何解决单个任务执行时间过长的问题
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
可以看出,假如 JS 计算超时导致动画 paint 超时,会造成卡顿。浏览器为避免该问题,采用 callback 回调的设计来规避,也就是让 JS 任务延后执行。
|
||||
|
||||
## 二、 flutter 里的单线程模型
|
||||
|
||||
|
||||
|
||||
### 1. event loop 机制
|
||||
|
||||
|
||||
|
||||
Dart 是单线程的,也就是代码会有序执行。此外 Dart 作为 Flutter 这一 GUI 框架的开发语言,必然支持异步。
|
||||
|
||||
一个 Flutter 应用包含一个或多个 **isolate**,默认方法的执行都是在 **main isolate** 中;**一个 isolate 包含1个 Event loop 和1个 Task queue。其中,Task queue 包含1个 Event queue 事件队列和1个 MicroTask queue 微任务队列**。如下:
|
||||
@@ -205,8 +175,6 @@ Dart 是单线程的,也就是代码会有序执行。此外 Dart 作为 Flutt
|
||||
|
||||
为什么需要异步?因为大多数场景下 应用都并不是一直在做运算。比如一边等待用户的输入,输入后再去参与运算。这就是一个 IO 的场景。所以单线程可以再等待的时候做其他事情,而当真正需要处理运算的时候,再去处理。因此虽是单线程,但是给我们的感受是同事在做很多事情(空闲的时候去做其他事情)
|
||||
|
||||
|
||||
|
||||
某个任务涉及 IO 或者异步,则主线程会先去做其他需要运算的事情,这个动作是靠 event loop 驱动的。和 JS 一样,dart 中存储事件任务的角色是事件队列 event queue。
|
||||
|
||||
Event queue 负责存储需要执行的任务事件,比如 DB 的读取。
|
||||
@@ -225,8 +193,6 @@ Event loop 不断的轮询,先判断微任务队列是否为空,从队列头
|
||||
|
||||
所以,一般需求下,异步任务我们使用优先级较低的 Event Queue。比如 IO、绘制、定时器等,都是通过事件队列驱动主线程来执行的。
|
||||
|
||||
|
||||
|
||||
Dart 为 Event Queue 的任务提供了一层封装,叫做 Future。把一个函数体放入 Future 中,就完成了同步任务到异步任务的包装(类似于 iOS 中通过 GCD 将一个任务以同步、异步提交给某个队列)。Future 具备链式调用的能力,可以在异步执行完毕后执行其他任务(函数)。
|
||||
|
||||
看一段具体代码:
|
||||
@@ -325,10 +291,6 @@ sub subTask 2 Future 4
|
||||
- 接着,执行 Task1 Future 5。本次事件循环结束
|
||||
- 等下一轮事件循环到来,打印队列中的 sub subTask 1 Future 4、sub subTask 1 Future 5.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 3. 异步函数
|
||||
|
||||
异步函数的结果在将来某个时刻才返回,所以需要返回一个 Future 对象,供调用者使用。调用者根据需求,判断是在 Future 对象上注册一个 then 等 Future 执行体结束后再进行异步处理,还是同步等到 Future 执行结束。Future 对象如果需要同步等待,则需要在调用处添加 **await**,且 Future 所在的函数需要使用 **async** 关键字。
|
||||
@@ -356,14 +318,10 @@ subTask 2 Future 2
|
||||
- Future 中的 Task1 Future 1 被添加到 Event Queue 中。其次遇到第一个 then,then 里面是 Future 包装的异步任务,所以 `Future(() => print("subTask 1 Future 2"))` 被添加到 Event Queue 中,所在的 await 函数也被添加到了 Event Queue 中。第二个 then 也被添加到 Event Queue 中
|
||||
- 第二个 Future 中的 'Task1 Future 2 不会被 await 阻塞,因为 await 是异步等待(添加到 Event Queue)。所以执行 'Task1 Future 2。随后执行 "subTask 1 Future 2,接着取出 await 执行 subTask 2 Future 2
|
||||
|
||||
|
||||
|
||||
### 4. Isolate
|
||||
|
||||
Dart 为了利用多核 CPU,将 CPU 层面的密集型计算进行了隔离设计,提供了多线程机制,即 Isolate。每个 Isolate 资源隔离,都有自己的 Event Loop 和 Event Queue、Microtask Queue。Isolate 之间的资源共享通过消息机制通信(和进程一样)
|
||||
|
||||
|
||||
|
||||
使用很简单,创建时需要传递一个参数。
|
||||
|
||||
```dart
|
||||
@@ -393,7 +351,7 @@ void main() {
|
||||
testIsolate() async {
|
||||
ReceivePort receivePort = ReceivePort(); // 创建管道
|
||||
Isolate isolate = await Isolate.spawn(coding, receivePort.sendPort); // 创建 Isolate,并传递发送管道作为参数
|
||||
// 监听消息
|
||||
// 监听消息
|
||||
receivePort.listen((message) {
|
||||
print("data: $message");
|
||||
receivePort.close();
|
||||
@@ -405,8 +363,6 @@ lbp@MBP ~/Desktop dart index.dart
|
||||
data: 3
|
||||
```
|
||||
|
||||
|
||||
|
||||
此外 Flutter 中提供了执行并发计算任务的快捷方式-**compute 函数**。其内部对 Isolate 的创建和双向通信进行了封装。
|
||||
|
||||
实际上,业务开发中使用 compute 的场景很少,比如 JSON 的编解码可以用 compute。
|
||||
@@ -430,46 +386,3 @@ int syncCalcuateFactorial(upperBounds) => upperBounds < 2
|
||||
- Isolate 是 Dart 中的多线程,可以实现并发,有自己的事件循环与 Queue,独占资源。Isolate 之间可以通过消息机制进行单向通信,这些传递的消息通过对方的事件循环驱动对方进行异步处理。
|
||||
- flutter 提供了 CPU 密集运算的 compute 方法,内部封装了 Isolate 和 Isolate 之间的通信
|
||||
- 事件队列、事件循环的概念在 GUI 系统中非常重要,几乎在前端、Flutter、iOS、Android 甚至是 NodeJS 中都存在。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
第一部分主要介绍 iOS 开发中遇到的问题或者有趣的知识
|
||||
|
||||
* [1、工程大小优化之图片资源](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.1.md)
|
||||
* [2、看透构造方法](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.2.md)
|
||||
://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.2.md)
|
||||
* [3、控制器加载的玄机](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.3.md)
|
||||
* [4、如何优雅地调试手机网页?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.4.md)
|
||||
* [5、事件响应者链](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.5.md)
|
||||
@@ -94,7 +94,7 @@
|
||||
* [88、fishhook 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.88.md)
|
||||
* [89、block 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)
|
||||
* [90、YYImage 框架原理,探索图片高效加载原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.90.md)
|
||||
* [91、DYLD](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md)
|
||||
* [91、DYLD 及 Mach-O](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md)
|
||||
* [92、flutter 无痕埋点](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.92.md)
|
||||
* [93、flutter 新功能引导](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.93.md)
|
||||
* [94、APM-Wake Up](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.94.md)
|
||||
@@ -109,4 +109,7 @@
|
||||
* [103、设计模式及其场景](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.103.md)
|
||||
* [104、NSNotification底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.104.md)
|
||||
* [105、iOS 界面渲染流程](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.105.md)
|
||||
* [106、NSUserDefault 底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.106.md)
|
||||
* [106、NSUserDefault 底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.106.md)
|
||||
* [107、IM技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.107.md)
|
||||
* [108、精准测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md)
|
||||
* [109、汇编学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.109.md)
|
||||
@@ -2,6 +2,147 @@
|
||||
|
||||
## TCP/UDP
|
||||
|
||||
TCP 传输的核心公式:速度 = 窗口大小/往返时间,这个公式对于理解传输本质和排查传输问题具有很强的知道意义。
|
||||
|
||||
TCP 里面有三种窗口:发送窗口、接收窗口、拥塞窗口。如果没有特别说明,TCP Window 指的是接收窗口。
|
||||
|
||||
TCP Window Full:指的是在途数据的大小等于接收窗口大小时,窗口会“满”
|
||||
|
||||
Wireshark 分析得到的信息,都会用方括号包起来,TCP 报文本身的信息,没有方括号。
|
||||
|
||||
|
||||
|
||||
### TCP 如何探测到拥塞
|
||||
|
||||
TCP 传输的起始阶段,速度都是从低到高升上来的,很少一上来就以最终速度运行的情况。
|
||||
|
||||
这个机制其实就是 TCP 的拥塞控制。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### TCP 拥塞控制
|
||||
|
||||
TCP 使用拥塞机制来确保传输速度和稳定性。拥塞机制是通信双方自己要实现的功能,在途中的网络设备(交换机、路由器)转发,不关心拥塞机制。
|
||||
|
||||
拥塞机制包括:慢启动、拥塞避免、快速重传、快速回复。
|
||||
|
||||
#### 慢启动
|
||||
|
||||
Slow start,即 TCP 传输的开始阶段是从一个相对低的速度开始的。之后拥塞窗口会翻倍方式增长。每次 TCP 收到一个确认了数据的 ACK,拥塞窗口就增加1个 MSS
|
||||
|
||||

|
||||
|
||||
当收到重复的 ACK 报文(即确认号一样),比如收到2个 ACK,但他们的确认号一样,那么第二个 ACK 就不算是“确认数据的 ACK”,拥塞窗口就不会增加2个 MSS,只会增加1个 MSS。
|
||||
|
||||
会持续增长吗?当然不是,有终止条件:
|
||||
|
||||
- 遇到了拥塞
|
||||
|
||||
- 拥塞窗口增长到慢启动阈值
|
||||
|
||||
注意:慢启动阶段,并不是“每过1RTT 就翻倍”,也可能会比翻倍少一些。什么意思呢?慢启动阶段,TCP 每收到一个 ACK,拥塞窗口就增加1MSS。假设初始拥塞窗口大小为2MSS,发送2个数据报之后:
|
||||
|
||||
- 收到1个ACK(间隔确认),那么在1RTT内,拥塞窗口从2变为3,没有翻倍
|
||||
|
||||
- 收到2个ACK,那么在1RTT内,拥塞窗口从2变为4,翻倍了
|
||||
|
||||
#### 慢启动阈值
|
||||
|
||||
**慢启动阈值** ssthresh,过了这个阈值,拥塞窗口的增长速度就立刻变慢了,变为每过一个 RTT,拥塞窗口就增加一个 MSS(之前是没收到一个确认数据的 ACK 就增加1个 MSS)
|
||||
|
||||

|
||||
|
||||
上图所示,假设 ICW 是4个 MSS,ssthresh 是32个 MSS,慢启动阶段经过1个 RTT 后,CW 扩大为8MSS、16MSS、32MSS。等到了阈值之后,TCP就进入拥塞避免阶段了。每过一个 RTT,拥塞窗口只增加1MSS,曲线就变为较为斜率较低的直线了。
|
||||
|
||||

|
||||
|
||||
QA:如果拥塞窗口大小正好等于慢启动阈值,那么发送方这时候是需要采用拥塞避免过程(线性增长)还是继续选择慢启动过程(指数增长)?[RFC5681](https://datatracker.ietf.org/doc/html/rfc5681) 规定是说两者都可以。
|
||||
|
||||
#### 间隔确认
|
||||
|
||||
很多 TCP 的实现中,如果收到连续多个报文,确认报文是间隔一个进行回复的。
|
||||
|
||||
比如:发送方发送1、2,接收方收到1、2,此时针对2进行确认。发送方发送3、4,接收方针对4进行确认。这样的机制下,会使得拥塞窗口的增长速度,比每次 ACK 更低一些(更稳)
|
||||
|
||||
#### 拥塞窗口
|
||||
|
||||
Congestion Window,拥塞窗口,简写 CW。拥塞窗口是针对每个连接进行维护的,比如某个主机有3个 TCP 连接在传输数据,那么这3个连接就各自维护自己的拥塞窗口。
|
||||
|
||||
**初始化拥塞窗口**,Initial Congestion Window,简写为 ICW(IW)。
|
||||
|
||||
在 Linux 内核3.0以前,ICW 的比较小,在2到4个MSS,2010年谷歌提出,为了充分利用现代互联网的传输能力,Linux 应该把 ICW 从2~4个MSS提升到10MSS。这也被应用到 Linux 内核3.0及其以后的版本中规定了 `TCP_INIT_CWND` 为10.
|
||||
|
||||
```c
|
||||
#define TCP_INIT_CWND 10
|
||||
```
|
||||
|
||||
这个值在慢启动阶段影响了传输速度,慢启动阶段每经过1个RTT,拥塞窗口就翻倍,所以不同的 ICW 就会造成不同的传输速度。
|
||||
|
||||
| ICW | 1RTT后 | 2RTT后 | 3RTT后 |
|
||||
| --- | ----- | ----- | ----- |
|
||||
| 2 | 4 | 8 | 16 |
|
||||
| 10 | 20 | 40 | 80 |
|
||||
|
||||
#### 拥塞避免
|
||||
|
||||
TCP 野蛮生长后当达到慢启动阈值之后,会进入拥塞避免阶段。这个阶段的特点是“和性增长乘性降低”(Addictive increase/multiplicative decrease,AIMD),解释下就是拥塞避免阶段每个RTT时间,拥塞窗口只增长1MSS,这个阶段的拥塞窗口增长是线性的(斜率比较低的直线),当探测到拥塞时,拥塞窗口就要往下降,下降是直接减半的,叫做乘性降低。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
#### 窗口和 MSS 的关系
|
||||
|
||||
窗口一般比MSS大,MSS 是有确定上限的,一般为1460,当然实际情况下值可能更低。
|
||||
|
||||
窗口一般是n个MSS。
|
||||
|
||||
|
||||
|
||||
#### 快速重传
|
||||
|
||||
TCP 每发送一个报文,就会启动一个超时计时器,若在限定时间内没有收到这个报文的确认,则发送方认为这个报文在网络上丢了,需要发送方重传这个报文,这个机制叫做“超时重传”。
|
||||
|
||||
TCP 最小超时重传时间为200ms,在这个机制下虽然解决了丢包问题,但带来一个新的问题:某个包都已经丢失了,还需要等待200ms或者更长时间,那体验不就更糟糕了吗?
|
||||
|
||||
TCP 会利用另一种机制来解决超时重传带来的时间等待问题,就是快速重传机制,一旦发送方收到3次重复确认(确认 ACK 报文中有确认号)加上第一次确认就一共4次,就不用等待超时计时器了,直接重传这个报文。
|
||||
|
||||
注意:快速重传看的是数据,超时重传看的是时间。
|
||||
|
||||
#### 快速恢复
|
||||
|
||||
快速恢复是 TCP Reno 算法引入的一个阶段,是和“快速重传”搭配工作的。跟之前的“慢启动-拥塞避免-慢启动-拥塞避免”不同的是,当遇到拥塞点之后,通过快速重传,就不再进入慢启动了,而是从这个减半的拥塞窗口开始,保持跟拥塞避免一样的线性增长,直到遇到下一个拥塞点。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
注意:“快速恢复”有几个小细节
|
||||
|
||||
- 快速恢复一定是和快速重传搭配使用的。那有何表现或者为什么这么设计?如果在快速重传的场景下(也就是收到连续3次重复确认 ACK 报文),那么这个特征可以理解为“网络虽然有点问题,但是能收到3次重复 ACK 报文,网络也没那么糟糕”,所以如果走传统的拥塞避免直接从0开始就很慢也很浪费效率,但如果按照拥塞避免直接减半之后再走慢启动,也很浪费网络,所以当遇到一个拥塞点之后,减半就按照拥塞避免(线性增长)。
|
||||
|
||||
|
||||
|
||||
总结:慢启动不只是在 TCP 连接启动的阶段才发生,传输的过程中遇到网络较差也会发生多次慢启动。一旦拥塞避免阶段探测到了拥塞,TCP 还是会回到慢启动过程,只不过这个慢启动阈值跟之前的不同,如果有多次拥塞,会重复这个过程直到传输结束。
|
||||
|
||||
### 总结
|
||||
|
||||
- 慢启动:每收到一个 ACK,拥塞窗口(CW)增加一个 MSS
|
||||
|
||||
- 拥塞避免:策略是“和性增长乘性降低”,每一个 RTT,CW 增加一个 MSS
|
||||
|
||||
- 快速重传:接收到 3 次或者以上的重复确认后,直接重传这个丢失的报文
|
||||
|
||||
- 快速恢复:结合快速重传,在遇到拥塞点后,跳过慢启动阶段,进入线性增长
|
||||
|
||||
拥塞窗口(CW)和接收窗口(RW)是如何决定了传输速度上限的,简单来说:
|
||||
|
||||
- 当 RW > CW 时,速度由 CW 决定
|
||||
|
||||
- 当 RW < CW 时,速度由 RW 决定
|
||||
|
||||
## HTTP 缓存控制
|
||||
|
||||
缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。
|
||||
@@ -455,9 +596,6 @@ TLS 是建立在 TCP 的上层协议,因此要先按照 TCP 的规则来,也
|
||||
|
||||
注意这里不是 TCP Fast Open,TFO 是用来加速连续 TCP 连接的数据交互 TCP 拓展协议。原理如下:TCP 三次握手的过程中,当用户首次访问 Server 时,发送 SYN 包,Server 根据用户 IP 生成 Cookie(已加密),并与 SYN-ACK 一同发回 Client;当 Client 随后重连时,在SYN 包携带 TCP Cookie;如果 Server 校验合法,则在用户回复 ACK 前就可以直接发送数据;否则按照正常三次握手进行。
|
||||
|
||||
|
||||
|
||||
|
||||
第三步:服务器会把证书也发送给客户端(Server Certificate),见下图第一个大红框中的内容。
|
||||
|
||||
同时服务器选择了 ECDHE 算法,所以在发送了服务器证书后马上发送“Server Key Exchange”消息。里面是椭圆曲线的公钥(Server Params),用来实现密钥的交换算法,再加上自己的私钥签名认证(用私钥对椭圆曲线的 public key 做了签名认证生成了 Signature)
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
* [88、fishhook 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.88.md)
|
||||
* [89、block 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)
|
||||
* [90、YYImage 框架原理,探索图片高效加载原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.90.md)
|
||||
* [91、DYLD](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md)
|
||||
* [91、DYLD 及 Mach-O](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md)
|
||||
* [92、flutter 无痕埋点技术方案](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.92.md)
|
||||
* [93、flutter 新功能引导](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.93.md)
|
||||
* [94、APM-Wake Up](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.94.md)
|
||||
@@ -109,6 +109,9 @@
|
||||
* [104、NSNotification底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.104.md)
|
||||
* [105、iOS 界面渲染流程](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.105.md)
|
||||
* [106、NSUserDefault 底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.106.md)
|
||||
* [107、IM技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.107.md)
|
||||
* [108、精准测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md)
|
||||
* [109、汇编学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.109.md)
|
||||
* [Chapter2 - Web FrontEnd](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/chapter2.md)
|
||||
* [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.1.md)
|
||||
* [2、正则表达式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.2.md)
|
||||
|
||||
BIN
assets/AssembleFunctionStack.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
assets/AssembleReturn.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
assets/CSStackFrame.png
Normal file
|
After Width: | Height: | Size: 952 KiB |
BIN
assets/Stack.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/StackFrame.png
Normal file
|
After Width: | Height: | Size: 472 KiB |
BIN
assets/StackPush.png
Normal file
|
After Width: | Height: | Size: 528 KiB |
BIN
assets/StatckFrame.png
Normal file
|
After Width: | Height: | Size: 472 KiB |
BIN
assets/TCP-CongestionAvoidance.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/TCP-CongestionAvoidanceProgress.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
assets/TCP-QuickRecover.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
assets/TCP-SlowStart.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/TCP-SlowStartChart.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/TCPWindowDiff.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
assets/emptyStack.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
assets/stackPop.png
Normal file
|
After Width: | Height: | Size: 459 KiB |