深入了解Tagged Pointer

objc 源码版本:779.1
当然还是推荐使用这个来学习:可编译的源码

在 2013 年苹果推出了首个使用 64 位架构的双核处理器的手机 iphone 5s。为了节约内存以及提高执行效率,苹果使用了一种叫做 ‘Tagged Pointer’ 的技术,现在跟着我来了解一下它吧。

Tagged Pointer

从 5s 开始,iPhone 均使用 arm64 指令集的处理器。在 64 位系统上,一个指针占 8 个字节,而指针指向的实例变量至少需要 16 个字节,并且还需要执行额外的一些操作,例如:申请内存,销毁内存。为了达到优化的目的,苹果将一些存储数据的类,例如 NSString,NSNumber,当它们需要保存的数据不需要占用那么多的字节时,直接将数据保存在“指针”里面。

下面让我们用代码来证实Tagged Pointer的存在

1
NSNumber *a = [NSNumber numberWithInt:1];

然后打个断点,使用 lldb 的命令调试,x/8xg a,该命令的意思是从a的起始地址开始,打印 8 个 16进制的 8字节长度的值
输出结果

说明指针 a 并不是指向 NSNumber 实例的指针。

或者下面这样更加直观一点

可以看到 a 并没有 isa 指针,所以它并不是一个 NSNumber 实例指针。

Tagged Pointer 如何存储数据

这里你最好打开源码对照着看。

LSB

在非 arm64 架构中,将最低位即 LSB 设置为 1,与正常的指针进行区分。
这样做的原因是,OC 类在创建实例最终调用的是 C 标准库中的 calloc 函数,它所返回的内存地址是 16 字节对齐的,参考 Aligned memory management?。这样的结果就是指针地址低 4 位都是 0,用最低位来表示也合理的

非 arm64 架构下标记位的设定

MSB

在 arm64 架构中,将最高位即 MSB 设置为 1,与正常的指针进行区分。
这样做的原因的是因为在 arm64 架构中,指针只用了低位的 48 位,高 16 位都是空着的,原因可以看下这个 为什么64位机指针只用48个位?

objc-internal.h 372 行以及 384 行,我们 我们可以看到掩码 _OBJC_TAG_MASK 的定义:

1
2
3
4
5
6
7
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
# define _OBJC_TAG_INDEX_SHIFT 60
#else
# define _OBJC_TAG_MASK 1UL
# define _OBJC_TAG_INDEX_SHIFT 1
#endif

内置类型和扩展类型

系统启动时生成两个全局的数组 objc_debug_taggedpointer_classes 和 objc_debug_taggedpointer_ext_classes。一个用来存储系统内置的Tagged Pointer类型,而另一个数组存储扩展的Tagged Pointer类型。

1
2
3
4
extern "C" { 
extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT];
extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
}
  • 内置类型数组的数目为 8,使用高 2 - 高 4 的 3 个 bit 来存储数据
  • 扩展类型数组的数目为 256,,使用高 5 - 高 12 的 8 个 bit 来存储数据

是否剩余的 bit 都用来保存数据了呢?答案是否定的,拿 NSUmber 举个例子,它属于内置类型,其 tagged pointer 额外使用 低 1 - 低 4 的 5 个 bit 用来保存数字的类型信息,即使用 56 个 bit 来保存数据。

内置类型如下:

1
2
3
4
5
6
7
8
9
10
11
enum
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
}

而当指针的高 1 - 高 4 的 4 个 bit 位均为 1 时,表示这是一个扩展类型。系统自带的扩展类型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum
{
// ...
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
// ...
}

数据混淆

运行下面的代码

1
2
3
4
5
6
7
8
9
10
11
- (void)boo
{
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithInt:2];
NSNumber *c = [NSNumber numberWithInt:16];

NSLog(@"pointer a is %lx", a);
NSLog(@"pointer b is %lx", b);
NSLog(@"pointer c is %lx", c);
NSLog(@"pointer d is %lx", d);
}

输出结果:

1
2
3
pointer a is ef59c3d36981ed4b
pointer b is ef59c3d36981ed7b
pointer c is ef59c3d36981ec5b

等等,不是说 Tagged Pointer 最高 4 位用来保存类型信息,剩下的几位都只用来保存数据嘛,为什么输出结果看起来这么复杂呢?
原因是从 iOS12 开始,为了系统安全,将数据与一个随机数进行 异或(^) 操作进行混淆,混淆函数如下:

1
2
3
4
5
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

objc_debug_taggedpointer_obfuscator是一个extern关键字的常量,因为被 extern 声明,所以我们可以用下面的代码来解码,得到真正的值

1
2
3
4
5
6
7
8
9
10
11
12
extern uintptr_t objc_debug_taggedpointer_obfuscator;

- (void)foo
{
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithInt:2];
NSNumber *c = [NSNumber numberWithInt:16];

NSLog(@"pointer a real value is %lx", ((uintptr_t)a ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer b real value is %lx", ((uintptr_t)b ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer c real value is %lx", ((uintptr_t)c ^ objc_debug_taggedpointer_obfuscator));
}

输出结果:

1
2
3
pointer a real value is b000000000000012
pointer b real value is b000000000000022
pointer c real value is b000000000000102

从输出结果可以看出,这几个值都是 0Xb 开头,16进制的 b 用 二级制表示为 1011,最高1用来表示这是一个 Tagged Pointer,而剩余 3 位的10进制数为 3,符合之前的定义OBJC_TAG_NSNumber = 3
至于为什么结尾都是 0x2,这个后面再解释。
下面让我们测试下 NSNumber 的 Tagged Pointer 使用多少位来保存数据。从之前的探究我们知道内置类型用 60 位来保存数据,而经过上面的实验我们可以看到还有 4 位用来做别的事了,那么是否剩余的 56 位都用来保存数据了呢?

1
2
3
4
5
- (void)foo
{
NSNumber *d = [NSNumber numberWithLongLong:-0x7FFFFFFFFFFFFF];
NSLog(@"pointer a real value is %lx", ((uintptr_t)d ^ objc_debug_taggedpointer_obfuscator));
}

输出结果:

1
pointer d real value is b800000000000013

结果符合预期。至于为什么最高位 b 后面的数字是 8,是因为高位 5 的位置变成了 1,用来表示这个数是负数(0则表示正数), 而 7 的二进制表示为 ob111,高位 5-8 连起来就是 0b1111,也就是16进制的 8 了。
还一个值得注意的是低位第一位的数字变成了 3,而不是之前的正整数 2,由此我们可以推测最低位的 4 位是用来表示存储数据类型的数据,例如 int,float,bool 这几个类型生成的 Tagged Pointer 最低4位数字应该是不同的。用下面的代表再来验证下:

1
2
3
4
5
6
7
8
9
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithShort:2];
NSNumber *c = [NSNumber numberWithFloat:1.];
NSNumber *d = [NSNumber numberWithLongLong:-0x7FFFFFFFFFFFFF];

NSLog(@"pointer a real value is %lx", ((uintptr_t)a ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer b real value is %lx", ((uintptr_t)b ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer c real value is %lx", ((uintptr_t)c ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer d real value is %lx", ((uintptr_t)d ^ objc_debug_taggedpointer_obfuscator));

输出结果

1
2
3
4
pointer a real value is b000000000000012
pointer b real value is b000000000000021
pointer c real value is b000000000000014
pointer d real value is b800000000000013

结果符合预期,说明我们的推测是正确的

参考

NSNumber 与 Tagged Pointer
深入解构 objc_msgSend 函数的实现

希望大家看了有所收获吧。

iOS的锁以及GCD相关

这段时间的研究内容的是,因为实际开发中用到的比较少,文中难免会有错误,希望能够多多指正。
这篇博客的第一部分是一些计算机的基础知识,然后介绍一些常见的锁以及它们的工作原理,最后部分是 GCD 相关的一些内容。

一些基础知识

下面是一些计算机知识,比较枯燥。你可以跳过这一部分,直接看后面的内容,等看到一些不懂的概念的时候再跳回来看这部分内容。

时间片

时间片又称为“量子”或者“处理器片”,是分时操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。现代操作系统(例如 Windows,Mac OS X)允许同时运行多个进程。例如,在打开音乐播放器的同时用浏览器浏览网页并下载文件。由于有些计算机只有一个CPU,所以不可能真正地同时运行多个任务。这些进程“看起来像”同时运行,实则是轮番运行,由于时间片通常很短(在Linux上为5ms-800ms),用户不会感觉到。

时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
通常状况下,一个系统所有的进程被分配到的时间片长短并不相等,尽管初始时间片基本相等,系统通过测量进程的阻塞执行状态的时间长短来计算每个进程的交互性。交互性和每个进程预设的静态优先级(Nice值)的叠加即是动态优先级,动态优先级按比例缩放就是要分配给那个进程时间片的长短。一般的,为了获得较快的响应速度,交互性强的进程(即趋向于IO消耗型)被分配到的时间片要长于交互性弱的进程。

进程基本状态

进程有以下几种状态:

  • new:创建状态。进程正在被创建,仅仅在堆上分配内存,尚未进入就绪态
  • ready:就绪态。进程已处于准备运行的状态,即进程已获得除了 CPU 之外的所需资源,一旦分配到 CPU 时间片即可进入运行状态
  • run:运行态。进程正在运行,占用 CPU 资源,执行代码。任意时间点,处于运行状态的进程(线程)的总数,不会超过 CPU 的总核数
  • wait:阻塞态。进程处于等待某一事件而放弃 CPU,暂停运行。阻塞状态分3种:
    • 阻塞在对象等待池:当进程在运行时执行wait()方法,将线程放入等待池
    • 阻塞在对象锁池:当对象在运行时企图获取已经被其它进程占用的同步锁时,会把线程放入锁池
    • 其它阻塞状态:当进程在运行时执行sleep()方法,或调用其它进程的join()方法,或发出I/O请求时,进入阻塞状态
  • dead:死亡态。进程正在被结束,这可能是进程正常结束或其它原因中断运行。进程结束运行前,系统必须置进程为dead态,再处理资源释放和回收等工作

在特定的情况下,这三种状态可以相互转换

  1. ready -> run: 就绪态的进程获得 CPU 时间片,进入运行态
  2. run -> ready: 运行态的进程在时间片用完后,必须出让 CPU,进入就绪态
  3. run -> wait: 当进程请求资源的使用权或等待事件发生(如I/O完成)时,由运行态转换为阻塞态
  4. wait -> ready: 当进程已经获取所需资源的使用权或者等待时间已完成时,中断处理程序必须把相应进程的状态由阻塞态转为就绪态

状态转移

进程以及线程的关系

进程是资源分配的最小单位

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
每个进程都占用一个进程表项,该表项包含了操作系统对进程进行描述和控制的全部信息,包括程序计数器,堆栈指针,内存分配状况,打开文件的状态,账号和调度信息

线程是”轻量级的进程“,是 CPU 调度的最小单位

一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。


进程和线程的区别有以下几点:

  1. 调度:在多线程os中,线程是调度和分配的基本单位,进程是资源分配的最小单位。在同一进程中,线程的切换不会引起进程的切换。线程上下文切换比进程上下文切换要快很多
  2. 资源:进程是拥有资源的一个基本单位,他可以拥有自己的资源,一般地说,线程不拥有系统资源(只有一些必不可少的资源),但它可以访问其隶属进程的资源
  3. 系统开销:在创建和销毁进程时,系统都要为之分配和回收资源,因此,操作系统所付出的开销显著的大于创建或销毁线程的开销
  4. 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信
  5. 在多线程os中,进程不是一个可执行的实体
对比维度 进程 线程
数据共享、同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂
内存、CPU 占用内存多,切换复杂,CPU利用率低 内存占用少,切换简单,CPU利用率高
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快
编程、调试 编程简单,调试简单 编程复杂,调试复杂
可靠性 进程间不会互相影响 线程可能会引起进程异常
分布式 适用于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适用于多核分布式

哈希表

哈希表(Hash table, 也叫散列表),是根据 key 而直接访问在内存存储位置的数据结构。哈希表本质是一个数组, 通过哈希函数(散列函数)将 key 转换成 index,根据 index 在数组中找到相应的数据。
举个例子:为了在电话本中查找某人的号码,可以创建一个按照人名首字母顺序排列的表,在首字母为“W”的表中查找“王”姓的电话号码,显然比直接查找就要快的多。这里使用人名作为 key,“取首字母“就是这个例子中的哈希函数 F(),存放首字母的表对应哈希表。

不管哈希函数设计的如何完美,都可能出现不同的 key 经过哈希函数处理后得到相同的 hash 值。解决哈希冲突的方法,常见的有下面两种:

  • 开放定址法:使用两个大小为N的数组(一个存放keys,一个存放values)。使用数组中的空位解决碰撞,当碰撞发生时,直接 hash 值+1,如果此时对应下标的位置仍被占用,则 hash 值继续+1;如果位置为空,则将 key 存放在此位置中。举个例子:
    将关键字为{89, 18, 49, 48, 69}插入到一个散列表中。假定取关键字除以10的余数为哈希函数法则。
散列地址 空表 插入89 插入18 插入49 插入58 插入69
0 49 49 49
1 58 58
2 69
3
4
5
6
7
8 18 18 18 18
9 89 89 89 89 89

第一次冲突发生在填装49的时候。地址为9的单元已经填装了89这个关键字,所以取49的哈希值并+1,得到10,也就是0,发现该地址为空,所以将49填装在地址为0的空单元。第二次冲突则发生在58上,取哈希值为8,因为位置9和0都已经占用,往下查找3个单位,将58填装在地址为1的空单元。69同理。

  • 拉链法:将哈希表同一个存储位置的所有元素保存在一个链表中。实现时,一种策略是散列表同一位置的所有冲突结果都是用栈存放的,即新元素被插入到链表头中。简单讲就是数组+链表

iOS 关联对象及 weak 对象均以该方法储存。

原子性

原子指化学反应中的基本为例,原子在化学反应中不可分割。
计算机中所谓原子性是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何的上下文切换(context switch)。

临界区

临界区指的是一个访问共享资源的代码片段,并非一种机制或是算法。一个程序可以拥有多个临界区域。
当有一个线程在访问临界区,那么其它线程将被挂起。临界区被释放后,其它线程可继续抢占该临界区

在计算机科学中,锁是一种同步机制,用于限制多线程环境中对临界区的访问,你可以理解锁是用于排除并发的一种的策略。
但如果使用不当,可能会引起死锁,锁封护(lock convoying,多个同优先级的线程重复竞争同一把锁,此时大量虽然被唤醒而得不到锁的线程被迫进行调度切换)等不良影响。

互斥锁

互斥锁是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。
互斥锁是排它的,当锁被某个线程获取之后,其它访问临界区的线程进入阻塞态。只有当获取了锁的线程释放这个锁,被阻塞的线程才会被唤醒进入运行态。

举个例子:一段代码(甲)正在分步修改一块数据。这时,另一条线程(乙)由于一些原因被唤醒。如果乙此时去读取甲正在修改的数据,而甲碰巧还没有完成整个修改过程,这个时候这块数据的状态就处在极大的不确定状态中,读取到的数据当然也是有问题的。更严重的情况是乙也往这块地方写数据,这样的一来,后果将变得不可收拾。因此,多个线程间共享的数据必须被保护。达到这个目的的方法,就是确保同一时间只有一个临界区域处于运行状态,而其他的临界区域,无论是读是写,都必须被挂起并且不能获得运行机会。

互斥锁在申请锁时,调用了pthread_mutex_lock方法,它在不同的系统上实现各有不同,有时候它的内部是使用信号量来实现,即使不用信号量,也会调用到lll_futex_wait函数,从而导致线程休眠。
上文说到如果临界区很短,忙等的效率也许更高,所以在有些版本的实现中,会首先尝试一定次数(比如 1000 次)的 test-and-test,这样可以在错误使用互斥锁时提高性能。
另外,由于pthread_mutex有多种类型,可以支持递归锁等,因此在申请加锁时,需要对锁的类型加以判断,这也就是为什么它和信号量的实现类似,但效率略低的原因。

@synchronized

使用方式:

1
2
3
@synchronized (obj) {
// do something...
}

@synchronized(id obj)关键字锁,使用的时候需要添加一个OC对象。在很多情况下,@synchronized 的可读性更高,使用更方便。
开始的时候,我一直不敢用这个锁。因为我不知道 obj 需要什么样子的变量才可以,局部变量有用吗,还是一定需要全局变量什么的?
但是看了关于 @synchronized,这儿比你想知道的还要多这篇博客之后,理解了它的实现原理,才明白,obj只要是个oc对象就行,当然,在实际使用的时候,你输入的这个对象最好不可以被外界所修改。

现在简单讲解一下关于 @synchronized,这儿比你想知道的还要多这篇博客里面 @synchronized 的工作原理:

@synchronized 使用哈希链表的方式存储锁SyncData。SyncData 是链表上的元素,每个SyncData都有一个递归互斥锁recursive_mutex_t mutex,一个id object(传入的obj),一个int threadCount(使用或等待的线程数量,等于0代表这个锁可以被复用)以及下一个节点struct SyncData* nextData。结构体SyncList的成员变量SyncData *data用来记录链表上的头节点,spinlock_t lock用来防止多线程并发对链表进行修改

当你调用 @synchronized(obj) 时,首先会根据 obj 的内存地址计算出其哈希值,然后在哈希表上找到相应的SyncList实例,接下来根据 obj 来查找有没有未被使用(threadCount == 0)的SyncData实例,如果有则使用这个锁;如果没有则新建一个SyncData锁实例,将锁插入到链表的SyncList的头结点中(这样查找会快一点,因为新建的锁往往使用的频繁一点)。上面的查找和新建过程都是加锁的,结束后解锁。

使用时可能有两种特殊情况

  • 输入 nil
    此时 @synchronized 不起作用,即锁不生效

  • 输入的 obj 在 @synchronized 的 block 里面被释放掉了
    对 @synchronized 的使用没有影响。你可以使用clang -rewrite-objc xx.m将代码转换成 C++ 实现

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
// 代码
- (void)foo {
NSObject *object = [NSObject new];
@synchronized (object) {
NSLog(@"测试@synchronized");
}
}

// c++实现
static void _I_MyObject_foo(MyObject * self, SEL _cmd) {
NSObject *object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));

{ id _rethrow = 0; id _sync_obj = (id)object; objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);

NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_MyObject_807aa3_mi_1);
} catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}
}

