0%

什么是 KCP

我们知道 UDP 是不可靠传输, 而 TCP 是可靠传输,但 TCP 本身也存在一些缺陷,例如:

  1. 连续丢包超时策略,连续丢包的RTO = RTO * 2,这个参数好像无法改动 ;
  2. 丢包重传策略,在没有 SACK 之前,TCP在收到3个相同的 ack 时,才会触发丢包重传策略,并且会将后面所有的包全部重传(有些包其实已经收到了);
  3. 退流控制策略, 为了保证网络传输效率,TCP有发送窗口、接收窗口、慢启动、丢包退让策略,这样使得数据传输在一定程度上被阻塞了;
  4. 延时ack
  5. 。。。。。。

那除了 TCP 以外还有什么方式可以实现可靠性传输呢?就是 KCP ,KCP 本身只是一个算法实现,平台无关,并没有指定传输协议,所以通过 KCP + UDP 就可以实现跟 TCP 一样的可靠性传输;

KCP 协议头

1
2
3
4
5
6
7
8
9
10
11
12
0                 4     5     6          8  (BYTE)
+-----------------+-----+-----+----------+ 0
| conv | cmd | frg | wnd |
+-----------------+-----+-----+----------+ 8
| ts | sn |
+-----------------+----------------------+ 16
| una | len |
+-----------------+----------------------+ 24
| |
| DATA (optional) |
| |
+----------------------------------------+
conv

连接号,用于表示属于哪个连接

Read more »

前言

自苹果禁用热更新以来(实际上就是禁用了 dlsym 等几个接口),使用了 JSpatch 等热更新库的应用也就无法更新了;
那么有没有一种方式可以代替通过 dlsym 实现的热更新呢?

OCRunner & MangoFix

这两个库都可以实现 iOS 的热更新,使用的原理是相同的,都是通过语法分析、词法分析最终生成抽象语法树,再通过解析器解析,这里相当于自己写了一个编译器;而底层方法交换是通过 libffi + runtime 实现的,这篇文章就来简单了解下 libffi 这个库的使用。

libffi

FFI 的全名是 Foreign Function Interface (外部函数接口)
libffi 提供了一套底层接口,在知道函数签名的情况下,可以根据相关接口完成函数调用;

调用惯例(Calling Convention)

函数调用是通过堆栈体现出来的,在调用函数时,需要按照约定将相关的参数入栈,
而这种约定就叫做:调用惯例(Calling Convention)
也就是说只要我们按照这个约定存放函数调用时使用的参数,就可实现函数调用的效果;
libffi 也就是实现了这样的一个功能。

libffi 调用任意 OC 方法

实现步骤:

  1. 通过 libffi 创建 closure 闭包
  2. 交换函数指针;之后调用原始方法,因为 imp 已经修改,最终会调用到闭包中
  3. 在闭包回调函数里面,将 imp 替换成新的,将消息通过 ffi_call 发送出去

换句话说通过 libffi 的闭包功能,再加上 OC 提供给我们的 runtime ,一样也可以实现任意方法的 hook 功能;同时也为热修复提供了基础能力。

Read more »

ARC

ARC (Automatic Reference Counting) 是由编译器跟运行时共同完成的(运行时标记);编译器会在编译时会自动加上 retain、release、autorelease、dealloc 操作。

__autoreleasing

如果一个变量被用关键字修饰 __autoreleasing 修饰,那么变量会立即加入到自动释放池中