看到这几行代码id _rethrow = 0; id _sync_obj = (id)object; objc_sync_enter(_sync_obj);,在内部会将 obj 的值复制一份,所以即使你将 obj 置为 nil,还是能够正常使用。

pthread_mutex

pthread_mutex 的常见用法如下:

1
2
3
4
5
6
7
8
9
10
pthread_mutexattr_t attr;  
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 定义锁的属性

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) // 创建锁

pthread_mutex_lock(&mutex); // 申请锁
// 临界区
pthread_mutex_unlock(&mutex); // 释放锁

对于 pthread_mutex 来说,它的用法和之前没有太大的改变,比较重要的是锁的类型,可以有PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE等等,具体的特性就不做解释了,网上有很多相关资料。
一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。
然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 pthread_mutex 支持递归锁,也就是允许一个线程递归的申请锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE 即可。

NSLock 和 NSRecursiveLock

使用方式:

1
2
3
4
5
6
7
8
9
10
11
- (void)foo {
NSLock *lock = [NSLock new];
[lock lock];
// do something...
[lock unlock];

NSRecursiveLock *recursiveLock = [NSRecursiveLock new];
[recursiveLock lock];
// do something...
[recursiveLock unlock];
}

NSLock 和 NSRecursiveLock 是 Objective-C 以对象的形式暴露给开发者的一种锁,它们的内部实现都是使用的pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定新能换来错误提示。理论上来说,NSLock 和 pthread_mutex 拥有相同的运行效率,实际由于封装的原因会略慢一点。由于有缓存存在,相差不会很多。

NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,NSRecursiveLock 的类型为 PTHREAD_MUTEX_RECURSIVE。

自旋锁

自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。因此操作系统的实现在很多地方往往用自旋锁。Windows操作系统提供的轻型读写锁(SRW Lock)内部就用了自旋锁。显然,单核CPU不适于使用自旋锁,这里的单核CPU指的是单核单线程的CPU,因为,在同一时间只有一个线程是处在运行状态,假设运行线程A发现无法获取锁,只能等待解锁,但因为A自身不挂起,所以那个持有锁的线程B没有办法进入运行状态,只能等到操作系统分给A的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。

获取、释放自旋锁,实际上是读写自旋锁的存储内存或寄存器。因此这种读写操作必须是原子的。通常用test-and-set(TLS 检查并设置)等原子操作来实现。

OSSpinLock

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import <libkern/OSAtomic.h>

- (void)foo {
__block OSSpinLock osLock = OS_SPINLOCK_INIT;

// 线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"线程1 准备上锁");
OSSpinLockLock(&osLock);
NSLog(@"线程1");
OSSpinLockUnlock(&osLock);
NSLog(@"线程1 解锁完成");
});

// 线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSLog(@"线程2 准备上锁");
OSSpinLockLock(&osLock);
NSLog(@"线程2");
OSSpinLockUnlock(&osLock);
NSLog(@"线程2 解锁完成");
});
}

由于使用OSSpinLock的使用中可能会出现优先级反转的问题,苹果在 iOS10 发布之后,将 OSSpinLock 比较为了 Deprecated,并且提供了新的 os_unfair_lock 作为代替。

优先级翻转:有高优先级任务 a,低优先级任务 b,资源 y。b 获得锁并在访问 y,a 在等待。此时由于自旋锁,所以 a 处于忙等状态而占用大量 CPU,此时 b 无法获得时间片,而一直无法完成任务,释放掉锁。详细可以看这篇博客不再安全的 OSSpinLock

os_unfair_lock

使用方式:

1
2
3
4
5
6
7
8
9
10
#include <os/lock.h>

// 初始化
os_unfair_lock_t lock;
lock = &(OS_UNFAIR_LOCK_INIT);
// 加锁
os_unfair_lock_lock(lock);
// 临界区
// 解锁
os_unfair_lock_unlock(lock);

自旋锁和互斥锁的对比

相同点:

  • 都能保证同一时间只有一个线程访问共享资源。都能保证线程安全

不同点: 

  • 互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒
  • 自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行
  • 自旋锁的效率高于互斥锁,因为没有切换线程的消耗

信号量

信号量(英语:semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该 semaphore 对象的等待(wait)时,该计数值减一;当线程完成一次对 semaphore 对象的释放(release)时,计数值加一。当计数值为0,则线程等待该 semaphore 对象不再能成功直至该 semaphore 对象变成 signaled 状态。semaphore 对象的计数值大于0,为 signaled 状态;计数值等于0,为 nonsignaled 状态.

semaphore 对象适用于控制一个仅支持有限个用户的共享资源,是一种不需要使用忙碌等待(busy waiting)的方法。

信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程当前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在linux系统中,二进制信号量(binary semaphore)又称互斥锁(Mutex)。

dispatch_semaphore_t

使用方式:

1
2
3
4
5
6
7
// 创建一个信号量5的锁
dispatch_semaphore_t lock = dispatch_semaphore_create(5);
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// 让信号量的值+1
dispatch_semaphore_signal(lock);

注意,正常的使用顺序是先降低(wait)然后再提高(signal),这两个函数通常成对使用。

信号量是允许并发访问的,也就是说,允许多个线程同时执行多个任务。信号量可以由一个线程获取,然后由不同的线程释放。

dispatch_semaphore_wait()函数中,第二个参数超时时间我们可以选择DISPATCH_TIME_NOW或者DISPATCH_TIME_FOREVER。根据这个值,信号量最终会表现为互斥或者自旋的方式实现,这也是为什么评测中信号量性能总是优于互斥低于自旋。虽然信号量的性能不是最优,但是这种结合方案保证了它的作用范围更大。

条件锁

在线程间的同步中,有这样一种情况:
线程 A 需要等条件 C 成立,才能继续往下执行.现在这个条件不成立,线程 A 就阻塞等待。而线程 B 在执行过程中,使条件 C 成立了,就唤醒线程 A 继续执行。
对于上述情况,可以使用条件变量来操作。

条件变量,类似信号量,提供线程阻塞与信号机制,可以用来阻塞某个线程,等待某个数据就绪后,随后唤醒线程。
一个条件变量总是和一个互斥量搭配使用。

NSCodition

它通常用于表明共享资源是可被访问或者确保一系列任务能按照指定的执行顺序执行。如果一个线程视图访问一个共享资源,而正在访问该资源的线程将其条件设置为不可访问,那么该线程会被阻塞,直到正在访问该资源的线程将访问条件更改为可访问状态或者说给被阻塞的线程发送信号后,被阻塞的线程才能正常访问这个资源。

NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@property (nonatomic, strong) NSCondition *lock;

- (void)customer {
[_lock lock];
while (!data) {
[lock wait];
}
// 消费者消费数据
[_lock unlock];
}

- (void)producer {
[_lock lock];
// 生产数据
[_lock signal];
[_lock unlock];
}

NSCodition 可以给每个每个线程加锁,加锁后线程仍旧能够进入临界区。所以 NSCodition 使用 wait 并加锁之后,并不能真正的保证线程安全。当一个 broadcast 操作发出后,如果有两个线程都在做消费者操作,那同时都会消耗掉资源,可能会引发错误。所以我们在方法customer中,使用while (!data)来判断资源是否存在,而不是if (!data)
注意:signal只能唤醒单个 race 竞太,而broadcast是广播,唤醒所有

NSCoditionLock

NSConditionLock 称为条件锁,只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。

这里分清两个概念:

  • unlockWithCondition: 它是先解锁,再修改 condition 参数的值。 并不是当 condition 符合某个件值去解锁。
  • lockWhenCondition: 它与unlockWithCondition:不一样,不会修改 condition 参数的值,而是符合 condition 的值再上锁。

在这里可以利用 NSConditionLock 实现任务之间的依赖.

条件变量和信号量的区别

每个信号量都有一个与之关联的值,signal 时+1,wait 时-1,任何线程都可以发出一个信号,即使没有线程在等待该信号量的值。
可是对于条件变量,例如 signal 发出信号后,没有任何线程阻塞在wait 上,那这个条件变量上的信号会直接丢失掉。条件变量 NSCodition 可以使用方法broadcast唤醒所有阻塞的线程。

读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

pthread_rwlock

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <pthread.h>

//初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);

//读加锁
pthread_rwlock_rdlock(&lock);
//读尝试加锁
pthread_rwlock_trywrlock(&lock);

//写加锁
pthread_rwlock_wrlock(&lock);
//写尝试加锁
pthread_rwlock_trywrlock(&lock);

//解锁
pthread_rwlock_unlock(&lock);
//销毁
pthread_rwlock_destroy(&lock);
  • 同一时间,只能有1个线程进行写操作
  • 同一时间,允许多个线程进行读操作
  • 同一时间,不允许同时有读和写操作

具体使用:

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
#import <pthread.h>
@property (nonatomic, assign) pthread_rwlock_t lock;

- (void)foo {
pthread_rwlock_init(&_lock, NULL);
dispatch_queue_global_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for (int i = 0; i < 100; i++) {
dispatch_async(queue, ^{
[self read];
});
dispatch_async(queue, ^{
[self write];
});
}
}

- (void)read {
pthread_rwlock_rdlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}

- (void)write {
pthread_rwlock_wrlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}

barrier

dispatch_barrier_asyncdispatch_barrier_sync的共同点:

  • 会先完成在它们前面插入的任务,然后再执行自己的任务
  • 执行完自己的任务,再执行后面插入的任务

不同点:

  • dispatch_barrier_sync会先将自己的任务执行完,在会插入后面的任务
  • dispatch_barrier_sync不会等自己的任务执行完,就会把后面的任务插入队列,然后等待自己的任务结束在执行后面的任务

注意:

  1. dispatch_barrier_asyncdispatch_barrier_sync指定的队列必须是自己创建的并发队列,如果是串行队列或者全局并发队列,那么这两个方法的行为就会类似dispatch_asyncdispatch_sync
  2. dispatch_barrier_sync 如果指定当前队列可能会引起死锁

具体使用:

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
- (void)foo {
dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"test1");
});
dispatch_async(queue, ^{
NSLog(@"test2");
});
dispatch_async(queue, ^{
NSLog(@"test3");
});
dispatch_barrier_async(queue, ^{
for (int i = 0; i <= 50000000; i++) {
if (5000 == i) {
NSLog(@"point1");
}else if (6000 == i) {
NSLog(@"point2");
}else if (7000 == i) {
NSLog(@"point3");
}
}
NSLog(@"barrier");
});
NSLog(@"aaa");
dispatch_async(queue, ^{
NSLog(@"test4");
});
dispatch_async(queue, ^{
NSLog(@"test5");
});
dispatch_async(queue, ^{
NSLog(@"test6");
});
}

GCD

串行和并发队列

dispatch queue分发队列,我喜欢叫它任务分发队列,而任务就是我们在 block 中写的代码。
任务队列有两种:

  • 串行:当一个任务执行完,才能执行下一个任务。
  • 并行:当一个任务刚提交,不需要等它结束,就开始执行下一个任务。并发队列支持障碍任务(barrier block)

这两种队列均遵循 FIFO 原则,即先提交的任务先执行,举个简单的例子:
有三个任务,三个任务的输出分别是1,2,3。串行队列输出的结果是1,2,3;而并行队列的输出结果就不一定了。

虽然并行队列可以同时执行多个任务,但还是需要当前系统的状态来。如果当前系统最多只能处理2个任务,那么1、2就会排在前面先执行,等其中一个任务结束了,再执行任务3。

同步和异步

同步和异步针对的是线程。

同步任务:

  • 同步任务会阻塞当前线程,必须等待任务执行完毕返回,才能继续执行下一个任务
  • 作为一个优化,可能会在当前线程执行同步任务,因为切换线程需要消耗较多资源。但如果是在主队列中,则该任务会在主线程中执行
  • 在当前队列中执行同步任务会导致死锁。

异步任务:

  • 提交完任务后就立即返回,不会等待任务执行完毕。不会阻塞当前线程,会开启新的线程

这里结合队列举几个例子
例子1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)foo {
dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
NSLog(@"任务0");

dispatch_sync(queue, ^{
sleep(1);
NSLog(@"任务1 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(10);
NSLog(@"任务2 %@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务3 %@", [NSThread currentThread]);
});
NSLog(@"任务4");
}

输出结果如下:

1
2
3
4
5
2019-08-23 11:18:06.165116+0800 190611[6192:1600283] 任务0
2019-08-23 11:18:07.166471+0800 190611[6192:1600283] 任务1 <NSThread: 0x280a95dc0>{number = 1, name = main}
2019-08-23 11:18:17.167838+0800 190611[6192:1600318] 任务2 <NSThread: 0x280414100>{number = 3, name = (null)}
2019-08-23 11:18:17.168202+0800 190611[6192:1600283] 任务3 <NSThread: 0x280a95dc0>{number = 1, name = main}
2019-08-23 11:18:17.168329+0800 190611[6192:1600283] 任务4

分析一下代码的执行流程:

  1. 创建一个串行队列。串行队列里的任务是顺序执行的,且需要当前任务执行返回,才能执行下一个任务
  2. 任务1是同步任务,会阻塞当前线程(主线程)。任务1在主线程中执行
  3. 等任务1执行后,执行任务2。任务2是一个异步任务,所以在一个新线程中执行。当任务2执行完毕(过了10秒),执行任务3
  4. 任务3是一个同步任务,阻塞当前线程。任务3在主线程中执行
  5. 任务3执行完毕后,主线程不再被阻塞,执行任务4

通过上面的例子,我们可以得出这样的结论:

  1. 串行中的任务遵循先提交新执行的原则,且是按顺序一个一个执行的。不管任务是同步的还是异步的
  2. 同步任务会在当前线程中执行,并阻塞当前线程。异步任务会在一个新的线程中执行

例子2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)foo {
dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
NSLog(@"任务0");

dispatch_sync(queue, ^{
sleep(1);
NSLog(@"任务1 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(10);
NSLog(@"任务2 %@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"任务3 %@", [NSThread currentThread]);
});
NSLog(@"任务4");
}

输出结果如下:

1
2
3
4
5
2019-08-23 11:26:58.546591+0800 190611[6206:1601450] 任务0
2019-08-23 11:26:59.547963+0800 190611[6206:1601450] 任务1 <NSThread: 0x280283c40>{number = 1, name = main}
2019-08-23 11:26:59.548243+0800 190611[6206:1601450] 任务3 <NSThread: 0x280283c40>{number = 1, name = main}
2019-08-23 11:26:59.548320+0800 190611[6206:1601450] 任务4
2019-08-23 11:27:09.553363+0800 190611[6206:1601490] 任务2 <NSThread: 0x280c060c0>{number = 3, name = (null)}

分析一下代码的执行流程:

  1. 创建一个并行队列。并行队列里的任务也是顺序执行的,但不需要当前任务结束返回就能执行下一个任务
  2. 任务1是一个同步任务,会阻塞当前线程(主线程),并且执行任务(耗时1秒)。结束后,释放线程。执行任务3
  3. 任务3也是一个同步任务,会阻塞当前线程(主线程)。结束后,执行任务4
  4. 任务2是一个异步任务,虽然这个任务的提交时间跟任务1差不多,但它需要执行(10秒),所以它是最后才完成的

通过上面的例子,我们可以得出这样的结论:

  1. 并发队列虽然是”并发”,但仍是按照任务提交顺序来执行任务的,只不过它不需要等待任务结束返回就可以开始执行下一个任务,所以表现起来像是并发的。
  2. 在上面的例子中,任务1和任务3同样都是同步任务。同步任务会阻塞当前线程,任务1因为先提交所以先执行,然后阻塞主线程。任务3虽然在任务1执行后也跟着执行,但是因为主线程被任务1阻塞了,所以必须等待任务1执行完毕释放线程才能接着执行任务

下面我提几个问题,因为我在我好多博客里都看到了错误的结论

  • 在主队列中使用同步任务是否会造成死锁?

如果你直接在主线程中使用主队列提交一个同步任务是会造成死锁的。但下面这种情况就不会
例子3:

1
2
3
4
5
6
7
8
9
- (void)foo {
dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务1");
});
});
NSLog(@"任务0");
}

输出结果是

1
2
2019-08-23 14:35:44.580059+0800 190611[6310:1614276] 任务0
2019-08-23 14:35:44.587546+0800 190611[6310:1614276] 任务1

为什么呢?同步任务往往会阻塞当前线程,任务1在一个异步任务中提交,而异步任务中会创建一个新线程。所以,任务1仅仅是阻塞了这个新的线程。主队列是一个串行队列,任务0是可以算作提交的第一个任务,任务1是后面提交的任务,所以先执行任务1,再执行任务1.

还有很多说在并行队列中同步任务是顺序执行的

  • 在并行队列中的同步任务是否是顺序执行的?

在上面的例子2中是顺序执行,但下面这个例子里的就不会
例子4:

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_queue_t queue1 = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue2, ^{
dispatch_sync(queue1, ^{
sleep(3);
NSLog(@"任务1");
});
});
dispatch_sync(queue1, ^{
NSLog(@"任务2");
});
NSLog(@"任务0");

输出结果:

1
2
3
2019-08-23 14:56:55.999218+0800 190611[6346:1617197] 任务2
2019-08-23 14:56:55.999259+0800 190611[6346:1617197] 任务0
2019-08-23 14:56:59.008903+0800 190611[6346:1617213] 任务1

在任务2顺序执行的原因是在第一个同步任务阻塞了当前的线程。但在这个例子中,第一个同步任务,阻塞的是异步任务中创建的新线程,而不是主线程,所以第二个同步任务任务2会率先完成。


所以不要去背别人写好的规则,要学会自己去分析。下面是几点总结,类似于数学中的”公理“,能帮助你分析

  • 串行和并行队列,都是先执行先提交的任务。串行会等这个任务结束再执行下一个任务,而并行队列不会等它结束就执行下一个任务
  • 同步任务会在当前线程中执行,除了主队列提交的同步任务会在主线程中执行
  • 异步任务中会创建一个新的线程

更难的案例分析

案例1

1
2
3
4
5
6
7
- (void)foo {
NSLog(@"任务1");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
}

输出结果:

1
2
3
2019-08-23 15:07:03.149869+0800 190611[6361:1618423] 任务1
2019-08-23 15:07:03.149920+0800 190611[6361:1618423] 任务2
2019-08-23 15:07:03.149937+0800 190611[6361:1618423] 任务3

这里谈一下我的理解

  1. 首先我会将 foo() 这个方法当做在主队列中的第一个同步任务,把它叫做任务0好了
  2. 任务0在主线程中执行。任务2是一个同步任务,所以它会阻塞主线程。任务2提交到了一个并发队列中,而不是主队列,所以不会造成死锁。
  3. 任务2完成,释放主线程,执行任务3

如果把任务2中的队列替换成主队列,就会造成死锁。

1
2
3
4
5
6
7
- (void)foo {
NSLog(@"任务1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
}

原因是主队列是一个串行队列,任务按顺序执行。所以先要执行任务0,而任务2需要任务0执行完毕才能执行,但是任务2会阻塞主线程,导致两个任务谁都无法完成,造成死锁。


案例2

1
2
3
4
5
6
7
8
9
10
11
12
- (void)foo {
dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
NSLog(@"任务1");
dispatch_async(queue, ^{
NSLog(@"任务2");
dispatch_sync(queue, ^{
NSLog(@"任务3");
});
NSLog(@"任务4");
});
NSLog(@"任务5");
}

输出结果是 1,5,2 或者是 1,2,5

分析流程:

  1. 首先执行任务1
  2. 创建一个串行队列,并添加一个异步任务。由于异步任务和任务5不知道哪个会先执行,所以输出结果可能是 1,5,2 或者是 1,2,5
  3. 在异步任务中,首先执行任务2.然后在当前串行队列中使用了同步任务,造成死锁

案例3

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)foo {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
});
NSLog(@"任务4");
while (1) {
}
NSLog(@"任务5");
}

输出结果是 1,4 或者是 4,1
分析流程:

  1. 首先在并行队列中添加一个异步任务,所以不确定任务1跟任务4哪个先执行。所以输出结果是 1,4 或者是 4,1
  2. 在异步任务中,先执行任务1。然后碰到一个主队列的同步任务,由于是同步任务,所以会阻塞当前线程。该同步任务会等待主队列中的任务5执行完成然后再执行,但是任务5前面有一个死循环,所以任务5永远不会完成,也就是任务2永远无法完成,于是会一直卡着线程

总结

写了很多,写的也很杂,但如果你想了解锁,了解多线程的话,这些知识都是少不了的。
为了让硬件得到充分利用,我们会使用 GCD 来使用多线程,而为了多线程安全,我们又会使用锁。学习本身也是一个递归的过程,希望大家看完能有所收货~

如何使用 JYSqlModel

JYSqlModel是一个高效的 model、sqlite数据的转换工具。在 JYSqlModel,每一条数据库的数据都会被当做一个 model,所以当你对 model 执行相应的增删改查操作时,即对数据库进行着增删改查操作,这意味着你不再需要记得什么字段,什么约束,甚至可以不再使用 sql 语句来操作数据库!
JYSqlModel 依赖于库FMDB,使用前请保证你的项目中有这个库。
JYSqlModel 的思路来源来自于YYModel

使用前请将JYSqlModelJYClassInfo的 .h 和 .m 文件,放到工程中。

创建表

在 JYSqlModel 中,一个 model 对应于一个表。首先,你需要给这个表确定好一个名字,所以你必须在 model 内重载 JYSqlModel 的类方法+ (nonnull NSString *)tbName;。注意这个名字不能重复重复。
系统会在启动的时候,会根据 model 结构创建相应的表。表中字段(column)与 model 属性是一一对应的,在默认情况下,字段名字即是属性名字,当然你也可以使用协议中的方法自定义字段名,这个后面会详细讲。字段的类型会根据属性的类型分成不同的类型,目前只支持下表中的几种类型:

字段类型 描述 对应属性类型
integer 整型数 c的整型,NSNumber
real 浮点数 c的浮点型,NSDecimalNumber
text 文本 NSString,NSMutableString,NSURL
blob 二进制数据 NSData,NSMutableData
date 日期 NSDate
bool 布尔值 c的bool
unknow 除上面所以,会报错 除上面所有

如果你的类型不是上面支持的几种类型之一,那么就会被归为 unkonw 类型,随即就会报错。这样做的原因一方面是因为自己的知识不够丰富,不知道怎么处理其它的类型,另一方面避免复杂类型引起不可预料的错误。在后期的话可能还会支持 NSArray 等容器类型。

下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Student : JYSqlModel
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *school;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, strong) NSDate *birthDay;
@end

@implementation Student
+ (NSString *)tbName
{
return @"sudent";
}
@end

下面是创建相应表的 sql:

1
create table if not exists sudent (school text ,age integer ,birthDay date ,name text ,height real)

是不是很简单,你只需要新建一个JYSqlModel的子类,并给它指定一个表名,就能自动的创建一个表。

JYSqlModel 提供了以下几种方法来帮助你将 model 写入数据库:

1
2
3
4
5
6
// 1
+ (BOOL)addModel:(nonnull __kindof JYSqlMode *)model;
// 2
+ (BOOL)addModels:(nonnull NSArray<__kindof JYSqlMode *> *)models;
// 3
- (BOOL)addToSql;

方法1和方法2类似,都是使用类方法来添加 model 到数据库中。需要注意的是,model 的类需要跟类一致,否则会添加失败。
方法3是实例方法。添加成功会返回 YES,否则返回 NO

JYSqlModel 提供了以下几种方法来帮助你将 model 从数据库中删除:

1
2
3
4
5
6
// 1
+ (BOOL)deleteModelBySql:(nonnull NSString *)sql;
// 2
+ (BOOL)deleteModelByPrimaryKey:(NSString *)primaryKey value:(NSInteger)value;
// 3
+ - (BOOL)deleteFromSql;

方法1的话需要你自己写删除 sql,适用于删除语句比较简单或者批量删除的时候。你可以在子类中,根据这个方法封装一个更简单的方法。举个例子:

1
2
3
4
5
6
7
8
9
10
@interface Student : JYSqlMode    
+ (BOOL)deleteModeByPKValue:(NSInteger)value;
@end

@implementation JYPerson
+ (BOOL)deleteModeByPKValue:(NSInteger)value {
NSString *sql = [NSString stringWithFormat:@"delete from tbName where pk = %zd", value];
[JYSonModel deleteModelBySql:sql];
}
@end

现在你只需要一个pk值就能删除数据库记录了。当然你也可以封装一个实例方法,这样连参数都不需要了。

方法2需要你指定一个主键名字以及主键值来删除相应记录。类似于方法1,你也可以根据该方法封装一个更简便的方法!

方法3不需要你提供什么参数,使用起来更简单,但是如果对应表中没有主键,该删除方法就会失败,因为无法定位到具体到某一条记录。

JYSqlModel 提供了以下几种方法来帮助你更新数据库中的记录:

1
2
3
4
// 1
+ (BOOL)updateModelBySql:(NSString *)sql;
// 2
+ (BOOL)updateModel:(nonnull __kindof JYSqlMode *)model primaryKey:(NSString *)primaryKey value:(NSInteger)value;

方法1的话需要你自己提供更新 sql,适用于更新字段比较少或者批量更新的时候。

方法2需要你提供一个主键来确定是哪条记录,以便更新相应数据。该方法将更新除主键和autoincrement以外所有的字段,适用于更新字段比较多的情况。

JYSqlModel 提供了以下几种方法来帮助你查找数据库中的记录:

1
2
3
4
// 1
+ (nullable NSArray<__kindof JYSqlMode *> *)findModelsBySql:(nonnull NSString *)sql;
// 2
+ (nullable NSArray<__kindof JYSqlMode *> *)findAllModels;

方法1需要你自己提供查找 sql,返回结果将以 model 数组返回。

方法2将会查找出表中所有的记录,以 model 数组的形式返回。

数据迁移

在项目更新中,如果我们需要增加、删除 model 中的某些属性,或者改变属性的类型要怎么办呢?
很简单,你只需要对 model 进行修改,在项目运行时,JYSqlMode 会检测新 model 的结构以及旧表的结构,来判断是否需要进行数据迁移。
所谓的数据迁移也就是,将旧表重命名,根据新 model 的结构新建一个表,然后将旧表的数据迁移到新的表中。如此操作之后,新表的结构与新 model 的结构就一一对应了,保证了你在执行增删改查操作时不会出错。

当然,也有一些情况下,可能你做了修改也不会触发数据迁移。例如,如果仅仅是改变了字段的约束条件,那么是不会触发数据迁移的。解决办法最后面会给出。

JYSqlMode

下面是协议JYSqlMode的可选方法,帮助你执行一些自定义操作:

1
2
3
4
// 1
+ (nullable NSDictionary<NSString *, id> *)jyCustomPropertyMapper;
// 2
+ (nullable NSDictionary<NSString *, NSDictionary *> *)jyCustomPropertyConstraint;

方法1帮助你实现自定义 属性 -> 表字段名 的映射。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface Student : JYSqlMode <JYSqlMode>
@property NSString *name;
@property NSString *title;
@property NSInteger age;
@end

implementation Student
+ (nullable NSDictionary<NSString *, id> *)jyCustomPropertyMapper {
return @{
@"name" : @"fName",
@"title" : @"ftitle",
@"age" : @"fage"
}
}
@end

在相应的表中,fName 将会对应属性 name,ftitle 将会对应属性 title,fage 将会对应属性 age

方法2帮助你实现自定义字段的约束条件。目前仅支持以下几种约束类型:

  • JYColumnConstraintKeyNotNull -> not null
  • JYColumnConstraintKeyDefault -> default
  • JYColumnConstraintKeyDefaultValue -> 默认值,需要与 JYColumnConstraintKeyDefault 配套使用
  • JYColumnConstraintKeyUnique -> unique
  • JYColumnConstraintKeyPrimaryKey -> primary key
  • JYColumnConstraintKeyAutoIncrement -> autoincrement

需要注意的是,JYColumnConstraintKeyDefault 需要配合 JYColumnConstraintKeyDefaultValue 使用,设置一个默认值。
举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface Student : JYSqlModel <JYSqlModel>
@property (nonatomic, assign) NSInteger fid;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *school;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, strong) NSDate *birthDay;
@end

@implementation Student
+ (nullable NSDictionary<NSString *, NSDictionary *> *)jyCustomPropertyConstraint;
{
return @{
@"fid" : @{
JYColumnConstraintKeyPrimaryKey : @YES,
JYColumnConstraintKeyUnique : @YES
JYColumnConstraintKeyAutoIncrement : @YES
},
@"school" : @{
JYColumnConstraintKeyDefault : @YES,
JYColumnConstraintKeyDefaultValue : @"背背山小学"
}
};
}
@end

这样子,你就定义了一个primary key unique autoincrement的主键 fid,以及一个default '背背山小学'的字段 school。

@note

限于个人水平,JYSqlModel 肯定会在一些的问题。也许你会奇怪明明按说明操作了,但没有出现想要的结构。下面我将总结一下使用时需要注意的地方:

  1. 已存在一个旧表,若只是修改了相应字段的约束条件,这些约束条件是不会立刻生效的。一个解决办法是预留一个字段,当你需要更新表结构的时候修改这个预留字段的名字
  2. 已存在一个旧表,如果你为它设置了一个主键,抱歉设置会失败。解决方法参考第1条
  3. NSNumber类型的属性会存储到 integer 类型的字段中,如果想保存成浮点数的话,请将属性类型改为 NSDecimalNumber 或者直接使用 C 的浮点类型 float, double等
  4. 如果你在运行时动态的为 model 新增了属性,抱歉,表结构也不会更新

YYModel实现原理

其实很早开始就想写这个了,因为自己一直在用这个库,而且它的代码量也比较少,另外作者ibireme写的代码质量很高。为了提高自己,拜读大神写的库是一个很好的方式。
本文将从头开始分析代码,所以文字可能会比较多。

YYClassMethodInfo

这个类保存了 Method 的信息(其实后面好像也没怎么用到。。。)。 该类的属性及描述如下:

属性名字 描述
Method method 对应的Method
NSString *name 方法的名字
SEL sel 方法选择器
IMP imp 方法实现
NSString *typeEncoding 参数及返回值类型的编码
NSString *returnTypeEncoding 返回值类型的编码
NSArray<NSString *> *argumentTypeEncodings 参数类型的编码

想更多的了解类型编码可以去看Type Encodings这篇博客,主要是runtime加快消息的分发
你可以使用@encode将相应类型转换成内部表示的字符串,当然也有一大部分内部使用的类型编码无法用@encode()返回。

YYClassPropertyInfo

这个类保存了 Property 信息,主要用到等有settergetter方法,成员变量名称,编码方式(很重要,根据这个来确定属性类型以及其它修饰符)。该类的属性及描述如下:

属性名字 描述
objc_property_t property 对应的Property
NSString *name 名字
YYEncodingType type 类型编码的类型
NSString *typeEncoding 类型编码
NSString *ivarName 成员变量名称,加了个_前缀
Class cls 属性类型
NSArray<NSString *> *protocols 被包含着的协议,可能为空
SEL getter getter方法,不能为空
SEL setter setter方法,不能为空

属性转换成YYClassPropertyInfo的过程比较麻烦,比较复杂的地方在于需要通过解析属性的类型编码,来确定 YYClassPropertyInfo 的YYEncodingType type属性。

YYEncodingType 主要包含了以下几方面的信息:

  • YYEncodingTypeMask: 属性值的类型,例如c的基本数据类型,结构体,id类型,class类型
  • YYEncodingTypeQualifierMask:不知道怎么形容这个,在Type Encodings这篇博客里面说是内部使用的类型编码。例如:const,in,out
  • YYEncodingTypePropertyMask:属性关键词,例如:strong, weak, readonly

YYClassIvarInfo

这个类保存了 Ivar 的信息。貌似不会用到,这里就不展开讲了。

YYClassInfo

每一个类(包括元类)都有一个对应的 YYClassInfo 实例。非元类对应的实例创建之后会被保存在一个静态字典缓存 classCache 中,元类对应的实例保存在另一个静态字典缓存 metaCache 中,以Class -> YYClassInfo的映射关系保存在缓存中。如果缓存中没有,则重新创建一个。
创建时,首先会通过 runtime 的方法得到父类,是否是元类,元类,名字等基本信息,然后按顺序分别将它所有的的 Method,Property,Ivar 生成对应的 YYClassMethodInfo,YYClassPropertyInfo,YYClassIvarInfo实例,添加到 YYClassInfo 中。创建完成之后开始创建父类的 YYClassInfo 实例,一直到根类。

YYClassInfo 保存了 Class 的绝大部分信息,但是 objc 是一门动态的语言,可以在运行时添加方法,属性等信息,这也意味着 YYClassInfo 里面保存的信息可能不是最新的。所以当你对 Class 做了一些修改之后,你需要先获得该 Class 对应的 YYClassInfo实例,然后手动调用- (void)setNeedUpdate;来刷新保存在缓存中的 info 信息。

属性名称 描述
Class cls 对应类
Class superCls 对应类的父类
Class metaCls 对应类的元类
BOOL isMeta 对应类是否是元类
NSString *name 类名
YYClassInfo *superClassInfo 父类对应的 YYClassInfo 实例
NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos 所有成员变量信息
NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos 所有方法信息
NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos 所有属性信息

YYModel协议

如果默认的模型转换不能满足你的需求,那么你可以通过实现 YYModel 协议中的方法达到自定义键值转化的过程。下面,简单介绍一下它的几个方法:

  • + (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper;

实现这个方法,你可以自定义mode property -> json key之间的映射关系,你可以定义一个属性映射多个 key。举个注释中的例子:

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
json: 
{
"n":"Harry Pottery",
"p": 256,
"ext" : {
"desc" : "A book written by J.K.Rowling."
},
"ID" : 100010
}

model:
@interface YYBook : NSObject <YYModel>
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end

@implementation YYBook
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"page" : @"p",
@"desc" : @"ext.desc",
@"bookID": @[@"id", @"ID", @"book_id"]};
}
@end