ARC规则

  1. 若方法名以下列词语开头,则其返回的对象归调用者所有:
    alloc new copy mutableCopy。归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。

  2. 除了会自动调用“保留”与“释放”方法外,ARC 还可以执行一些手工操作很难甚至无法完成的优化。如果发现在同一个对象上执行多次“保留”与“释放”操作,那么ARC有时可以成对地移除这两个操作。
    一般,在方法中返回自动释放的对象时,要执行一个特殊函数。此时不直接调用对象的 autorelease 方法,而是改为调用 objc_autoreleaseReturnValue 。此函数会检视当前方法返回之后即将要执行的那段代码。若发现那段代码在返回的对象上执行 retain 操作,则设置全局数据结构(此数据结构的具体内容因处理器而异)中的一个标志位而不执行 autorelease 操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行 retain,而是改为执行 objc_retainAutoreleaseReturnValue 函数。此函数要检测刚才提到的那个标志位,若已经置位,则不执行 retain 操作。设置并检测标志位,要比调用 autorelease 和 retain 更快。
    备注:objc_autoreleaseReturnValue 优化不一定开启,会根据不同CPU类型决定
    另外,这个标记位存在哪里呢?关键字:线程局部存储(TLS)

Read more »

简介

Websocket 基于 TCP 的全双工通信协议,属于应用层协议,他必须依赖 HTTP 协议进行一次握手,握手成功后直接通过单个 TCP 传输数据。

特点

  1. 握手阶段使用HTTP连接;
  2. 可以发送文本,也可以发送二进制数据;
  3. 全双工通信;
  4. 协议标识符ws,加密是wss;

解决了什么问题

在没有 Websocket 之前,一般是通过 HTTP 轮询或者长轮询来实现数据推送

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

这两种方式都比较耗资源,而 Websocket 可以很好的解决这类问题

主要使用场景

  1. 股票行情推送
  2. 消息推送
  3. IM聊天

握手流程

通过 HTTP 连接,连接完成后用 TCP 通信

请求头

客户端发起带有 Upgrade 字段的 Get 请求,请求头字段如下:

1
2
3
4
5
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: AAAAAAAA==
Sec-WebSocket-Version: 13
  1. Connection :表示要升级协议
  2. Upgrade :要升级的协议是 websocket
  3. Sec-WebSocket-Extensions :表示客户端所希望执行的扩展(如消息压缩插件)
  4. Sec-WebSocket-Key :webSocket 协议校验值,服务端拼接一段固定字符串后加密返回回来,防止错误连接
  5. Sec-WebSocket-Version :websocket 的版本
Read more »

初始化流程

- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;

初始化入口

- (void)_SR_commonInit;

队列之类的数据初始化。
这里要说的是,SRWebSocket 内部创建一个常驻线程,用来接收数据流,还有一个专门用来处理业务的队列;当没有数据传输时常驻线程会进入休眠,此时队列任务发现_readBuffer没有数据,也会跳出循环等待,这样没有数据时也就不需要消耗多少资源了。

- (void)_initializeStreams;

初始化输入输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)_initializeStreams {
uint32_t port = _url.port.unsignedIntValue;
if (port == 0) {
if (!_secure) {
port = 80;
} else {
port = 443;
}
}
NSString *host = _url.host;

CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;
//将 host port 与输入输出流绑定在一起
CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);

_outputStream = CFBridgingRelease(writeStream);
_inputStream = CFBridgingRelease(readStream);

_inputStream.delegate = self;
_outputStream.delegate = self;
}

连接流程

- (void)open;

连接入口

- (void)openConnection;
  1. 将输入输出流注册到常驻线程
  2. 开启输入输出流