在上面的那个例子中,实现该方法后,字典中 n 对应于属性 name,p 对应于属性 page。。。


  • + (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;

实现该方法,你可以自定义容器类属性的泛型。例如 NSArray 和 NSDictionary 类的属性,你可以通过该方法你定义它们的元素(数组) value(字典)的类型


  • + (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;

通过该方法,你可以自定义 json/字典 在不同的情况转化成不同 Class 的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@class YYCircle, YYRectangle, YYLine;

@implementation YYShape

+ (Class)modelCustomClassForDictionary:(NSDictionary*)dictionary {
if (dictionary[@"radius"] != nil) {
return [YYCircle class];
} else if (dictionary[@"width"] != nil) {
return [YYRectangle class];
} else if (dictionary[@"y2"] != nil) {
return [YYLine class];
} else {
return [self class];
}
}

@end

  • + (nullable NSArray<NSString *> *)modelPropertyBlacklist;

黑名单,数组中的元素代表的是属性的名字。如果这个方法实现了,那么数组中的属性则不会被赋值。


  • + (nullable NSArray<NSString *> *)modelPropertyWhitelist;

白名单,数组的元素代表的是属性的名字。如果这个方法实现,那么不在这个数组中的属性不会被赋值


  • - (NSDictionary *)modelCustomWillTransformFromDictionary:(NSDictionary *)dic;

在数据模型转换之前,对json字典进行修改。如果实现了这个方法,那么将使用修改后的字典进行mode转换


  • - (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic;

如果你想自定义数据模型转换,那么你可以实现这个方法。在这个方法里你可以生成 mode 的实例,并根据 dic 对实例的属性赋值,记得创建成功后返回 YES

  • - (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic;

fixme 不知道干嘛

_YYModelMeta

这是一个内部类,它主要用来保存类的信息,每一个类都有一个对应的 _YYModelMeta 实例。创建好的实例会被保存在一个静态字典缓存中 cache,以clss -> _YYModelMeta的映射关系存储。如果缓存中没有该实例,则重新创建一个。看到这你是不是会觉得很像之前提到的YYClassInfo?简单点说,YYClassInfo 全盘记录了 class 的信息,_YYModelMeta 是对这些信息进行了加工整理。

下面是 _YYModelMeta 的构造函数,代码比较多,可以直接跳到后面看分析:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
- (instancetype)initWithClass:(Class)cls {
YYClassInfo *classInfo = [YYClassInfo classInfoWithClass:cls];
if (!classInfo) return nil;
self = [super init];

// 得到自定义黑名单
NSSet *blacklist = nil;
if ([cls respondsToSelector:@selector(modelPropertyBlacklist)]) {
NSArray *properties = [(id<YYModel>)cls modelPropertyBlacklist];
if (properties) {
blacklist = [NSSet setWithArray:properties];
}
}

// 得到自定义白名单
NSSet *whitelist = nil;
if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) {
NSArray *properties = [(id<YYModel>)cls modelPropertyWhitelist];
if (properties) {
whitelist = [NSSet setWithArray:properties];
}
}

// 自定义容器类属性的泛型
NSDictionary *genericMapper = nil;
if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
genericMapper = [(id<YYModel>)cls modelContainerPropertyGenericClass];
if (genericMapper) {
NSMutableDictionary *tmp = [NSMutableDictionary new];
[genericMapper enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![key isKindOfClass:[NSString class]]) return;
Class meta = object_getClass(obj);
if (!meta) return;
if (class_isMetaClass(meta)) {
tmp[key] = obj;
} else if ([obj isKindOfClass:[NSString class]]) {
Class cls = NSClassFromString(obj);
if (cls) {
tmp[key] = cls;
}
}
}];
genericMapper = tmp;
}
}

// 创建所有的元属性(不包括根类NSObject和NSProxy等属性)
NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
YYClassInfo *curClassInfo = classInfo;
while (curClassInfo && curClassInfo.superCls != nil) { // recursive parse super class, but ignore root class (NSObject/NSProxy)
for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) {
if (!propertyInfo.name) continue;
if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
_YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
propertyInfo:propertyInfo
generic:genericMapper[propertyInfo.name]];
if (!meta || !meta->_name) continue;
if (!meta->_getter || !meta->_setter) continue;
if (allPropertyMetas[meta->_name]) continue;
allPropertyMetas[meta->_name] = meta;
}
curClassInfo = curClassInfo.superClassInfo;
}
if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy;

NSMutableDictionary *mapper = [NSMutableDictionary new];
NSMutableArray *keyPathPropertyMetas = [NSMutableArray new];
NSMutableArray *multiKeysPropertyMetas = [NSMutableArray new];

// 得到自定义数据字典key到mode属性之间的映射关系
if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
NSDictionary *customMapper = [(id <YYModel>)cls modelCustomPropertyMapper];
[customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *mappedToKey, BOOL *stop) {
_YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName];
if (!propertyMeta) return;
[allPropertyMetas removeObjectForKey:propertyName];

if ([mappedToKey isKindOfClass:[NSString class]]) {
if (mappedToKey.length == 0) return;

propertyMeta->_mappedToKey = mappedToKey;
NSArray *keyPath = [mappedToKey componentsSeparatedByString:@"."];
for (NSString *onePath in keyPath) {
if (onePath.length == 0) {
NSMutableArray *tmp = keyPath.mutableCopy;
[tmp removeObject:@""];
keyPath = tmp;
break;
}
}
if (keyPath.count > 1) {
propertyMeta->_mappedToKeyPath = keyPath;
[keyPathPropertyMetas addObject:propertyMeta];
}
propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;

} else if ([mappedToKey isKindOfClass:[NSArray class]]) {

NSMutableArray *mappedToKeyArray = [NSMutableArray new];
for (NSString *oneKey in ((NSArray *)mappedToKey)) {
if (![oneKey isKindOfClass:[NSString class]]) continue;
if (oneKey.length == 0) continue;

NSArray *keyPath = [oneKey componentsSeparatedByString:@"."];
if (keyPath.count > 1) {
[mappedToKeyArray addObject:keyPath];
} else {
[mappedToKeyArray addObject:oneKey];
}

if (!propertyMeta->_mappedToKey) {
propertyMeta->_mappedToKey = oneKey;
propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil;
}
}
if (!propertyMeta->_mappedToKey) return;

propertyMeta->_mappedToKeyArray = mappedToKeyArray;
[multiKeysPropertyMetas addObject:propertyMeta];

propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;
}
}];
}

// 对所有的元属性数据进行整理
[allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
propertyMeta->_mappedToKey = name;
propertyMeta->_next = mapper[name] ?: nil;
mapper[name] = propertyMeta;
}];

// 将整理好的数据赋值给属性
if (mapper.count) _mapper = mapper;
if (keyPathPropertyMetas) _keyPathPropertyMetas = keyPathPropertyMetas;
if (multiKeysPropertyMetas) _multiKeysPropertyMetas = multiKeysPropertyMetas;

_classInfo = classInfo;
_keyMappedCount = _allPropertyMetas.count;
_nsType = YYClassGetNSType(cls);
_hasCustomWillTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomWillTransformFromDictionary:)]);
_hasCustomTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformFromDictionary:)]);
_hasCustomTransformToDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformToDictionary:)]);
_hasCustomClassFromDictionary = ([cls respondsToSelector:@selector(modelCustomClassForDictionary:)]);

return self;
}
  1. 创建一个 YYClassInfo 的实例 classInfo
  2. 如果实现了协议的方法modelPropertyBlacklist,则得到黑名单列表 blacklist
  3. 如果实现了协议的方法modelPropertyWhitelist,则得到白名单列表 whitelist
  4. 如果实现了协议的方法modelContainerPropertyGenericClass,则得到容器类的泛型 genericMapper,映射关系为property -> class
  5. 创建该类以及父类(不包括根类)所有的元属性 _YYModelPropertyMeta
    • 遍历第一步中得到的 classInfo 的 propertyInfos 属性
    • 如果 blacklist 不为空,并且该属性的名字在里面,则 continue
    • 如果 whitelist 不为空,且该属性的名字不在里面,则 continue
    • 创建 _YYModelPropertyMeta 实例 meta,将 YYClassPropertyInfo 信息赋值给 meta,并添加到字典 allPropertyMetas 中,映射关系为property name ->_YYModelPropertyMeta
  6. 创建字典 mapper,其映射关系为mappedToKey -> _YYModelPropertyMeta,mappedToKey 即json字典中的key。
    • 如果实现了协议的方法modelCustomPropertyMapper,得到字典 customMapper,映射关系property name -> json key。为了方便我们把 key 叫做 propertyName, value 叫做 mappedToKey
    • 遍历 customMapper。根据 propertyName 在 allPropertyMetas 中找到对应的 _YYModelPropertyMeta 实例 meta,如果 meta 不为空,则将其移出 allPropertyMetas
    • 如果 mappedToKey 是字符串类型,则将其赋值给 meta 的 _mappedToKey 属性。如果 mappedToKey 的格式使用的 keyPath(类似 @”json.key”),则将该 mappedToKey 使用 @”.” 分割,将分割后等数组赋值给 mata 的 _mappedToKeyPath 属性,并且将 meta 添加到 keyPathPropertyMetas 数组。最后以mappedToKey -> meta的映射将其添加到字典 mapper 中,
    • 如果 mappedToKey 是数组类型,则说明一个属性可能映射了多个json字典的key。此时需要遍历 mappedToKey,它的每一个元素为 oneKey,使用@”.”分割,来判断是否使用了keyPath,分割后得到数组 keyPath。如果 keyPath 的 count 大于1,则将数组 keyPath 添加到 mappedToKeyArray 数组,如果不是则将 oneKey 添加到 mappedToKeyArray,meta 的 _mappedToKey 取值于遍历时第一个 oneKey。当遍历结束,将 mappedToKeyArray 赋值给 meta 的 mappedToKeyArray。最后以mappedToKey -> meta的映射关系将其添加到字典 mapper,将 meta 添加到数组 multiKeysPropertyMetas 中
    • 遍历第五步中的 allPropertyMetas,为了方便我们将字典中的 key 称为 key,value 称为 meta。 将 key 赋值给对应 meta 的 _mappedToKey,并且以property name -> meta的映射添加到 mapper
    • 当一个json字典的key对应着多个属性时,你可以使用 _YYModelPropertyMeta 的 _next来处理 fixme
  7. 最后是为 _YYModelMeta 其它一些属性赋值

从这个函数中我们可以看出,_YYModelMeta 处理了好几种情况下的数据模型转换问题

  1. 当自定义了映射关系,一个属性对应多个 key 时,使用 _multiKeysPropertyMetas 来处理
  2. 当自定义了映射关系,一个 key 对应多个属性时,使用 _YYModelPropertyMeta 的 _next 来处理
  3. _mapper 中包含了 key 跟 属性之间的映射关系
  4. 默认情况下,key 即为属性名,此时使用 _allPropertyMetas 来处理
  5. 通过一些bool值来表明 Class 实现了 》的哪几个方法

通过下图的_YYModelPropertyMeta的属性说明,能帮助更好的理解这一点

属性 说明
_classInfo 对应YYClassInfo实例
_mapper 字典,映射关系:json字典key -> 属性。如果一个key对应多个属性时,_mapper数量会小于属性数量
_allPropertyMetas 所有的_YYModelPropertyMeta实例数组
_keyPathPropertyMetas 使用了keyPath映射的_YYModelPropertyMeta实例数组
_multiKeysPropertyMetas 被多个key映射的_YYModelPropertyMeta实例数组
_keyMappedCount 等同于_mapper的count
_nsType 是 Founddation 的什么类,可能不是
_hasCustomWillTransformFromDictionary 是否实现了协议方法modelCustomWillTransformFromDictionary
_hasCustomTransformFromDictionary 是否实现了协议方法modelCustomTransformFromDictionary
_hasCustomTransformToDictionary 是否实现了协议方法modelCustomTransformToDictionary
_hasCustomClassFromDictionary 是否实现了协议方法modelCustomClassForDictionary

数据模型转换

通过 _YYModelMeta 生成实例的 modelMeta ,我们可以知道json字典跟属性之间的对应关系。所以,接下来要做的就是数据模型之间转换了。

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
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
if (!dic || dic == (id)kCFNull) return NO;
if (![dic isKindOfClass:[NSDictionary class]]) return NO;


_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
if (modelMeta->_keyMappedCount == 0) return NO;

if (modelMeta->_hasCustomWillTransformFromDictionary) {
dic = [((id<YYModel>)self) modelCustomWillTransformFromDictionary:dic];
if (![dic isKindOfClass:[NSDictionary class]]) return NO;
}

ModelSetContext context = {0};
context.modelMeta = (__bridge void *)(modelMeta);
context.model = (__bridge void *)(self);
context.dictionary = (__bridge void *)(dic);


if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
if (modelMeta->_keyPathPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
if (modelMeta->_multiKeysPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
} else {
CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
CFRangeMake(0, modelMeta->_keyMappedCount),
ModelSetWithPropertyMetaArrayFunction,
&context);
}

if (modelMeta->_hasCustomTransformFromDictionary) {
return [((id<YYModel>)self) modelCustomTransformFromDictionary:dic];
}
return YES;
}

首先根据 modelMeta 的 _hasCustomClassFromDictionary 来判断是否自定义了mode的类型,如果是,则获得自定的 mode 类型cls,并根据cls得到对应的 _YYModelMeta 实例 modelMeta。根据 modelMeta 的 _hasCustomWillTransformFromDictionary 来判断是否在转换前json字典进行修改,如果修改了则使用修改后的字典来转换mode。随后生成一个结构体 context,用来存储转换时要用到的信息。

比较 modelMeta 的 _keyMappedCount 与 json字典的 count 之间的大小

  • _keyMappedCount 大于等于字典的 count,则首先遍历字典,对里面的键值对调用ModelSetWithDictionaryFunction方法。如果 modelMeta 的 _keyPathPropertyMetas 和 _multiKeysPropertyMetas 不为空,则对里面的每个元素调用ModelSetWithPropertyMetaArrayFunction方法
  • _keyMappedCount 小于字典的 count,则对 _allPropertyMetas 里面的每个元素调用ModelSetWithPropertyMetaArrayFunction方法

(应该是哪个少遍历哪个,减少开销。。。)


首先让我们看一下上面提到的第一个方法ModelSetWithDictionaryFunction

1
2
3
4
5
6
7
8
9
10
11
12
static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
ModelSetContext *context = _context;
__unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
__unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)];
__unsafe_unretained id model = (__bridge id)(context->model);
while (propertyMeta) {
if (propertyMeta->_setter) {
ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta);
}
propertyMeta = propertyMeta->_next;
};
}

这个方法比较简单,根据 key 在 meta 的 _mapper 中找到相应的 _YYModelPropertyMeta 实例,随后调用ModelSetValueForProperty方法为该属性赋值,这个方法稍后再提。因为存在一个 key 对应多个 属性的情况,所以对该属性赋值后,会沿着 _next 继续为其它对应的属性赋值。


然后再看一下第二个方法ModelSetWithPropertyMetaArrayFunction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) {
ModelSetContext *context = _context;
__unsafe_unretained NSDictionary *dictionary = (__bridge NSDictionary *)(context->dictionary);
__unsafe_unretained _YYModelPropertyMeta *propertyMeta = (__bridge _YYModelPropertyMeta *)(_propertyMeta);
if (!propertyMeta->_setter) return;
id value = nil;

if (propertyMeta->_mappedToKeyArray) {
value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray);
} else if (propertyMeta->_mappedToKeyPath) {
value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath);
} else {
value = [dictionary objectForKey:propertyMeta->_mappedToKey];
}

if (value) {
__unsafe_unretained id model = (__bridge id)(context->model);
ModelSetValueForProperty(model, value, propertyMeta);
}
}

这个方法里主要做了这么几件事情:

  • 找到json字典中的数据
  • 调用ModelSetValueForProperty方法将数据赋值给属性,是的,又是这个方法

如何取值

当调用 ModelSetWithPropertyMetaArrayFunction 方法时,传入了上下文 context(里面包含了我们要用到的json字典context->dictionary)和元属性_YYModelPropertyMeta *propertyMeta
接下来就是分不同的情况来取值了:

  1. 如果有多个key对应同一个属性时,那么会取第一个key且相应json字典中value不为空时的value。例如,有一个json @{@”name” : @”11”, @”title” : @”222”}, 如果有 @”age”, @”name”, @”title” 这几个 key 对应同一个属性 name,那么只会取json中 @”name” 对应的值
  2. 如果使用了 keyPath 来定义属性的映射,那么在json字典中会逐级获取数据(不知道怎么表达了。。。),例如有一个json @{@”info” : @{@”name” : @”111”}}, 并且使用@”info.name”来映射属性,那么首先会取得 @”info”对应的字典 dic,然后再在dic中取得@”name”的值
  3. 如果没有上述两种情况,则直接根据key在json字典中取值

好了,是不是看了感觉还挺简单的,复杂的其实在赋值这一步!

如何赋值

赋值的过程比较复杂,且代码量比较多, 这里就不贴出来了。在这里我简单的分析一下过程:


首先属性是基本数据类型

1
2
3
4
5
if (meta->_isCNumber) {
NSNumber *num = YYNSNumberCreateFromID(value);
ModelSetNumberToProperty(model, num, meta);
if (num) [num class]; // hold the number
}

这个比较简单。首先将从json字典得到的value进行处理,得到一个 NSNumber 类型的数据 num。然后将 num 转换成相应类型的数据,通过objc_msgSend消息发送赋值给该属性。由于在赋值的函数中参数的类型是__unsafe_unretained(类似weak),所以需要在赋值成功前持有该数据,否则程序会因为 num 成为野指针而崩溃,所以在ModelSetNumberToProperty后面还有这样一行代码if (num) [num class];,看似没用,其实还是有点用的。如果你想对__unsafe_unretained了解深一点可以看孙源的这篇博客


然后时属性属于 Foundation 类型时,会先将 value 转换成属性的类型meta->_nsType,然后通过objc_msgSend赋值给属性。
当属性属于数组(NSArray, NSMutableArray)和字典(NSDictionary, NSMutableDictionary)时复杂一点:

  • 数组
    • 如果没有指定泛型,那么直接把value复制给属性
    • 会遍历数组value,将元素转换成相应的泛型,然后添加到一个新数组value中,最后将该value赋值给属性
  • 字典
    • 如果没有指定泛型,那么直接把value复制给属性
    • 会遍历字典values,将value转化成相应的泛型,添加到一个新字典vlues中。最后将该values赋值给属性

属性属于其它的类型,例如自定义的 objc 类,block,c的结构体,联合,数组等,转换过程跟之前也是差不多的。

总结

写了好几天,终于完成了。希望大家看完后对这个库的使用能够有所帮助。在这里可算是帮我解决了个疑问:
在自定义mode中就算为容器类指定了泛型,但转换的时候还是会失败, 原因是我们不能在类型编码中得到泛型的信息…

引用:YYModel

iOS应用 main 执行前发生的事情

这篇是对 iOS 应用启动时,main 函数执行前发生的事的一点总结,限于水平,如有错误请指正~

FAT 二进制

FAT 二进制文件,将多种架构的 Mach-O 文件合并而成。它通过 Fat Header 来记录不同架构在文件中的偏移量,Fat Header 占一页(64位16kb,32位4kb)的空间。
按分页来存储这些 segement 和 header 会浪费空间,但这有利于虚拟内存的实现。

Mach-O 文件

Mach-O为 Mach Object 文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。
在 Mac OS X 系统中使用 Mach-O 作为其可执行文件类型。
它的组成结构如下图所示:

Mach-O 文件结构

每个 Mach-O 文件包括一个 Mach-O Header,然后是一系列的载入命令 load commands,再是一个或多个段(segment),每个段包括0到255个节(section)。Mach-O使用REL再定位格式控制对符号的引用。Mach-O在两级命名空间中将每个符号编码成“对象-符号名”对(所以需要保持 selector 的唯一),在查找符号时则采用线性搜索法。

Mach-O包含了几个 segment,每个 segment 又包含几个 section。segment的名字都是大写的,例如__DATA;section的名字都是小写的, 例如 __text。在 Mach-O 的类型不为MH_OBJECT时,空间大小为页的整数倍。页的大小跟硬件有关,在 arm64 架构一页是16kb,其余为4kb。
section 虽然没有整数倍页大小的限制,但是 section 之间不会有重叠。

Mach-O Header

推荐使用MachOView这个软件查看 Mach-O 的文件结构。注意需要手动关闭 processing,不然会闪退。下面是用 MachOView 查看自己的应用结构:
使用MachOView查看文件结构

东西有点多就没有截取全部。我们查看一下Mach-O Header部分
Header结构

下面是64位架构下header的数据结构:

1
2
3
4
5
6
7
8
9
10
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};

image

Mach-O 全部的 filetype 和 flags 可以在loader.h中找到。


除了MH_OBJECT以外的所有类型,段(Segment)的空间大小均为页的整数倍。页的大小跟硬件有关系,在 arm64 架构下一页为16kb,其它为4kb。

Load commands

Load commands紧跟在头部之后, 当加载过 header 之后,会通过解析Load commands来加载剩下的数据,确定其内存的分布。
下面是 load commands 的结构定义:

1
2
3
4
struct load_command {
uint32_t cmd; /* 载入命令类型 */
uint32_t cmdsize; /* total size of command in bytes */
};

所有load commands的大小即为 Header->sizeofcmds, 共有 Header->ncmds 条load command
load command 以LC开头,不同的加载命令有不同的专有的结构体,cmd 和 cmdsize 是都有的,分别为命令类型(即命令名称),这条命令的长度。这些加载命令告诉系统应该如何处理后面的二进制数据,对系统内核加载器和动态链接器起指导作用。如果当前 LC_SEGMENT 包含 section,那么 section 的结构体紧跟在 LC_SEGMENT 的结构体之后,所占字节数由 SEGMENT 的 cmdsize 字段给出。

cmd(命令名称) 作用
LC_SEGMENT_64 将对应的段中的数据加载并映射到进程的内存空间去
LC_SYMTAB 符号表信息
LC_DYSYMTAB 动态符号表信息
LC_LOAD_DYLINKER 启动动态加载连接器/usr/lib/dyld程序
LC_UUID 唯一的 UUID,标示该二进制文件,128bit
LC_VERSION_MIN_IPHONEOS/MACOSX 要求的最低系统版本(Xcode中的Deployment Target)
LC_MAIN 设置程序主线程的入口地址和栈大小
LC_ENCRYPTION_INFO 加密信息
LC_LOAD_DYLIB 加载的动态库,包括动态库地址、名称、版本号等
LC_FUNCTION_STARTS 函数地址起始表
LC_CODE_SIGNATURE 代码签名信息

注意:不同类型的 segment 会使用不同的函数来加载

Segment

Mach-O 文件中由许多个段(Segment),每个段都有不同的功能,每个段包含了许多个小的Section。LC_SEGMENT意味着这部分文件需要映射到进程的地址空间去,几乎所有 Mach-O 都包含这三个段:

  1. __TEXT:包含了执行代码和其它只读数据(如C 字符串)。权限:只读(VM_PROT_READ),可执行(VM_PROT_EXECUTE)
  2. __DATA:程序数据,包含全局变量,静态变量等。权限:可读写(VM_PROT_WRITE/READ) 可执行(VM_PROT_EXECUTE)
  3. __LINKEDIT:包含了加载程序的”元数据”,比如函数的名称和地址。权限:只读(VM_PROT_READ)

除了上面三个,还有一个常见的 segment:

  • __PAGEZERO:空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对 NULL 指针的引用

LC_SEGMENT_64 的结构定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};

可以看到这里大部分的成员变量都是帮助内核将 segment 映射到虚拟内存的。nsects即表明该段中包含多少个 section,section是具体数据存放的地方。cmdsize表示当前 segment 结构体以及它所包含的所有 section 结构体的总大小。

文件映射的起始位置由fileoff给出,映射到地址空间的vmaddr处。

Section

section 的名字均为小写。section 是具体数据存放的地方,它的结构体跟随在 LC_SEGMENT 结构体之后。在64位环境中它的结构定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};

其中flasg字段储存了两个属性的值:type 和 attributes。type 只能有一个值,而 attributes 的值可以有多个。如果 segment 中任何一个 section 拥有属性 S_ATTR_DEBUG,那么该段所有的 section 都会拥有这个属性。属性详情可以参考loader.h

section name 作用
__text 主程序代码
__stub_helper 用于动态链接的存根
__symbolstub1 用于动态链接的存根
__objc_methname Objective-C 的方法名
__objc_classname Objective-C 的类名
__cstring 硬编码的字符串
__lazy_symbol 懒加载,延迟加载节,通过 dyld_stub_binder 辅助链接
_got 存储引用符号的实际地址,类似于动态符号表
__nl_symbol_ptr 非延迟加载节
__mod_init_func 初始化的全局函数地址,在 main 之前被调用
__mod_term_func 结束函数地址
__cfstring Core Foundation 用到的字符串(OC字符串)
__objc_clsslist Objective-C 的类列表
__objc_nlclslist Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行
__objc_const Objective-C 的常量
__data 初始化的可变的变量
__bss 未初始化的静态变量

虚拟内存

在 segment 的结构体中,我们可以看到vmaddrvmsize两个成员变量,它们分别代表 segment 在虚拟内存中的地址以及大小。

虚拟内存就是一层间接寻址(indirection)。软件工程中有句格言就是任何问题都能通过添加一个间接层来解决。虚拟内存解决的是管理所有进程使用物理 RAM 的问题。通过添加间接层来让每个进程使用逻辑地址空间,它可以映射到 RAM 上的某个物理页上。这种映射不是一对一的,逻辑地址可能映射不到 RAM 上,也可能有多个逻辑地址映射到同一个物理 RAM 上。针对第一种情况,当进程要存储逻辑地址内容时会触发 page fault;第二种情况就是多进程共享内存。

对于文件可以不用一次性读入整个文件,可以使用分页映射(mmap())的方式读取。也就是把文件某个片段映射到进程逻辑内存的某个页上。当某个想要读取的页没有在内存中,就会触发 page fault,内核只会读入那一页,实现文件的懒加载。

也就是说 Mach-O 文件中的__TEXT段可以映射到多个进程,并可以懒加载,且进程之间共享内存。__DATA段是可读写的。这里使用到了Copy-On-Write技术,简称 COW。也就是多个进程共享一页内存空间时,一旦有进程要做写操作,它会先将这页内存内容复制一份出来,然后重新映射逻辑地址到新的 RAM 页上。也就是这个进程自己拥有了那页内存的拷贝。这就涉及到了 clean/dirty page 的概念。dirty page 含有进程自己的信息,而 clean page 可以被内核重新生成(重新读磁盘)。所以 dirty page 的代价大于 clean page。

在多个进程加载 Mach-O 文件时__TEXT__LINKEDIT因为只读,都是可以共享内存的。而__DATA因为可读写,就会产生 dirty page。当 dyld 执行结束后,__LINKEDIT就没用了,对应的内存页会被回收。

dyld

dyld

dyld(the dynamic link editor),Apple 的动态链接器。在内核完成映射进程的工作后会启动dyld,负责加载应用依赖的所有动态链接库,准备好运行所需的一切。
在 App 启动的时候,首先会加载 App 的 mach-o 文件,从 load commands 中得到 dyld 的路径,并且运行。随后 dyld 做的事情顺序概括如下:

  1. 初始化运行环境
  2. 加载主程序执行文件 生成 image, 将image添加到一个全局容器中
  3. 加载共享缓存
  4. 根据依赖链递归加载动态链接库 dylib,如果在缓存中有加载好的 image 则直接拿出来,否则生成一个新的 image,将image添加到一个全局容器中
  5. link 主执行文件
  6. link dylib
    • 根据依赖链递归修正指针 Rebase
    • 根据依赖链递归符号绑定 Bind
  7. 初始化 dylib(runtime 的初始化就在这个时候)

在加载完所有的 dylib 之后,它们处于互相独立的状态,所以还需要将它们绑定起来。代码签名让我们不能修改指令,所以不能直接让一个 dylib 调用另一个 dylib,这时需要很多间接层。
这个时候需要 dyld 来修正指针(rebasing)和绑定符号(binding)。

详细可以查看 dyld 的源码中的_main函数。
下面会分析上述的其中几个步骤。

ImageLoader

ImageLoader是一个将 mach-o 文件里面二进制数据(编译过的代码、符号等)加载到内存的基类,它负责将 mach-o 中的二进制数据映射到内存,它的实例就是我们熟悉的 image。
每一个 mach-o 文件都会有一个对应的 image,实例的类型根据 mach-o 格式的不同也会不同。

  • ImageLoaderMachOClassic: 用于加载__LINKEDIT段为传统格式的 mach-o 文件
  • ImageLoaderMachOCompressed: 用于加载__LINKEDIT段为压缩格式的 mach-o 文件

因为dylib之间有依赖关系,所以系统会沿着依赖链递归加载 image。

Rebasing

dylib的二进制数据会随机的映射到内存的一个随机地址ASLR(Address space layout randomization,)中,这个随机的地址跟代码和数据指向的旧地址(preferred_address)会有一定的偏差,dyld需要修正这个偏差(slide),做法就是将dylib内部的指针地址都加上这个偏移值,偏移值的计算方法如下:

slide = actual_address - preferred_address

随后就是不断的将__DATA段中需要修正的指针加上这个偏移值。
注意:每次程序启动后的地址都会变化,所以指针的地址都需要重新修正。

在 mach-o 的一个载入命令LC_DYLD_INFO_ONLY可以查看到rebase, bind, week_bind,lazy_bind的偏移量和大小。

Binding

binding处理那些指向dylib外部的指针,它们实际上被符号名称(symbol)绑定,也就是个字符串。比如我们 objc 代码中需要使用到 NSObject, 即符号_OBJC_CLASS_$_NSObject,但是这个符号不存在当前的 image 中,而是在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起。

Lazy Binding

lazyBinding就是在加载动态库的时候不会立即 binding, 当时当第一次调用这个方法的时候再实施 binding。 做到的方法也很简单: 通过dyld_stub_binder这个符号来做。lazy binding 的方法第一次会调用到 dyld_stub_binder, 然后 dyld_stub_binder负责找到真实的方法,并且将地址bind到桩上,下一次就不用再bind了。
多数符号都是 lazy binding 的

Runtime

每一个dylib都有自己的初始化方法,当相应的 image 被加载到内存后,就会调用初始化方法。当然这不是调用名为initialize方法,而是C++静态对象初始化构造器,__attribute__((constructor))标记的方法以及Initializer方法。你可以在程序中设置环境变量DYLD_PRINT_INITIALIZERS来打印dylib的初始化方法。

打印信息

我们可以看到程序首先调用了libSystem这个dylib的初始化方法。libSystem是很多系统的lib的集合,包括 libdispatch(GCD), libsystem_c(c语言库), libsystem_blocks(block)。
libSystem的源码init.c中我们可以看到,它的初始化方法libSystem_initializer会调用libdispatch_init();, 然后逐步调用到_objc_init,也就是 objc 和 runtime 的入口。
添加一个符号断点_objc_init,下面是方法调用栈:

断点调试

注意:runtime 和 objc 属于libobjc


下面是_objc_init的实现:

1
2
3
4
5
void _objc_init(void)
{
// 省略...
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

上面的map_images不是将 image 添加到内存中的意思,在这个方法被调用的时候,已经完成了 image 的映射以及指针修正,绑定符号的工作了。
这个函数实际上是将 image 中 OBJC 相关的信息进行初始化,具体实现可以查看_read_image的源码,因为代码太多所以这里就不贴出来了,下面是具体做的事情:

  • 会将所有的 Class 存放在一张映射类名与 Class 的全局表中gdb_objc_realized_classes
  • 随后调用readClass函数将 每一个 Class 添加到gdb_objc_realized_classes表中。
  • 确定 selector 是唯一的
  • read protocols: 读取protocol
  • realizeClasses:这一步的意义就是动态链接好class, 让class处于可用状态,主要操作如下:
    • 检查ro是否已经替换为rw,没有就替换一下。
    • 检查类的父类和metaClass是否已经realize,没有就先把它们先realize
    • 重新layout ivar. 因为只有加载好了所有父类,才能确定ivar layout
    • 把一些flags从ro拷贝到rw
    • 链接class的 nextSiblingClass 链表
    • attach categories: 合并categories的method list、 properties、protocols到 class_rw_t 里面
  • read categories:读取类目

map_images结束会调用load_images函数。这一步做的事情比较少,就是调用我们熟悉的+(load)函数。父类会先调用,除了 Class,每个类目的+(load)方法也会被调用一次,但顺序就不一定了。

总结

在这里对 main 函数之前的操作做一个小总结吧:

  1. 将 App 的 mach-o header 读取到内存中
  2. 根据 load commands 获取 dyld 的路径,运行 dyld
  3. 初始化运行环境,加载 dylib,如果缓存中存在则从缓存中拿出加载过的 image,否则新建一个 image,加载到内存中
  4. 修正指针(rebase),绑定符号(bind)
  5. 初始化 dylib,运行 runtime
  6. runtime 将 image 中有关 OBJC 的数据进行初始化
  7. 调用 +(load) 方法
  8. dyld 调用 main 函数

花了一周的时间用来研究这部分的内容,终于填完坑了很舒服
最大的感受就是学习完后,看 clang 编译后的 C++ 代码能看懂的更多了。比如添加完一个类目之后,会将这个这个类目添加到__DATA的section __objc_catlist中,以前不知道啥意思现在就明白了。也明白 xcode 的许多设置是用来干嘛的,总之好处多多

学习也是一个递归的过程,加油跳出这个递归吧!

引用

iOS 程序 main 函数之前发生了什么
优化 App 的启动时间
dyld源码分析-动态加载load
dyld与ObjC

Objective-C类和对象的内存分布

之前在别人博客下面看到了一个问题,觉得挺有意思的。但是自己想回答的时候又发现好像有一些知识点还不是很熟悉,觉得有点迷糊,所以准备再研究一下底层再来回答问题。现在把这个坑填上吧。

OC对象的指针类型

Objective-C 是一门动态语言,而动态语言是在运行时确定数据类型,变量使用之前不需要类型声明。但是我们在写代码的时候还是要给对象一个类型或者使用id的,我自己觉得这么做是为了通过编译(例如声明了类型为NSObject的实例sark,却调用了方法foo,那么编译就通不过了)。
实际上动态语言的一个特性多态就是这么实现的,即用父类的指针指向子类的实例。

对象的内存分布

还是举个例子会明白一点。需要注意的是需要在模拟器上调试,在真机调试会有问题的。

1
2
3
4
5
6
7
8
9
10
11
@interface Father : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation ViewController
- (void)foo {
Father *father1 = [Father new];
father1.name = @"001";
id father2 = [Father new];
}
@end

调试之前,我们要明白几点常识。在计算机中每个字节都是有一个地址的,每个字节有8个bit,每个bit可以存储1或者0,这8个bit就是这个字节的值。在小端系统中,低位的值存储在低地址上。
使用 x 命令调试。格式:x/<n/f/u> <addr>

  • x 显示内存
  • n 正整数,表示需要显示的内存单元的个数
  • f 表示addr指向的内存内容的输出格式
    • s: 对应输出字符串
    • x: 按十六进制格式显示变量
    • d: 按十进制格式显示变量
    • c: 按字符格式显示变量
  • u 以多少个字节作为一个内存单元
    • b: 1 byte
    • h: 2 bytes
    • w: 4 bytes
    • g: 8 bytes

打断点,然后输入命令: x/8xg father1, 即:以8个字节为一个单元,从 father1 指针的地址开始起8个单元的值

1
2
3
4
5
6
7
8
9
10
11
(lldb) x/8xg father1
0x6000000128f0: 0x000000010be34050(Class) 0x000000010bdcc058(name)
0x600000012900: 0x00006000000128a0 0x0000000100000002
0x600000012910: 0x000000010f8f8e58 0x0000000000000000
0x600000012920: 0x0000000000000000 0x0000000000000000

(lldb) x/8xg father2
0x600000012490: 0x000000010be34050(Class) 0x0000000000000000(name)
0x6000000124a0: 0xbadd2dcdc19dbead 0x00006000000124f0
0x6000000124b0: 0x0000000000000000 0x0000000000000000
0x6000000124c0: 0x00007f8ae3c140c0 0x00006080000092b0

这里我提前将这些地址代表的意思标注好了。
father2虽然是id类型的,但是它跟father1第一个8字节所存储的地址是相同的,都是0x000000010be34050。其实这个地址就是 Father类的地址。我们可以使用下面的方法验证:

1
2
(lldb) po (Class)0x000000010be34050
Father

所以一个实例对象第一个8字节存储的是这个类的指针,那么后面的字节存储的是什么呢?答案是这个实例的成员变量,在上面的例子中我们给实例father1的成员变量name赋值了001, 现在让我们验证一下:

1
2
(lldb) po (id)0x000000010bdcc058
001

因为我们没有对father2的成员变量 name 赋值,所以这8个字节的值是空的。


打开 runtime 750版本源码,查看 id 和 Class 的定义

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
typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
private:
isa_t isa;
}