1
2
3
4
[_outputStream scheduleInRunLoop:aRunLoop forMode:mode];
[_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
[_outputStream open];
[_inputStream open];
Read more »

什么是 Module

module 表示代码编译的最小模块,也就是一个.m文件编译成.o目标文件,那么这个.o就是一个 module。而 modulemap 可以将多个 module 合并成一个 modulemodule 还有另一个功能就是充当 Framework 中 OC 跟 Swift 的桥接文件。
module 可以用来取代C/C++预言传统的头文件引入方式,C/C++ 中单个文件被引入了N次就需要编译N次,而使用 module 只需要1次。
module和头文件之间则是通过 modulemap 关联的

#include

include 使用不当会导致头文件重复导入。
预编译时会将 include 文件递归导入进.m文件,
假如有M个文件,每个文件都引入N个头文件,那么编译时间变为 M * N

#import

改进了 include ,可以防止重复添加头文件

@import

@import 导入的就是一个 module 的头文件。在使用 module 之前我们需要先打开这个功能

1
2
Defines Module = YES
Module Map File = "手动创建的文件路径" //可选

module到底有什么作用呢?
module 会先把头文件编译成二进制文件,哪里需要使用这个头文件都会直接使用这个编译好的二进制头文件,除非这个头文件自身发生改动。
同一个头文件只需要编译一次就行了

Read more »

Allocations

Allocations 一般包含一个 VM Tracker,后面会简单介绍

Statistics

直译:统计 的意思
表示当前系统的内存占用列表

img

All Heap Allocation

开发者手动申请的内存(堆),虚拟内存,这一部分是由开发者控制的。未使用的内存不会直接分配物理内存,只有使用了的内存才会在物理内存上分配空间。

All Anonymous VM

相对于All Heap Allocation,这里的是匿名的虚拟内存,开发者无法控制的内存。memory mapped file 、CALayer back store 好像都是在这里的

1
All Heap & Anonymous VM 指的就是 All Heap Allocation + All Anonymous VM

VM:ImageIO_PNG_Data

使用 [UIImage imageNamed:@"*.png"] 缓存的解压后的图片

VM:CG raster data

通过CG解压的图片.光栅化数据,也就是像素数据

Call Tree

显示调用函数,点击具体的函数能跳转到对应的代码

1
2
Invert Call Tree 倒置函数栈
Hide System Libraries 隐藏系统库

Allocations list

可以按照单次分配的内存大小排序,可以清楚的看到对应的调用栈

img

Generations

查看两个时间点之间的内存变化

VM Tracker

  • 打开界面后,需要先启动 VM Tracker
  • img
img

Resident 指的是当前物理内存(已加载的代码段+脏内存)
VM Region 一个 VM Region 是指一段连续的内存页(在虚拟地址空间里),这些页拥有相同的属性(如读写权限、是否是 wired,也就是是否能被 page out)
VMObject 每个 VM Region 对应一个数据结构,名为 VM Object。

% of Res. 当前 Type 的 Resident 占 总 Resident 的比例
Type 虚拟内存的类型
# Regs VM Region 的个数,也就是 VMObject 的个数?
Path VM Region 从哪个文件映射过来的
Dirty Size 脏内存,也就是系统无法回收的内存
Swapped Size OSX 中被交换的内存。iOS 没有交换区,此时的Swapped Size就是压缩内存
Virtual Size 虚拟内存总大小
Res. % 当前物理内存占虚拟内存的总大小

image-20220904111138382

0x00 内存分类

根据不同 Section 可以将内存分为

  • 代码段 .text
  • 已初始化数据段 .data
  • 未初始化数据段 .bss
  • 堆 heap
  • 栈 stack
image-20220904111317704

根据内存能否被系统回收,可以分为

  • Clean Memory
  • Dirty Memory

Clean Memory

内存紧张时可以被覆盖,下次需要使用时,触发缺页中断,然后从磁盘加载到内存 (Page In)

  • system framework
  • binary executable of your app
  • memory mapped files
1
2
疑问:链接的 framework 中 _DATA_CONST 并不绝对属于 clean memory,当 app 使用到 framework 时,就会变成 dirty memory。
这里,嗯,没理解什么意思

Dirty Memory

无法被系统回收的内存,内存紧张时会给进程发送通知,需要程序手动释放这部分内存。同时系统会压缩这部分的内存,等下次使用时再解压。

  • heap allocation
  • caches
  • decompressed images
  • compressed memory
1
2
3
4
5
6
虚拟内存
Virtual Memory = Clean Memory + Dirty Memory
物理内存
Resident Memory = Clean Memory(Loaded in Physical Memory) + Dirty Memory
实际内存占用
memory footprint = dirty size + compressed size

0x01 内存管理

内存管理可以分为两部分

  • APP内存管理,由APP内部控制
  • 系统内存管理,由系统控制

APP内存管理

APP管理方案有3中

Tagged Pointer

在64位的机器上,未引入 Tagged Pointer 之前内存结构如下图,对于一些很小的数据,在64位的机器下占用的内存翻了一倍,单单是指针就占用了2/3的字节,同时还要在堆分配内存,维护引用计数等

基于以上问题,苹果引入了 Tagged Pointer 对象,把一个对象的指针分为两部分,一部分作为数据标识,一部分存储数据。此时对象的指针不再是指针,更像是一个变量,并且不需要在堆中分配内存,这样不仅减少了占用内存,还提高了使用效率。
image-20220904111357275

Tagged Pointer 对象会在使用时创建,存放在栈区,同一个值每次创建都是同一个地址。
iOS默认开启了 Tagged Pointer 混淆,调试时设置 OBJC_DISABLE_TAG_OBFUSCATION = YES 后,数据正常了,每次APP启动后,都是同一个值

测试

1
2
NSNumber *num1 = @(7); //0x8000000000000393
NSNumber *num2 = @(3); //0x8000000000000193

低3位表示类标识
低4~7位表示数据类型
最高位表示是否是Tagged Pointer

Non-pointer iSA

在64位的架构下,指针查找数据并不需要64位,而苹果实际上只用33位来存储地址,剩下的用来存储一些其他的数据,iSA指针的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 注意真机和模拟器的结构是不一样的
union isa_t
{
Class cls;
uintptr_t bits;

# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; //是否开启 nonpointer
uintptr_t has_assoc : 1; //关联对象
uintptr_t has_cxx_dtor : 1; //表明对象是否有C++或ARC析构函数
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1; //弱引用
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1; //是否当前的引用计数过大
uintptr_t extra_rc : 19; // 引用计数 = extra_rc + 1,超过就通过 SideTable 存储
};
};