union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

class_rw_t *data() {
return bits.data();
}
}
  1. id的定义很简单,是一个指向 objc_object 的指针,而 objc_object 只有一个私有成员变量 isa。objc_class 继承于 objc_object,所以你也可以用 id 来声明 Class 的变量,例如id foo = [NSObject class];
  2. isa是一个联合体,里面的 struct 在不同架构的CPU中定义是不同的。在 64 位CPU中,isa 可以用来存储更多的信息,例如引用计数,是否有关联对象等,可以看我的这篇博客Objective-C引用计数原理

使用clang rewrite-objc ViewController.m将代码转化成C++实现,可以看到 Father 这个类变成了如下的结构体

1
2
3
4
5
6
7
struct Father_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
struct NSObject_IMPL {
Class isa;
};

看到这个结构体你是不是就明白了为什么对象的内存分布是下图这个样子的?
需要注意的是,NSObject 的实例虽然理论上只有8个字节,但是它的实例实际上有 16 个字节,后面8个字节是空的。

实例内存分布图

研究到这里,我们就可以回答开头的那个问题了。

  1. 指针的类型是id类型,而指针指向的类型可以是别的类。因为 OC 是动态语言,变量的类型需要在运行时才能够确定。
  2. 指针保存的是对象内存的首地址
  3. 64位平台中,对象首地址开始的8个字节存储的是类的指针。也就是通过这个才能确定该类的类型

是不是很简单!下面继续让我们研究下 Class 的内存分布问题

Class的内存分布

让我们继续回到之前的代码调试。上一节中我们已经知道了Father类的地址了

1
2
3
4
5
6
7
8
9
(lldb) x/16xg 0x000000010be34050
0x10be34050: 0x000000010be34028(meta-class) 0x000000010f8f8e58(superClass)
0x10be34060: 0x00006000000972f0(bucket_t *_buckets) 0x0000000200000003(_mask & _occupied )
0x10be34070: 0x0000600000074302 0x000000010f8f8e08
0x10be34080: 0x000000010f8f8e08 0x000000010f548520
0x10be34090: 0x0000000000000000 0x000000010bdd7df0
0x10be340a0: 0x000000010be34078 0x000000010f8f8e58
0x10be340b0: 0x000000010f548520 0x0000000000000000
0x10be340c0: 0x000000010bdd7e38 0x000000010f8f8e08

PS: 注意不要使用真机来调试,因为我调试的时候发现跳不到那个内存地址中,但在模拟器中没这个问题…

配套的我们把 objc_class 的定义放到下面。

1
2
3
4
5
6
7
8
9
10
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

class_rw_t *data() {
return bits.data();
}
}

因为 objc_class 继承于 objc_object,所以 Class 的第一个8字节还是 isa 指针,也就是一个指向元类(meta-Class)的指针。如果你不知道元类是什么意思的话就去百度,我也懒得讲了。第2个8字节储存的是指向父类的指针。先让我们验证一下

1
2
3
4
5
lldb) po (Class)0x000000010be34028
Father

(lldb) po (Class)0x000000010f8f8e58
NSObject

结论正确。让我们接着看cache_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
}

cache_t关系到方法查找的缓存。当对实例发送消息后,会先到Class的缓存中查找有没有该方法的缓存,如果有则直接调用方法的实现,提高效率。
大致可以看出,bucket_t是一个哈希表,根据_key找到其映射的方法实现_imp,而_key就是 SEL(方法的名字 const char *)。cache_t是中的_mask_occupied是两个4字节的变量,应该代表的是缓存的数量。所以,Class 第三个8字节存储的是bucket_t *类型的指针,第4个8字节保存的是 _mask 和 _occupied。因为是小端,低位地址存储低位的数据,所以 _mask 的值是0x00000003,而 _occupied 的值是0x00000002

接下来看 Class 的第3个成员变量class_data_bits_t bits;

1
2
3
4
5
6
7
8
9
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;

public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}

在64位下,uintptr_t 为8个字节。class_data_bits_t 的公共方法有很多,主要是配合掩码进行一些读写操作。
继续看class_rw_t的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
};

在结构体中,你可以看到有一个成员变量的类型是class_ro_t,是不是很像class_rw_t。从字面意思上可以猜测,一个是readwriite,一个是readonly。因为 OC 是动态语言,可以在运行时添加方法和成员变量,运行时添加的方法或者成员变量就是添加到class_rw_t上的,而class_ro_t存储的是一些编译后Class的信息。
class_data_bits_t的定义中,我们知道了需要掩码FAST_DATA_MASK才能得到 class_rw_t 的地址。下面是 class_rw_t的内存分布

1
2
3
4
5
6
7
8
9
// 得到class_rw_t的内存地址
0x0000600000074302 & 0x00007ffffffffff8 = 0x600000074300;

(lldb) x/16xg 0x600000074300
0x600000074300: 0x00000000800a0000(flags & version) 0x000000010bdd7da8(ro)
0x600000074310: 0x000000010bdd7d18(methods) 0x000000010bdd7d90(properties)
0x600000074320: 0x0000000000000000(protocols) 0x000000010be33f60(firstSubclass)
0x600000074330: 0x000000010ee88c68(nextSiblingClass) 0x0000000000000000(demangledName)
0x600000074340: 0xbadd2dcdc19dbead 0x0000600000074240

因为在代码中我还声明了一个 Father 的子类 Son,没想到在这里出现,没错,就是这个 firstSubclass。至于如果有多个子类,确定哪个是 firstSubclass 我就不清楚了。。。

1
2
(lldb) po (Class)0x000000010be33f60
Son

再来看一下class_ro_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}

然后是它的内存分布:

1
2
3
4
5
6
(lldb) x/16xg 0x000000010bdd7da8
0x10bdd7da8: 0x0000000800000184(flags & instanceStart) 0x0000000000000010(instanceSize & reserved)
0x10bdd7db8: 0x000000010bd3ea79(ivarLayout) 0x000000010bd3eafc(name)
0x10bdd7dc8: 0x000000010bdd7d18(baseMethodList) 0x0000000000000000(baseProtocols)
0x10bdd7dd8: 0x000000010bdd7d68(ivars) 0x0000000000000000(weakIvarLayout)
0x10bdd7de8: 0x000000010bdd7d90(baseProperties) 0x0000002800000081
  1. 可以看到 ro 的成员变量中有instanceStartinstanceSize。这两个值的作用是非脆弱成员变量。即如果基类如果增加了成员变量,不需要重新编译,只需要在初始化系统自动修改instanceStartinstanceSize的值,就能够继续使用子类。具体你可以看我的这篇博客 谈Objective-C类成员变量
  2. ivarLayout 记录了那些是 storng 的ivar
  3. name 存储的是这个类的名字,你可以使用po (char *)0x000000010bd3eafc打印该名字
  4. ivars 存储的是该类的成员变量(不包括关联对象)
  5. weakIvarLayout 记录了哪一些是 weak 的ivar

还可以看到 ro 的baseMethodList和rw的methods的地址都是0x000000010bdd7d18,ro 的baseProperties和rw的properties的地址都是0x000000010bdd7d90

实际上 rw 的三个成员变量,methods, properties, protocols的类型都继承于list_array_tt,这个列表可能有以下3中值:1. 空值 2. 指向列表的指针 3. 指向列表的指针的数组。所以这就是为什么Class可以在类目中添加方法和协议,只需要在这个列表数组中再添加一个指向类目中方法和协议列表的指针就好了。
因为在这个实例中没有使用类目添加方法,所以rw中methods数组仅有一个值,这个值等于ro的baseMethodList。

先来研究methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
}

struct method_list_t {
uint32_t entsizeAndFlags;
uint32_t count;
method_t first;
}

(lldb) x/16xg 0x000000010bdd7d18
0x10bdd7d18: 0x000000030000001a(entsizeAndFlags & count) 0x000000010f547965(name)
0x10bdd7d28: 0x000000010bd41271(types) 0x000000010b7e01e0(imp)
0x10bdd7d38: 0x000000010fd3a28e(name) 0x000000010bd41284(types)
0x10bdd7d48: 0x000000010b7e0180(imp) 0x0000000112f11912(name)
0x10bdd7d58: 0x000000010bd4128c(types) 0x000000010b7e01a0(imp)
0x10bdd7d68: 0x0000000100000020 0x000000010be30c50
0x10bdd7d78: 0x000000010bd19fc8 0x000000010bd4130b
0x10bdd7d88: 0x0000000800000003 0x0000000100000010

entsizeAndFlags 第一个4字节保存的是 entsize 和标记, entsize 我的理解好像是method_t的长度。第二个4字节保存的是方法的数量,在上面的例子中我们可以知道一共保存了3个方法。后面保存了3个method_t的实例,每个实例占用了24个字节。每个 method_t 实例,第一个8字节为 sel,即方法名字;第二个8自己保存了方法的参数类型;第3个8字节是方法的函数指针。我们把上面保存的3个方法的信息按顺序打印出来

  • .cxx_destruct v16@0:8
  • name @16@0:8
  • setName: v24@0:8@16

第2和第3个方法比较好理解,系统为我们自动生成了属性 name 的 getter 和 setter 方法。
第1个方法cxx_destruct 的作用是在delloc时释放该类的成员变量的,具体你可以看这篇博客 探究ARC下dealloc实现

properties 与 methods 类似,因为继承与同一个结构体。这里简单分析一下,内存分布为 entsizeAndFlags(4字节), count(4字节),property_t数组。property_t里面有两个成员变量,一个是属性的名字,一个是属性的属性。。。


大致上这就是 Class 的内存分布了,下面这张图能够简要的概括了:

类的内存分布

引用

ObjectC对象内存布局分析

Objective-C引用计数原理

在 Objective-C 2.0 中,我们无需手动进行内存管理,因为ARC会自动帮我们在编译的时候,在合适的地方对对象进行retainrelease操作。
本文将结合runtime 750版本源码 探究 ARC 环境下引用计数的实现原理。

如何存储引用计数

从 5S 开始,iPhone 都采用了64位架构的处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念,专门用来存储小的对象,例如 NSNumber 和 NSDate。这一类变量本身的值需要占用的内存大小常常不需要8字节,拿整数来说,4个字节所能表示的有符号整数可以达到20多亿(2^31=2147483648,另外 1 位作为符号位),基本可以处理大多数情况。所以我们将一个对象的指针(64位下8字节)拆成两部分,一部分用来存储数据,另一部分作为特殊标记,表示这是一个 Tagged Pointer, 不指向任何一个地址。也就是当某些类使用 Tagged Pointer 来存储数据后,它就不是一个对象了,因为它并没有指向任何地址,变成了一个披着对象皮的普通变量而已,而对于这一类的‘对象’,它的内存是分配在中,由系统分配以及释放,所以它的引用计数也没有意义了,当然你仍然可以使用CFGetRetainCount方法去获取它的引用计数,返回的是它的指针地址。

而在某些平台中(比如arm64),isa 实例的一部分空间也会被用来存储引用计数,当引用计数超过一定值之后,runtime 会使用一张散列表(哈希表)来管理其引用计数;如果不使用 isa 存储引用计数则会直接存储到散列表中。

isa 指针

用64位(8字节)来存储一个内存地址显然是种浪费,于是可以将一部分的空间用来存储引用计数。当 isa 指针第一位为1时即表示使用优化的 isa 指针,这里列出64位环境下的 isa 结构:

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
union isa_t 
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

#if SUPPORT_NONPOINTER_ISA

# if __arm64__
# define ISA_MASK 0x00000001fffffff8ULL
# define ISA_MAGIC_MASK 0x000003fe00000001ULL
# define ISA_MAGIC_VALUE 0x000001a400000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
uintptr_t magic : 9;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};

// SUPPORT_NONPOINTER_ISA
#endif

};

SUPPORT_NONPOINTER_ISA表示是否支持在 isa 指针内添加额外的信息,例如引用计数,析构状态,被__weak变量引用的情况等。目前仅支持 arm64架构的设备支持。

变量名 含义
indexed 0 表示普通的 isa 指针,1 表示可以存储引用计数
has_assoc 表示该对象是否包含 associated object(关联对象)
has_cxx_dtor 表示该对象是否有 C++ 的析构函数
shiftcls 类的指针
magic 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化
weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
deallocating 表示该对象是否正在析构
has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
extra_rc 存储引用计数值减一后的结果

在64位环境下,isa 会存储引用计数,当 has_sidetable_rc 的值为1时,那么溢出的引用计数将会存储在一张全局散列表中,也就是引用计数 = isa保存的引用计数 + 哈希表保存的引用计数 + 1。后面会详细讲到。

哈希表 DenseMap

1
2
3
4
5
6
7
8
9
10
11
12
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

template<typename KeyT, typename ValueT,
bool ZeroValuesArePurgeable = false,
typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap
: public DenseMapBase<DenseMap<KeyT, ValueT, ZeroValuesArePurgeable, KeyInfoT>,
KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable>
{
// ...省略
}

runtime 使用 DenseMap 哈希表(也叫散列表,类似NSDictionary)的别名RefcountMap来存储引用计数。DenseMap 继承于 DenseMapBase 这个 C++ 类,通过观察 DenseMapBase 的内部实现我们可以发现以下几点:

  • 键 KeyT 的类型为DisguisedPtr<objc_object>,这个类是对objc_object *指针及其一些操作进行的封装,目的是不受内存泄漏工具leaks的检测
  • 值 ValueT 的类型为 size_t, size_t在64位环境下等同于 unsigned long。保存的值等于引用计数减一
  • 模板的 KeyInfoT 类型为 DenseMapInfo,在这里等同于DenseMapInfo<DisguisedPtr>。DenseMapInfo 封装了比较重要的方法,用于在哈希表中查找 key 映射的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
struct DenseMapInfo<T*> {
static inline T* getEmptyKey() {
uintptr_t Val = static_cast<uintptr_t>(-1);
return reinterpret_cast<T*>(Val);
}
static inline T* getTombstoneKey() {
uintptr_t Val = static_cast<uintptr_t>(-2);
return reinterpret_cast<T*>(Val);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; }
};

指针哈希算法实现:

1
2
3
4
5
6
7
8
9
#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}
#endif

虽然不完美,但是速度很快(注释说的。。。)


简单来讲,DenseMap 通过对象的指针地址来映射其引用计数

SideTable

1
2
3
4
5
6
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
// ...省略
}

介绍完存储引用计数的哈希表,那么这个哈希表是存储在哪里的呢?
答案是保存在一个叫做SideTable的结构体中,通过观察它的结构组成,我们可以可以看到有三个成员变量slock, refcntsweak_table

  • slock是一个自旋锁,保证线程安全
  • refcnts的类型是 RefcountMap,也就是上一节提到过的 DenseMap 类型的别名。用来保存引用计数
  • weak_table用来保存__weak修饰的指针。当一个对象 delloc 时,通过这个表将这些指向要释放对象的用__weak修饰的指针置为nil,避免野指针的情况出现。

StripedMap

知道引用计数的哈希表是保存在SideTable中,那么SideTable实例保存在哪里呢?
答案是在一个全局的StripedMap<SideTable *>类型的静态变量SideTableBuf

1
2
3
4
5
6
7
8
9
alignas(StripedMap<SideTable>) static uint8_t 
SideTableBuf[sizeof(StripedMap<SideTable>)];

static void SideTableInit() {
new (SideTableBuf) StripedMap<SideTable>();
}
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

之所以在初始化时将 SideTableBuf 定义成 uint8_t 是因为方便计算内存大小,在SideTables()方法中我们可以看到SideTableBuf会被强制转换成StripedMap<SideTable>*类型。实际上 SideTableBuf 也是哈希表,根据指针地址映射到相应的SideTable类型的变量。下面是StripedMap这个类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];

static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
// ...省略
}

StripedMap中有一个PaddedT类型的数组array,在模拟器中容量为64,在真机中为8。PaddedT结构体大小为64个字节,其成员变量 value 的类型实际是我们之前传入 SideTable。当系统调用SideTable& table = SideTables()[]时首先会执行SideTables()得到SideTableBuf, 然后在StripedMap中执行T& operator[] (const void *p)方法获取相应的SideTable。

1
2
3
4
5
6
7
8
T& operator[] (const void *p) { 
return array[indexForPointer(p)].value;
}

static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

indexForPointer()函数中返回相应 SideTable 的index。(addr >> 4) ^ (addr >> 9)这一步我也不是很懂,应该是类似于产生一个随机数,后面的% StripeCount返回一个 [0, StripeCount)的数,也就是相应 SideTable 的index。所以一个 SideTable 应该是对应许多的对象的。


保存引用计数的哈希表保存在SideTable结构体中,而SideTable保存在一个全局的静态变量StripedMap<SideTable> SideTableBuf中。在真机下,SideTableBuf能够储存8个SideTable实例。StripedMap的方法indexForPointer()通过对象的指针计算出相应 SideTable 的 index。一个 SideTable 对应多个对象

获取引用计数

在 ARC 环境下我们可以使用方法CFGetRetainCount得到对象的引用计数。在 runtime 中,通过调用objc_objectrootRetainCount()获取引用计数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
inline uintptr_t 
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}

sidetable_unlock();
return sidetable_retainCount();
}
  1. isTaggedPointer在前面我们已经分析过了如果是Tagged Pointer类型的对象时是怎么样的。此时对象在栈中分配,由系统自动销毁内存(先进后出),所以此时对它求引用计数返回其地址。
    下面让我们重点看一下sidetable_retainCount()这个方法
  2. 当 isa 的 nonpointer = 1 的情况我们开头也分析过了,此时 isa 指针也用来存储引用计数,如果引用计数溢出则将溢出部分存储在哈希表中
  3. 下面让我们研究一下不使用isa优化是怎么从哈希表中获取引用计数的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];

size_t refcnt_result = 1;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
  1. 首先得到 SideTable 实例。
  2. 成员变量 refcnts 就是之前说的保存引用计数的哈希表,在哈希表中根据指针值查找引用计数。
  3. it->second >> SIDE_TABLE_RC_SHIFT 注意result从第三位才开始保存数据,所以需要将数据向右移动2位才能取到引用计数。第1位用来保存该对象是否被用__weak修饰的变量引用,第2位用来表示该对象是否正在析构
  4. 将右移后得到的数+1(refcnt_result)后返回。这也是为什么之前说哈希表保存的引用计数是实际值 -1 之后的值的原因。

Retain

在非 ARC 环境中可以使用retainrelease方法对引用计数进行加减操作,在 ARC 环境中我们无需也无法使用这两个方法操作引用计数,但是你可以使用CFRetain()对对象进行 retain 操作。最终会调用 objc_objectrootRetain方法

1
2
3
4
5
6
7
8
9
inline id 
objc_object::rootRetain()
{
assert(!UseGC);

if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}

类似于上一节中获取引用计数的方法,当对象属于Tagged Pointer时则返回该对象。所以我们接着看sidetable_retain()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();

return (id)this;
}

首先得到 SideTable 实例。从实例中得到存储引用技术的哈希表refcnts,在哈希表中根据对象的地址找到对应的引用计数refcntStorage,判断引用计数的值是否有溢出,如果没有则对引用计数 + 1,返回对象。
上一节我们讲过 refcntStorage 中第三位才开始用来存储引用计数,所以读数时需要先往右边移动两位,那为什么这里的代码没有呢?

1
2
3
4
5
6
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2

注意观察SIDE_TABLE_RC_ONE的定义,是一个8字节的 unsigned long 类型,值为1,向左偏移了两位。refcntStorage += SIDE_TABLE_RC_ONE两者相加的话则直接从第三位开始相加了,所以可以使用 SIDE_TABLE_RC_ONE 对引用计数进行 +1 和 -1 操作。
同样的,在上面的代码中, SIDE_TABLE_RC_PINNED用来判断引用计数值是否有溢出。

Release

release 最终会调用 objc_object的方法rootRelease()

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
inline bool 
objc_object::rootRelease()
{
if (isTaggedPointer()) return false;
return sidetable_release(true);
}

uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

bool do_dealloc = false;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}

在这个方法你可以知道为什么哈希表中保存的引用计数是实际值 -1 之后的值。
it->second < SIDE_TABLE_DEALLOCATING用来判断保存的引用计数值是否小于1,如果小于1的话则对该值标记为正在析构:it->second |= SIDE_TABLE_DEALLOCATING;,并且在随后对该对象发送 delloc 消息。
举个例子,一个对象 sark,实际的引用计数为1,在哈希表中保存的值为0,当这个对象进行release操作后,sark 的引用计数变成了0,也就是需要进行销毁操作了。而到了该方法中,会判断保存的引用计数的值是否小于1,如果是的话则进行 delloc 操作,并且将哈希表中存储的值标记为正在析构状态。而 sark 原先保存着的引用计数值就是 =0,这样设计避免了在哈希表存储的引用计数出现负数的情况。

alloc,new, copy 和 mutableCopy

copy 以及 mutableCopyNSCopyingNSMutableCopying 协议上的方法,需要在各类上自己去实现copyWithZone: mutableCopyWithZone:方法。无论是深拷贝还是浅拷贝都会增加引用计数。

1
2
3
4
5
6
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
+ (id)alloc {
return _objc_rootAlloc(self);
}

[cls alloc]以及[cls allocWithZone:nil]方法最终会调用callAlloc()方法,所以 alloc 和 new 这两个方法后面都会调用callAlloc()这个方法,因为 Objective-C 2.0 忽视垃圾回收和 NSZone,那么后续的调用顺序依次是为:

1
2
3
4
callAlloc()
class_createInstance()
_class_createInstanceFromZone
calloc()

calloc()函数相比于malloc()函数的优点是它将分配的内存区域初始化为0,相当于malloc()后再用memset()方法初始化一遍。

单例

其实这一节是对上一节内容的补充。
记得我刚出来工作的时候,单例是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation Son
+ (instancetype)shareManager
{
static Son *son;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
son = [super allocWithZone:nil];
});
return son;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [self shareManager];
}
@end

当时组长问我为什么要这样子写(因为跟他们写的方式不一样),我也答不上来,因为这种代码都是直接google的。但是看了callAlloc()实现之后我明白为什么了。
在上一节我们已经知道了 alloc 和 new 都会接着调用callAlloc()

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
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif

// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}

如果类重载了allocWithZone方法,那么cls->ISA()->hasCustomAWZ()将会返回YES,也就是说当我们用alloc或者new创建实例的时候,就不会走系统的方法,而会走重载的allocWithZone方法了。我们在重载allocWithZone方法时返回[self shareManager](注意此时的self代表Son类), 因为shareManager方法返回的是一个静态变量。

还有一个需要注意的点就是在shareManager中,我们使用son = [super allocWithZone:nil];初始化实例,为什么不使用son = [[super alloc] init];来初始化呢?
代码中的[super alloc];在编译后会变成objc_msgSendSuper(objc_super super, @selector(alloc))(大致意思是这样)。其中objc_super是一个结构体,只有两个成员变量id receiverClass class,receiver 仍是 self(Son类), class 为 Father类。当我们想通过[super alloc]创建实例的时候,会从 Father类中查找 +alloc 方法,如果没有实现则在 NSObject 中查找 +alloc 方法。而方法里面的参数 self 仍旧为 Son 类而不是 Father 类,所以还是会去调用重载的allocWithZone方法,导致死循环。

引用

Objective-C 引用计数原理

谈Objective-C关联对象

#前言

前不久刚写了 谈Objective-C类成员变量 ,分析了成员变量的实现原理以及不能动态添加的原因,在这篇文章里我们来根据 objc4-646.tar.gz版本 源码来谈一下 Objective-C 关联对象的实现原理。

关联对象(Associated Objects)是 Objective-C 2.0运行时的一个特性,起始于OS X Snow Leopard和iOS 4。它允许开发者对已经存在的类在扩展中添加自定义的属性。相关参考可以查看 <objc/runtime.h> 中定义的三个允许你将任何键值在运行时关联到对象上的函数:

  • void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) 用于给对象添加关联属性,传入nil则移除已有的关联对象
  • id objc_getAssociatedObject(id object, const void *key) 用于获取关联属性
  • void objc_removeAssociatedObjects(id object) 移除一个对象所有的关联属性,但不建议手动调用这个函数,因为这可能会导致其它人对其添加的属性也被移除了。你可以调用objc_setAssociatedObject方法并传入nil来指定移除某个关联

下面分析一下 objc_setAssociatedObject 两个参数 keypolicy

#key

通常来说该属性应该是常量、唯一的,在getter和setter方法中都可以访问到。这里有两种常见的添加方式:

第一种是添加 static char 类型的变量,当然更推荐是指针型的。

1
2
3
4
5
static char kAssociatedObjectKey;
- (void)setMenber:(NSString *)menber {
objc_setAssociatedObject(self, &kAssociatedObjectKey, menber, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

当然更推荐的是使用更简单的方式实现:用 selector(getter方法):

1
2
3
- (void)setMenber:(NSString *)menber {
objc_setAssociatedObject(self, @selector(menber), menber, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

#关联策略 policy

关联策略跟属性修饰符的使用方法差不多,属性可以根据定义在 objc_AssociationPolicy 上的类型被关联到对象上:

关联策略 等价属性 说明
OBJC_ASSOCIATION_ASSIGN @property (assign)或 @property (unsafe_unretained) 弱引用关联对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 强引用关联对象,且为非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 复制关联对象,且为非原子操作
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 强引用关联对象,且为原子操作
OBJC_ASSOCIATION_COPY @property (atomic, copy) 复制关联对象,且为原子操作

#关联对象实现

下面让我们具体来分析一下这几个函数的具体实现吧!

分析objc_setAssociatedObject实现

objc_setAssociatedObject的实现被定义在objc-auto.mm文件 467 行

1
2
3
4
5
6
7
8
9
10
GC_RESOLVER(objc_setAssociatedObject)

#define GC_RESOLVER(name) \
OBJC_EXPORT void *name##_resolver(void) __asm__("_" #name); \
void *name##_resolver(void) \
{ \
__asm__(".symbol_resolver _" #name); \
if (UseGC) return (void*)name##_gc; \
else return (void*)name##_non_gc; \
}
  • ## 符号: 连接宏。举个例子:#define COMMAND(A, B) A##B , int COMMAND(temp, Int) = 10 等同于 int tempInt = 10
  • UseGC 是否使用垃圾回收,在 iPhone 平台上被定义为 NO
    所以这个宏展开来为下面的代码
1
2
3
4
void GC_RESOLVER(name)                                 
{
return (void*)objc_setAssociatedObject_non_gc();
}

objc_setAssociatedObject_non_gc的实现在objc-runtime.m文件,再经过一些跳转,可以发现 objc_setAssociatedObject 最终会调用 _object_set_associative_reference方法 (objc-runtime.m 268行)

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
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
  • AssociationsManager manager;, 会创建一个AssociationsManager结构体的变量 manager,在调用它的构造函数时会上锁,调用析构函数时解锁。结构体内有一个静态变量 AssociationsHashMap, 懒加载该变量。
  • DISGUISE(object) 用来获取 object 的指针地址
  • AssociationsHashMap是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射
  • ObjectAssociationMap 是一个map,维护了从 key 到 ObjcAssociation 的映射
  • ObjcAssociation 是一个 C++ 类, 主要包括两个成员变量:uintptr_t _policy(关联策略) id _value(关联对象的值)

简单的讲解上面那个函数的流程:

  1. 新建一个 AssociationsManager 实例 manager,同时上锁。通过 manager 得到 AssociationsHashMap 关联哈希表 associations,通过 DISGUISE()函数得到 object 的指针 disguised_object。在哈希表 associations 中 根据 disguised_object 查找 ObjectAssociationMap,如果没有则新建一个 refs。
  2. 新建一个 ObjcAssociation 实例 new_association,存储在 refs 中
  3. 如果传入的value是nil,则在 refs 移除该映射关系
  4. 释放掉旧的 old_association
  5. 作用域结束释放掉 manager,解锁

添加关联对象流程图

分析objc_getAssociatedObject实现

按照上一节的流程,我们首先找到 objc_getAssociatedObject 的最终实现源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
}
return value;
}

代码量比上一节少了还挺多哈,过程也类似,就不讲的很细了

  1. 先得到 AssociationsHashMap 实例 associations(静态变量)。根据 object 的指针地址,在 associations 得到映射的 ObjectAssociationMap refs。
  2. 在 refs 根据 key 得到映射的 ObjcAssociation 实例 entry,在 entry 中可以得到成员变量 _value,也就是我们所关联属性的值。
  3. 根据关联策略 policy 进行相应的操作(autorelease, retain)后返回 value

分析objc_removeAssociatedObjects实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}

其实不看代码应该也能够猜出个大概了吧.

  1. 根据 object地址 找到映射的 refs,遍历 refs,将保存着的 value 保存在 vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements
  2. 删除 refs, 然后一个个的释放 elements 里面的值

#给类对象关联对象

看完源代码后,我们知道实例对象地址与 ObjectAssociationMap map是一一对应的。那么是否可以给类对象添加关联对象呢?
答案是可以,因为Class也是一个对象,我们完全可以用同样的方式给类对象添加关联对象,只不过我们一般情况下不会这样做,因为更多时候可以通过 static 变量来实现类级别的变量。

你可以通过下面的代码这样操作

1
2
3
4
5
6
7
8
9
10
11
12
@implementation NSObject (AssociatedObject)
+ (NSString *)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}
+ (void)setAssociatedObject:(NSString *)associatedObject {
objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

- (void) foo {
NSObject.associatedObject = @"associatedObject";
}

#何时释放关联对象

探究ARC下dealloc实现 中我们研究过,当对象引用计数变为0时会调用 dealloc 方法,然后最终调用 objc_destructInstance 方法来执行释放所有__weak修饰的指向该对象的指针,释放关联对象,释放该对象成员变量的操作

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
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}
return obj;
}

void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}

是不是有点熟悉呢,在上上节中我们刚刚分析过这个方法。当对象 dealloc 时,会自动调用 objc_removeAssociatedObjects 方法来释放所有的关联对象。

#总结一下

  • 类实例跟关联对象(关联的属性)并没有直接的存储关系,关联对象在创建时后存储在一个静态哈希表中,根据类实例的指针映射到该关联对象
  • 当类实例 dealloc 后,会从哈希表中释放该实例的所有的关联对象
  • 关联对象的关联策略跟属性的修饰符非常的相似,要合理使用避免 crash
  • 比起其他解决问题的方法,关联对象应该被视为最后的选择

#引用

谈Objective-C类成员变量

#我是前言

Objective-C 是一门动态语言,所以它总是将一些决定工作从编译延迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统来执行编译后的代码。这就是 runtime 存在的意义,它是 Objective-C 框架的一块基石。
runtime 有两个版本:modeen 和 leagcy,我们现在使用的是 modern 版的。
本文 runtime 源码为objc4-646.tar.gz版本

在老版本的 runtime 中,如果修改了基类的成员变量布局(比如增加成员变量),子类需要重新编译。

父类NSObject,子类MyObject成员变量布局

如果苹果发布了新的 iOS SDK,NSObject 增加了几个成员变量,那么我们原先的代码将无法运行。因为 MyObject 成员变量布局在编译时就确定了,父类新增的成员变量的地址跟子类成员变量的内存区域重叠了。此时,我们只能重新编译 MyObject 的代码,程序才能在新版本系统上运行。如果 MyObject 存在于别人编写的静态库,那我们只能希望作者快点发布新版本了。

新版本后NSObject,MyObject的成员变量布局

非脆弱[Non-fragile]实例变量是新版 Objective-C 的一个新功能,应用于iPhone和64位Mac上。它们提供给框架开发者更多的灵活性,且不会失去二进制的兼容性

非脆弱成员变量

#如何寻址成员变量

点开 runtime 的源码,让我们找到 ivar 的定义:

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
typedef struct objc_class *Class;
typedef struct objc_object *id;

// 类实例
struct objc_object {
private:
isa_t isa;
// ...省略
}

union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
// ...省略
}

// 类定义
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
// ...省略
}

struct class_data_bits_t {

// ...省略
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// ...省略
}

struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;
// ...省略
}

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
const method_list_t * baseMethods;
const protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
const property_list_t *baseProperties;
};

image

  • 每个 OC 类实例实际上都是一个内存上指向objc_object结构体的指针,成员变量 isa 有指向objc_class结构体的指针Class cls;
  • class_ro_t结构体中可以找到成员变量 const ivar_list_t * ivars,这个就是存储类所有成员变量的列表
  • class_ro_t结构体中成员变量const uint8_t * ivarLayout;const uint8_t * weakIvarLayout;的作用可以看一下孙源的这篇博客
1
2
3
4
@interface MyObject : NSObject {
NSString *_age;
}
@end

使用 clang -rewrite-objc MyObject.h 将代码转化成 C++ 实现,你可以看到编译后的 MyObject 实例的内存布局:

1
2
3
4
5
6
7
struct MyObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *__strong _age;
};
struct NSObject_IMPL {
__unsafe_unretained Class isa;
};

类实例内存布局

ivar_list_t 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct ivar_list_t {
uint32_t entsize;
uint32_t count;
ivar_t first;
};

struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// ...省略
};

我们可以看到ivar_t有名为offset的成员变量,这个就是成员变量在对象中的位置偏移量。在应用启动时,如果父类size变大时,runtime 会通过修改 offset,更新成员变量的偏移量,来正确的找到成员变量的地址。


1
2
3
4
5
6
7
8
9
10
11
@interface MyObject : NSObject {
NSString *_age;
}
@end

@implementation MyObject
- (void)test
{
self -> _age = @"hhh";
}
@end

使用命令行clang -F -cc1 -S -emit-llvm -fblocks MyObject.m,将代码编译成 IR(intermediate representation)。
注意要加*-F*,好多人的博客里面都少了这个标志,会报错。在 stackoverflow 找到答案。
下面是编译后的代码:

1
2
3
4
5
6
7
8
@"OBJC_IVAR_$_MyObject._age" = hidden global i64 8, section "__DATA, __objc_ivar", align 8
// ...
%6 = load i64, i64* @"OBJC_IVAR_$_MyObject._age", align 8, !invariant.load !8
%7 = bitcast %0* %5 to i8*
%8 = getelementptr inbounds i8, i8* %7, i64 %6
%9 = bitcast i8* %8 to %1**
store %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to %1*), %1** %9, align 8

可以简化成如下的代码

1
2
3
int32_t g_ivar_MyClass_age = 8;  // 全局变量
*(NSString *)((uint8_t *)obj + g_ivar_MyObject_age) = @"hhh";

  • 编译时,LLVM 为每各类的每一个成员变量定义一个全局变量,用于存储该成员变量的偏移量
  • 根据成员变量的偏移量,可以直接找到成员变量的地址并赋值

这也是为什么结构体ivar_t的成员变量offsetint32_t *类型,因为保存的是该全局变量的地址。

#Non Fragile ivars