是否使用 Non-pointer iSA 由苹果决定

1
2
3
4
5
6
1:包含swift代码
2:sdk版本低于10.11
3:runtime读取image时发现这个image包含__objc_rawisa段
4:开发者自己添加了OBJC_DISABLE_NONPOINTER_ISA=YES到环境变量中
5:某些不能使用Non-pointer的类,GCD等
6:父类关闭

SideTables

SideTables 是一个散列表, 用来管理对象的引用计数和弱引用。由于对象引用计数的操作是原子性操作所以 SideTable 中使用了自旋锁,SideTables 分成了8个 SideTable,实现了分离锁技术,提高了效率。

1
2
3
4
5
6
7
8
struct SideTable {
//非公平的自旋锁
spinlock_t slock;
//强引用相关,内部是一个hash表。
RefcountMap refcnts;
//弱引用相关,内部也是一个哈希表,每一个元素指向一个可变数组
weak_table_t weak_table;
}
  • RefcountMap 仅在未开启 isa 优化或 isa 优化情况下的引用计数溢出时才会用到
  • 8个 SideTable 可以一定程度上解决效率问题

系统内存管理

当系统发现没有可用的内存页时,可能会有以下步骤

  • 覆盖掉优先级较低的 Clean Memory ,以页为单位
  • 给所有的前后台APP进程发送内存警告通知(一般APP会释放掉一些可以再次加载的内存)
  • 通过上面两个步骤后,内存依然不够用,低内存管理机制 Jetsam 会根据优先级 kill 对应的进程

为什么手机APP容易被系统杀死,电脑APP不会被杀死,但却容易卡死

  • 电脑的 Swap 区在硬盘中,硬盘本身很大,很轻松的就虚拟出一个内存(虚拟内存)。机械硬盘不限读写次数,所以内存和硬盘之间可以无限读写。
  • 手机一般是用 flash 做存储器的,读写次数有限,如果用 flash 做 Swap 区,那么 flash 很可能在短时间内报废。所有手机一般都无 Swap 区。原因:1. flash 大小有限。2. flash 读写次数有限制