在前面部分我们已经知道该如何寻址成员变量,那么当基类的size变化时,runtime 是如何更新子类成员变量的offset呢?

在应用程序启动后,main 函数执行之前,runtime 在加载类的时候,会使用static Class realizeClass(Class cls)函数对类进行初始化,分配其读写数据的内存,返回类的真实结构

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
/* realizeClass
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
*/
static Class realizeClass(Class cls) {
class_rw_t *rw = cls->data();
//...省略
if (ro->instanceStart < super_ro->instanceSize) {
// Superclass has changed size. This class's ivars must move.
// Also slide layout bits in parallel.
// This code is incapable of compacting the subclass to
// compensate for a superclass that shrunk, so don't do that.
class_ro_t *ro_w = make_ro_writeable(rw);
ro = rw->ro;
moveIvars(ro_w, super_ro->instanceSize,
mergeLayouts ? &ivarBitmap : nil,
mergeLayouts ? &weakBitmap : nil);
gdb_objc_class_changed(cls, OBJC_CLASS_IVARS_CHANGED, ro->name);
layoutsChanged = YES;
}
// ...省略
}

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
const ivar_list_t * ivars;
// ...省略
};

  • rw 是当前类的可读数据,ro 是类的 Ivar Layout,ro 的结构体定义在上面
  • 在初始化类时,如果父类 ro 的instanceSize比子类的instanceStart大的话,那么会调用moveIvars函数更新子类的instanceSize以及子类成员变量的偏移量

再让我们看一下 moveIvars 的源码:

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
/***********************************************************************
* moveIvars
* Slides a class's ivars to accommodate the given superclass size.
* Also slides ivar and weak GC layouts if provided.
* Ivars are NOT compacted to compensate for a superclass that shrunk.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
static void moveIvars(class_ro_t *ro, uint32_t superSize,
layout_bitmap *ivarBitmap, layout_bitmap *weakBitmap)
{
rwlock_assert_writing(&runtimeLock);

uint32_t diff;
uint32_t i;

assert(superSize > ro->instanceStart);
diff = superSize - ro->instanceStart;

if (ro->ivars) {
// Find maximum alignment in this class's ivars
uint32_t maxAlignment = 1;
for (i = 0; i < ro->ivars->count; i++) {
ivar_t *ivar = ivar_list_nth(ro->ivars, i);
if (!ivar->offset) continue; // anonymous bitfield

uint32_t alignment = ivar->alignment();
if (alignment > maxAlignment) maxAlignment = alignment;
}

// Compute a slide value that preserves that alignment
uint32_t alignMask = maxAlignment - 1;
if (diff & alignMask) diff = (diff + alignMask) & ~alignMask;

// Slide all of this class's ivars en masse
for (i = 0; i < ro->ivars->count; i++) {
ivar_t *ivar = ivar_list_nth(ro->ivars, i);
if (!ivar->offset) continue; // anonymous bitfield

uint32_t oldOffset = (uint32_t)*ivar->offset;
uint32_t newOffset = oldOffset + diff;
*ivar->offset = newOffset;

if (PrintIvars) {
_objc_inform("IVARS: offset %u -> %u for %s (size %u, align %u)",
oldOffset, newOffset, ivar->name,
ivar->size, ivar->alignment());
}
}

// Slide GC layouts
uint32_t oldOffset = ro->instanceStart;
uint32_t newOffset = ro->instanceStart + diff;

if (ivarBitmap) {
layout_bitmap_slide(ivarBitmap,
oldOffset >> WORD_SHIFT,
newOffset >> WORD_SHIFT);
}
if (weakBitmap) {
layout_bitmap_slide(weakBitmap,
oldOffset >> WORD_SHIFT,
newOffset >> WORD_SHIFT);
}
}

*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;

if (!ro->ivars) {
// No ivars slid, but superclass changed size.
// Expand bitmap in preparation for layout_bitmap_splat().
if (ivarBitmap) layout_bitmap_grow(ivarBitmap, ro->instanceSize >> WORD_SHIFT);
if (weakBitmap) layout_bitmap_grow(weakBitmap, ro->instanceSize >> WORD_SHIFT);
}
}
  • 首先计算 superSize 与 instanceStart 之间的差值 diff
  • 得到结构体中最大的成员变量的size:maxAlignment, 然后赋值:alignMask = maxAlignment - 1
  • 比较 diff 和 alignMask,通过算法 if (diff & alignMask) diff = (diff + alignMask) & ~alignMask; 对diff重新赋值

编译器在给结构体开辟空间时,首先找到结构体中最大的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。
为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。了解更多可以看这篇博客

  • 更新成员变量的 offset,ivar.newOffset = diff + ivar.oldOffset
  • 更新子类 ro 的 instanceStart 和 instanceSize,ro.newinstanceStart = ro.oldinstanceStart + diff,ro.newinstanceSize = ro.oldinstanceSize + diff
  • 当父类变大时会调用该函数来移动子类ivar,当父类变小时则子类ivar不变化

通过这个函数,即使父类size变大了,我们还是可以通过子类的 ro.instanceStart + ivar.offset 访问到成员变量

#不能动态添加成员变量

在 runtime 中有一个函数 class_addIvar()可以为类添加成员变量, 下面是该方法的一部分注释:

his function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.

上面的大致意思是该函数只能在类注册之前使用,且不能为元类添加成员变量。

让我们设想一下如果 OC 允许动态增加成员变量:

1
2
3
4
5
6
7
8
9
@interface Father : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@end

@interface Son : Father
@property (nonatomic, copy) NSArray *toys;
@end

当Father初始化之后,instanceStart,instanceSize,offset已经确定
为 Father 添加新的成员变量 sex,则使用 Son 的实例对象 son 会出错误,因为 son.instanceStart < Father.instanceSize,即 father 成员变量的 sex 的内存区域会跟 son 的一部分重合

我们有时会在类目中动态的为类添加关联对象(添加对象),为什么可以添加关联对象呢?
具体的你可以看一下我的另一篇博客 谈Objective-C关联对象
这里我简单解释一下:关联对象被保存在一个静态的 map 中,以类实例的指针地址为映射,而不是保存在类实例的结构体中,不影响对象中成员变量的使用,所以可以在运行时添加成员变量。

#引用

Objective-C类成员变量深度剖析

探究ARC下dealloc实现

我是前言

目前正在看 oc 底层的东西,看了许多大牛的博客,发现有一些小问题:

  • runtime 的版本可能跟作者当时写的版本不一致
  • 许多方法一笔带过,因为基础知识的薄弱看不懂。。。
  • 没有标明苹果文档的出处

所以我打算解决上面的一些问题,然后重新发一版,当然大部分的内容还是原作者写的 。runtime 的源码为 objc4-646.tar.gz版本

进入正题

在 ARC 环境下,我们不需要主动的调用系统的析构函数 dealloc 就能够完成将对象以及父类的成员变量内存释放掉的操作:

1
2
3
4
5
6
7
- (void)dealloc
{
// ... //
// 非Objc对象内存的释放,如CFRelease(...)
// ... //
}

问题来了:

  1. 这个对象成员变量(ivars)的释放操作去哪儿了?
  2. 没有主动调用 [super dealloc],那么是什么时候调用这个方法的?

ARC文档中对dealloc过程的解释

clang ARC文档

A class may provide a method definition for an instance method named dealloc. This method will be called after the final release of the object but before it is deallocated or any of its instance variables are destroyed. The superclass’s implementation of dealloc will be called automatically when the method returns.

大概意思是:dealloc 方法在最后一次 release 后被调用,但此时实例变量(ivars)并未释放,父类的dealloc的方法将在子类dealloc方法返回后自动调用

The instance variables for an ARC-compiled class will be destroyed at some point after control enters the dealloc method for the root class of the class. The ordering of the destruction of instance variables is unspecified, both within a single class and between subclasses and superclasses.

ARC下对象的实例变量在根类 [NSObject dealloc] 中释放(通常root class都是NSObject),变量释放顺序各种不确定(一个类内的不确定,子类和父类间也不确定,也就是说不用care释放顺序)


所以,我们不需要主动调用 [super dealloc] ,系统会自动调用,后面我们再讲这是怎么实现的。接下来我们来探究在根类 NSObject 析构时发生了什么

NSObject的析构过程

通过 runtime 源码,我们可以发现 NSObject 调用 dealloc 时会调用 _objc_rootDealloc(NSObject.mm 2071行) 继而调用object_dispose(objc-object.h 301行) 随后调用objc_destructInstance(objc-runtime-new.mm 6838行), 下面讲一下rootDealloc objc_destructInstance函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
inline void objc_object::rootDealloc()
{
assert(!UseGC);
if (isTaggedPointer()) return;

if (isa.indexed &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor)
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}

64位下,isa 指针的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
uintptr_t magic : 9;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
// ...
  • indexed(1 bit) 0 表示普通的 isa 指针,1 表示使用优化,即Tagged Pointer存储引用计数
  • has_assoc(1 bit) 表示该对象是否包含 associated object,如果没有,则析构(释放内存)时会更快
  • has_cxx_dtor(1 bit) 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构(释放内存)时更快
  • shiftcls(30 bits) 类的指针
  • magic(9 bits) 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。
  • weakly_referenced(1 bit) 表示该对象是否有过 weak 对象,如果没有,则析构(释放内存)时更快
  • deallocating(1 bit) 表示该对象是否正在析构
  • has_sidetable_rc(1 bit) 表示该对象的引用计数值是否过大无法存储在 isa 指针
  • extra_jc(19 bits) 表示引用计数值减一后的结果。例如,如果对象引用计数为4,则extra_jc为3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}

return obj;
}

objc_destructInstance干了三件事情:

  1. 执行了一个 object_cxxDestruct 函数
  2. 执行_object_remove_assocations函数去除和这个对象 assocate 的对象(常用于类目中添加的属性 )
  3. 执行clearDeallocating, 清空引用计数并清除弱引用表,将所有使用__weak修饰的指向该对象的变量置为nil

所以,ARC 自动释放实例变量的地方就在 object_cxxDestruct 这个方法里面没跑了。

探究 object_cxxDestruct

上面找到的名为object_cxxDestruct的方法最终成为下面的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);

// Call cls's dtor first, then superclasses's dtors.

for ( ; cls != NULL; cls = _class_getSuperclass(cls)) {
if (!_class_hasCxxStructors(cls)) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_internal) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
_class_getName(cls));
}
(*dtor)(obj);
}
}
}

代码的大致意思是通过继承链(isa)向上递归调用 SEL_cxx_destruct这个函数的函数实现
这篇文章提到:

ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

和《Effective Objective-C 2.0》中的:

When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

可以了解到cxx_destruct方法原本是为了 C++ 对象析构的,ARC 借用了这个方法插入代码实现了自动释放的工作。

通过实验找出 .cxx_destruct

1
2
3
4
5
6
7
8
@interface Father : NSObject
@property (nonatomic, copy) NSString *name;
@end

@interface Son : Father
@property (nonatomic, copy) NSArray *toys;
@end

只有两个简单的属性,找个地方写简单的测试代码:

1
2
3
4
5
6
7
8
9
// start
{
// before new
Son *son = [Son new];
son.name = @"sark";
son.toys = @[@"sunny", @"xx"];
// after new
}
// gone

当过了大括号的作用域,son 对象就会被释放。所以在after new这行son对象初始化完成,在gone这行son对象被dealloc。

本次实验使用 NSObject+DLIntrospection 这个扩展来作用调试工具,通过它可以轻松打出一个类的方法,成员变量等。
将这个扩展引入工程,在 after new 处设置一个断点,在这里打印出 Son 类所有的方法名:

1
2
3
4
5
6
7
po [[Son class] instanceMethods]
<__NSArrayI 0x280982520>(
- (void)setToys:(id)arg0 ,
- (id)toys,
- (void).cxx_destruct
)

发现出现了.cxx_destruct这个方法,经过几次实验,发现:

  1. 只有在ARC下这个方法才会出现(试验代码的情况下)
  2. 只有当前类拥有实例变量时(不论是不是用property)这个方法才会出现,且父类的实例变量不会导致子类拥有这个方法
  3. 出现这个方法和变量是否被赋值,赋值成什么没有关系

使用 watchpoint 定位内存释放时刻

依然在 after new 断点处,输入 lldb 命令:

1
2
watchpoint set variable son->_name

name的变量加入watchpoint,当这个变量被修改时会触发trigger:
从中可以看出,在这个时刻,_name 从 0x0000000104ac5048 变成了0x0000000000000000,也就是nil,赶紧看下调用栈:
发现果然跟到了.cxx_destruct方法,而且是在objc_storeStrong方法中释放

刨根问底.cxx_destruct

知道了ARC环境下,对象实例变量的释放过程在 .cxx_destruct 内完成,但这个函数内部发生了什么,是如何调用 objc_storeStrong 释放变量的呢?
从上面的探究中知道,.cxx_destruct 是编译器生成的代码,那它很可能在clang前端编译时完成,这让我联想到clang的Code Generation,因为之前曾经使用clang -rewrite-objc xxx.m时查看过官方文档留下了些印象,于是google:
.cxx_destruct site:clang.llvm.org

结果发现clang的 doxygen 文档中 CodeGenModule 模块正是这部分的实现代码,cxx相关的代码生成部分源码在
http://clang.llvm.org/doxygen/CodeGenModule_8cpp-source.html
位于1827行,删减掉离题部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// EmitObjCIvarInitializations - Emit information for ivar initialization
/// for an implementation.
void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D)
{
DeclContext* DC = const_cast<DeclContext*>(dyn_cast<DeclContext>(D));
assert(DC && "EmitObjCIvarInitializations - null DeclContext");
IdentifierInfo *II = &getContext().Idents.get(".cxx_destruct");
Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
ObjCMethodDecl *DTORMethod = ObjCMethodDecl::Create(getContext(),
D->getLocation(),
D->getLocation(), cxxSelector,
getContext().VoidTy, 0,
DC, true, false, true,
ObjCMethodDecl::Required);
D->addInstanceMethod(DTORMethod);
CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, DTORMethod, false);
}

这个函数大概作用是:获取到 .cxx_destruct 的selector,创建 Method,然后加入到这个类的方法列表中,最后一行的调用才是真的创建这个方法的实现。这个方法位于http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 1354行,包含了构造和析构的 cxx 方法,继续跟随 .cxx_destruct,最终调用 emitCXXDestructMethod 函数,代码如下:

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
static void emitCXXDestructMethod(CodeGenFunction &CGF, ObjCImplementationDecl *impl)
{
CodeGenFunction::RunCleanupsScope scope(CGF);

llvm::Value *self = CGF.LoadObjCSelf();

const ObjCInterfaceDecl *iface = impl->getClassInterface();
for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin(); ivar; ivar = ivar->getNextIvar())
{
QualType type = ivar->getType();

// Check whether the ivar is a destructible type.
QualType::DestructionKind dtorKind = type.isDestructedType();
if (!dtorKind) continue;

CodeGenFunction::Destroyer *destroyer = 0;

// Use a call to objc_storeStrong to destroy strong ivars, for the
// general benefit of the tools.
if (dtorKind == QualType::DK_objc_strong_lifetime) {
destroyer = destroyARCStrongWithStore;

// Otherwise use the default for the destruction kind.
} else {
destroyer = CGF.getDestroyer(dtorKind);
}

CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);
CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
cleanupKind & EHCleanup);
}

assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
}

分析这段代码以及其中调用后发现:它遍历当前对象所有的实例变量(Ivars),调用objc_storeStrong,从clang的ARC文档上可以找到 objc_storeStrong 的示意代码实现如下:

1
2
3
4
5
6
void objc_storeStrong(id *object, id value) {
id oldValue = *object;
value = [value retain];
*object = value;
[oldValue release];
}

在 .cxx_destruct 进行形如 objc_storeStrong(&ivar, null) 的调用后,这个实例变量就被release和设置成nil了

自动调用[super dealloc]的实现

按照上面的思路,自动调用 [super dealloc] 也一定是 CodeGen 的工作了,位于http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 492行 StartObjCMethod 方法中:

1
2
if (ident->isStr("dealloc"))
EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind());

上面代码可以得知在调用dealloc方法时被插入了代码,由FinishARCDealloc结构定义:

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
struct FinishARCDealloc : EHScopeStack::Cleanup {
void Emit(CodeGenFunction &CGF, Flags flags) override {
const ObjCMethodDecl *method = cast<ObjCMethodDecl>(CGF.CurCodeDecl);

const ObjCImplDecl *impl = cast<ObjCImplDecl>(method->getDeclContext());
const ObjCInterfaceDecl *iface = impl->getClassInterface();
if (!iface->getSuperClass()) return;

bool isCategory = isa<ObjCCategoryImplDecl>(impl);

// Call [super dealloc] if we have a superclass.
llvm::Value *self = CGF.LoadObjCSelf();

CallArgList args;
CGF.CGM.getObjCRuntime().GenerateMessageSendSuper(CGF, ReturnValueSlot(),
CGF.getContext().VoidTy,
method->getSelector(),
iface,
isCategory,
self,
/*is class msg*/ false,
args,
method);
}
};

上面代码基本上就是向父类转发dealloc的调用,实现了自动调用[super dealloc]方法。

总结

  • ARC下对象的成员变量在编译器插入的.cxx_desctruct方法自动释放
  • ARC下[super dealloc]方法也由编译器自动插入
  • 所谓编译器插入代码过程需要进一步了解,还不清楚其运作方式

  • ARC环境,对象的实例变量将在根类 NSObject 的 dealloc 方法中释放内存
  • Father 的实例变量(如果有)将在它的 .cxx_desctruct方法中被释放,而 Son 的实例变量(如果有)将在它的 .cxx_desctruct方法中被释放
  • 子类在调用 dealloc 方法时会被插入代码,自动调用父类的 dealloc 方法

引用

ARC下dealloc过程及.cxx_destruct的探究
iOS内存管理之二