本篇主作总结笔记,参考和摘抄了很多优质博客,在最底部。
内存布局

内存分五大区,App 启动时,系统会把程序拷贝到内存,在内存中执行代码。
首先说一下排在内存五大区之外的内核区和保留区:
- 内核区:主要处理内核模块,比如我们的系统内存为 4GB,那么我们实际上能使用 3GB,剩下的 1GB 就是给了内核区,指针地址 0xc0000000(3x1024x1024x1024)
- 保留区:用来给系统提供一些必要空间
当一个 app 启动后,代码区、常量区、全局区大小就已经固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(也即是野指针报错)
1. 栈区(Stack)
栈区里面会放一些函数参数以及一些局部变量,逐渐增多并在内存地址中由高向低延伸,由于栈的数据结构的原因,它的内存地址是连续的,栈区的内存大小在App启动时就确定下来的,若在压入时申请的内存空间大于栈的剩余空间,就会出现栈溢出(内存泄露),打印的地址为0x7在栈区
- 栈区是一块
连续的
内存空间,内存大小在 App 启动时就确定下来,若在压入时申请的内存空间大于栈的剩余空间,就会出现栈溢出,即内存泄漏 - 存储结构
从高地址往低地址
延伸 - 栈区存储的是
局部变量
,函数
,方法
,参数
,指针
- 栈区的地址空间一般是以
0x7
开头
举例:
objc
- (void)testStack{
// 栈区
int a = 10;
int b = 20;
NSObject *object = [NSObject new];
NSLog(@"a == %p",&a);
NSLog(@"b == %p",&b);
NSLog(@"object == %p",&object);
NSLog(@"%lu",sizeof(&object));
NSLog(@"%lu",sizeof(a));
}
打印结果如下:
举个栈溢出的例子:
objc
while (10000) {
int a = 2;
}
上面这段代码会造成内存暴涨,因为栈区只有1M大小,一个int类型是4字节,创建一个局部变量就会压入一个4字节到栈区,当超过栈区的上限时就会造成栈溢出。
解决方法可以给代码加一个自动释放池
objc
栈区在内存中是由高向低存储,堆区是由低向高存储,当两者相遇时,就出现了堆栈溢出。
2. 堆区(heap)
- 堆内存大小是
动态变化
的,取决于系统的虚拟内存 - 存储结构是
从低地址向高地址
扩展 - 系统是用链表来管理堆的内存的,所以它的内存地址是
不连续
的 - 堆区存储的是
对象
,alloc
或new
出来的变量。 - 堆区地址空间一般以
0x6
开头
举例:
objc
- (void)testHeap{
// 堆区
NSObject *object1 = [NSObject new];
NSObject *object2 = [NSObject new];
NSObject *object3 = [NSObject new];
NSObject *object4 = [NSObject new];
NSObject *object5 = [NSObject new];
NSObject *object6 = [NSObject new];
NSObject *object7 = [NSObject new];
NSObject *object8 = [NSObject new];
NSObject *object9 = [NSObject new];
NSLog(@"object1 = %@",object1);
NSLog(@"object2 = %@",object2);
NSLog(@"object3 = %@",object3);
NSLog(@"object4 = %@",object4);
NSLog(@"object5 = %@",object5);
NSLog(@"object6 = %@",object6);
NSLog(@"object7 = %@",object7);
NSLog(@"object8 = %@",object8);
NSLog(@"object9 = %@",object9);
}
结果如下:
在 OC,系统是通过引用计数判断是否释放对象,当引用计数为 0 就说明没有任何变量使用该空间,系统将释放对象。
3. 全局区/静态区
全局变量和静态变量的存储是放在一块的,分为.bss段
和.data段
,内存地址一般由0x1
开头:
Bss
段:未初始化
的全局变量和静态变量在一块区域Data
段:已初始化
的全局变量和静态变量在相邻的另一块区域
举例:
objc
static int bss;
static int bssStr;
static int data = 10;
static NSString *dataStr = @"nihao";
- (void)globalTest {
// 全局区
NSLog(@"****bss****");
NSLog(@"bss == %p",&bss);
NSLog(@"bssStr == %p",&bssStr);
NSLog(@"****data****");
NSLog(@"data == %p",&data);
NSLog(@"dataStr == %p",&dataStr);
}
结果如下:

若在.h
中创建一个静态变量ws_number
,则使用这个静态变量的每个文件都会生成一个ws_number
的静态变量,且初始值都一样。也就是说在多个文件使用同一个静态变量,系统会各自生成一个相同初始值
且地址不同
的静态变量 ,这样在各自文件内使用就不会互相干扰,数据比较安全。这也是为什么静态区也称作静态安全区
。
4. 常量区
- 常量字符串就是放在这里的
- 程序结束后由系统释放
5. 代码段(.text)
- 存储程序代码,在编译时加载到内存中,代码会被编译成
二进制的形式
进行存储
举个例子来理解一下:
1、对象查找过程: 在程序查找一个对象时,首先到栈区找到对象的指针,再通过这个对象指针到堆区找到这个对象。
内存管理
NONPOINTER_ISA
正如上面所说,防止地址空间浪费,isa
指针设计成了联合体,在isa
地址中存储了很多信息。
isa
的结构如下:
c
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
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
#endif
};
OC对象的本质,每个OC对象都含有一个isa指针,__arm64__
之前,isa
仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__
架构之后,apple对isa
进行了优化,变成了一个联合体union
结构,同时使用位域来存储更多的信息。 它是通过isa
的bits
进行位运算,取出响应位置的值,runtime中的isa
是被联合体位域优化过的,它不单单是指向类对象了,而是把64位中的每一位都运用了起来,其中的shiftcls
为33位,代表了类对象的地址,其他的位都有各自的用处。
nonpointer
:表示是否对isa指针开启指针优化 0:不开启,表示纯isa指针。 1:开启,不单单是类对象的地址,isa中包含了类信息和对象的引用计数等。has_assoc
:关联对象标识位,0没有,1有,没有关联对象会释放的更快。has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。shiftcls
:存储类指针class
的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤用来存储类指针。magic
:固定值为0xd2,用于在调试时分辨对象是否完成初始化。weakly_referenced
:表示对象是否被指向或者曾经指向一个 ARC 的弱引用变量, 没有弱引⽤的对象可以更快释放。deallocating
:标志对象是否正在释放内存。has_sidetable_rc
:当对象的引用计数大于10,以至于无法存储在isa指针中时,用散列表sidetable去计数。extra_rc
:表示该对象的引用计数,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数⼤于 10, 则需要使⽤到has_sidetable_rc。
TaggedPointer
为了节省内存和提高执行效率,苹果提出了
Tagged Pointer
的概念。对于 64 位程序,引入Tagged Pointer
后,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。
NSTaggedPointer
类型的对象采用和isa
一样的联合体位域的方式,可直接从地址中读取出想要的值,一般当数据类型的"value"足够小时,系统会自动转换为NSTaggedPointer
类型,比如NSString
转换为NSTaggedPointerString
。
对象的值直接存储在了指针中,不必在堆上为其分配内存,节省了很多内存开销。
更详细的 TaggedPointer 解析可以看iOS - 老生常谈内存管理(五):Tagged Pointer。
引用计数
主要摘自iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-一
引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象时,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。
SideTables
引用计数要么存放在 isa
的 extra_rc
中,要么存放在引用计数表中,而引用计数表包含在一个叫 SideTable
的结构中,它是一个散列表,也就是哈希表。而 SideTable
又包含在一个全局的 StripeMap
的哈希映射表中,这个表的名字叫 SideTables
。
NSObject.mm
中SideTables
对应的源码如下
objc
// SideTables
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
// SideTable
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; // 引用计数表
weak_table_t weak_table; // 弱引用表
// other code ...
};
它们的关系如下图:
自旋锁spinlock_t
自旋锁适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
spinlock_t slock
用于对sidetable
加锁,保证数据安全,使用自旋锁作为安全锁其实是因为引用计数的操作非常快且频繁。
引用计数器RefcountMap refcnts
具体的引用计数数量是记录在这里的,refcnts
是C++
的Map
,在SideTable
中需要再次调用table.refcnts.find(0x0000)
或者table.refcnts.find(0x000f)
找到真正的引用计数器。
引用计数器的存储结构如下图所示
具体的,引用计数器RefcountMap refcnts
经过find
查找到的value
其实是个位域,类似NONPOINTER_ISA
指针:
- 1UL<<0:
WEAKLY_REFERENCED
表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。 - 1UL<<1:
DEALLOCATING
表示对象是否正在被释放。1正在释放,0没有。 REAL COUNT
图中REAL COUNT
的部分才是对象真正的引用计数存储区。所以咱们说的引用计数加一或者减一,实际上是对整个unsigned long
加四或者减四,因为真正的计数是从2^2位开始的。- 1UL<<(WORD_BITS-1):
SIDE_TABLE_RC_PINNED
其中WORD_BITS在32位和64位系统的时候分别等于32和64。其实这一位没啥具体意义,就是随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。
- (1UL<<0)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动0位(就是原地不动):0b0000 0000 0000 0000 0000 0000 0000 0001
- (1UL<<1)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动1位:0b0000 0000 0000 0000 0000 0000 0000 0010
弱引用列表weak_table_t weak_table
weak_table_t
结构如图所示
weak_entry_t *weak_entries
:是一个数组,上面的RefcountMap
是要通过find(key)
来找到精确的元素的。weak_entries
则是通过循环遍历来找到对应的entry
。referent
:被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。referrers
:可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers
里的所有指针都会被设置成nil。inline_referrers
:只有4
个元素的数组,默认情况下用它来存储弱引用的指针。当大于4
个的时候使用referrers
来存储指针。
num_entries
:用来维护保证数组始终有一个合适的size
。比如数组中元素的数量超过3/4
的时候将数组的大小乘以2
。
Q:既然SideTables
是一个哈希映射的表,为什么不用 SideTables
直接包含自旋锁,引用计数表和弱引用表呢?
这是因为在众多线程同时访问这个 SideTable
表的时候,为了保证数据安全,需要给其加上自旋锁,如果只有一张 SideTable
的表,那么所有数据访问都会出一个进一个,单线程进行,非常影响效率,虽然自旋锁已经是效率非常高的锁,这会带来非常不好的用户体验。针对这种情况,将一张 SideTable
分为多张表的 SideTables
,再各自加锁保证数据的安全,这样就增加了并发量,提高了数据访问的效率,这就是为什么一个 SideTables
下涵盖众多 SideTable
表的原因。
Q:为什么SideTables
已经通过Hash映射了,还需要RefcountMap
再映射一次
其实苹果采用的是分块化思想 ,内存中对象的数量实在是太庞大了我们通过第一个Hash
表只是过滤了第一次,然后我们还需要再通过这个Map
才能精确的定位到我们要找的对象的引用计数器。
假设现在内存中有16个对象,0x0000、0x0001、...... 0x000e、0x000f
,咱们创建一个SideTables[8]
来存放这 16 个对象,那么查找的时候发生Hash
冲突的概率就是八分之一。假设SideTables[0x0000]
和SideTables[0x0x000f]
冲突,映射到相同的结果。
objc
SideTables[0x0000] == SideTables[0x0x000f] ==> 都指向同一个SideTable
苹果把两个对象的内存管理都放到同一个SideTable
中。你在这个SideTable
中需要再次调用table.refcnts.find(0x0000)
或者table.refcnts.find(0x000f)
来找到他们真正的引用计数器。
内存管理相关操作
alloc
从一个面试题开始探索alloc
流程:
问:alloc 之后,引用计数如何变化?
答:在初始化isa
的时候,并没有对extra_rc
进行操作。也就是说alloc
方法实际上并没有设置对象的引用计数值为 1。
验证如下:创建一个LGPerson
类,在main
方法中创建一个实例对象
objc
#import <Foundation/Foundation.h>
#import "LGPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *objc2 = [LGPerson alloc];
NSLog(@"Hello, World! %@",objc2);
}
return 0;
}
从NSObject.mm
类的+ (id)alloc
方法开始探索,方法(函数)从上往下依次执行,省略了部分影响解读的代码
objc
+ (id)alloc {
return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
// some code ...
id obj = class_createInstance(cls, 0);
return obj;
}
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
// 内部调用 calloc 方法分配内存。
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
// some code ...
id obj;
obj = (id)calloc(1, size); // 此时分配内存
obj->initInstanceIsa(cls, hasCxxDtor);
return obj;
}
上面的代码主要目的是分配内存
,真正的初始化在initInstanceIsa
,内部会初始化isa
指针的内容
objc
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
if (!nonpointer) {
isa.cls = cls;
} else {
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
// 将cls右移3位赋值给shiftcls
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
initIsa
里面,将bits
赋值了ISA_MAGIC_VALUE
,ISA_MAGIC_VALUE
为宏定义0x001f800000000001ULL
,断点后可以看到nonpointer
为1
,magic
为59
,如下图 执行
shiftcls
赋值方法后,cls
值已经变成了LGPerson
(自定义的类名),即已经赋值成功,如下图 此时可以注意到,
extra_rc
的值是0
,表示alloc
这一步实际上分配了内存,初始化了对象,但引用计数实际上是 0(调用init
之后就变成 1 了)。
值得一提的是callAlloc
函数中的slowpath
和fastpath
,callAlloc
完整实现如下:
objc
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())) {
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
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
方法中使用到的 slowpath
和 fastpath
,其实这两个都是宏定义,与代码逻辑本身无关,定义如下:
objc
// x 很可能为 true,希望编译器进行优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 很可能为 false,希望编译器进行优化
#define slowpath(x) (__builtin_expect(bool(x), 0))
其实它们是所谓的快路径和慢路径,为了解释这个,我们来看一段代码:
objc
if (x)
return 1;
else
return 39;
由于计算机并非一次只读取一条指令,而是读取多条指令,所以在读到 if
语句时也会把 return 1
读取进来。如果 x
为 0,那么会重新读取 return 39
,重读指令相对来说比较耗时。
如果 x
有非常大的概率是 0,那么 return 1
这条指令每次不可避免的会被读取,并且实际上几乎没有机会执行,造成了不必要的指令重读。
因此,在苹果定义的两个宏中,fastpath(x)
依然返回 x
,只是告诉编译器 x
的值一般不为 0,从而编译可以进行优化。同理,slowpath(x)
表示 x
的值很可能为 0,希望编译器进行优化。
objc
// 以下代码表示,
// 很可能 checkNil && !cls 的结果是 false,
// 编译器可以不用每次都读取 return nil 指令
if (slowpath(checkNil && !cls)) return nil;
当然,当checkNil && !cls
判断成立的时候,return nil
指令还是会被读取,然后执行的。
fastpath(expression)
也是同样的机制,表示很可能 expression 结果是 true
。
init
objc
// NSObject.mm
// Calls [[cls alloc] init].
id
objc_alloc_init(Class cls)
{
return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
基类的init
方法啥都没干,只是将alloc
创建的对象返回。我们可以重写init
方法来对alloc
创建的实例做一些初始化操作。
retainCount
retainCount
方法是取出对象的引用计数值。怎么取值的呢?相信你们已经想到了,isa
或Sidetable
,下面我们进入源码看看它的取值过程。
retainCount
方法的函数调用栈如下
objc
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
uintptr_t _objc_rootRetainCount(id obj)
{
return obj->rootRetainCount();
}
inline uintptr_t
objc_object::rootRetainCount()
{
// 如果是 tagged pointer,直接返回 this
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
// 获取 isa
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
// 如果 isa 是 nonpointer
if (bits.nonpointer) {
// isa指针里的引用计数字段extra_rc,再+1,为引用计数的值
// alloc没有让引用计数+1,而获取retainCount却是1原因就在这
uintptr_t rc = 1 + bits.extra_rc;
// 如果还额外使用 sidetable 存储引用计数
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock(); // 加上 sidetable 中引用计数的值
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
// 如果 isa 不是 nonpointer,返回 sidetable_retainCount() 的值
return sidetable_retainCount();
}
size_t
objc_object::sidetable_getExtraRC_nolock()
{
ASSERT(isa.nonpointer);
// 获得 SideTable
SideTable& table = SideTables()[this];
// 获得 refcnts
RefcountMap::iterator it = table.refcnts.find(this);
// 如果没找到,返回 0
if (it == table.refcnts.end()) return 0;
// 如果找到了,通过 SIDE_TABLE_RC_SHIFT 位掩码获取对应的引用计数
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
#define SIDE_TABLE_RC_SHIFT 2
小结
- 在
arm64
之前,isa
不是nonpointer
。对象的引用计数全都存储在SideTable
中,retainCount
方法返回的是对象本身的引用计数值 1,加上SideTable
中存储的值; - 从
arm64
开始,isa
是nonpointer
。对象的引用计数先存储到它的isa
中的extra_rc
中,如果 19 位的extra_rc
不够存储,那么溢出的部分再存储到SideTable
中,retainCount
方法返回的是对象本身的引用计数值 1,加上isa
中的extra_rc
存储的值,加上SideTable
中存储的值。 - 所以,其实我们通过
retainCount
方法打印alloc
创建的对象的引用计数为 1,这是retainCount
方法的功劳,alloc
方法并没有设置对象的引用计数。
alloc
方法没有设置对象的引用计数为 1,它内部也没有调用retainCount
方法,init
时也只是返回alloc
创建的对象,按照引用计数的定义,对象不会直接dealloc
吗?
dealloc
方法是在release
方法内部调用的。只有你直接调用了dealloc
,或者调用了release
且在release
方法中判断对象的引用计数为 0 的时候,才会调用dealloc
。详情请参阅release
源码分析。
retain&release
- retain
- 将
isa
中的extra_rc
+1,如果溢出,就将extra_rc
的RC_HALF
转移到sidetable
中存储,extra_rc
是19
位,而RC_HALF
宏是(1ULL<<18)
,实际上相等于进行了 +1 操作 - 对于
NSTaggedPointer
类型,直接返回,不参与引用计数计算,因为NSTaggedPointer
对象的值直接存储在了指针中,不必在堆上为其分配内存,这点在objc_msgSend
也有体现,首先会判断LNilOrTagged
。
- 将
- release
- 将
isa
中的extra_rc
-1,如果下溢,则判断has_sidetable_rc
是否为true
,即是否使用了sidetable
,如果有的话就从sidetable
中转移RC_HALF
个引用计数给extra_rc
,若不够RC_HALF
个,就有多少转移多少,如果extra_rc
中引用计数为 0 且has_sidetable_rc
为false
或者Sidetable
中的引用计数也为 0 了,那就dealloc
对象。
- 将
详情参阅 [iOS - 老生常谈内存管理 - retain&release]
weak
如果用__weak 修饰一个变量,底层执行了什么呢?以下面代码为例
objc
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
底层的操作其实是
objc
objc_initWeak(&obj1, obj);
// NSObject.mm
① objc_initWeak
② storeWeak
// objc-weak.mm
③ weak_unregister_no_lock
④ weak_register_no_lock
接下来查看源码
objc_initWeak
objc
// *location 为 __weak 指针地址(即obj1),newObj(即obj)为被弱引用对象地址
id objc_initWeak(id *location, id newObj)
{
// 如果对象为 nil,那就将 weak 指针置为 nil
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
storeWeak
objc
// 更新 weak 变量
// 如果 HaveOld == true,表示对象有旧值,即旧地址,它需要被清理掉,这个旧值可能为 nil
// 如果 HaveNew == true,表示一个新值需要赋值给变量,这个新值可能为 nil
// 如果 CrashIfDeallocating == true,则如果对象正在销毁或者对象不支持弱引用,则停止更新
// 如果 CrashIfDeallocating == false,则存储 nil
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry: // 分别获取新旧值相关联的弱引用表
// 如果变量有旧值,获取已有对象(该旧值对象)和旧表
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
// 如果有新值要赋值给变量,则创建新表
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 分别给 oldTable 和 newTable 加锁
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// 判断 oldObj 和 location 是否是同一对象,如果不是就重新获取旧值相关联的表
if (haveOld && *location != oldObj) {
// 解锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
// 如果有新值,则判断新值所属的类是否已经初始化,没初始化的话在此初始化
// 这一步是防止 +initialize 内部调用 storeWeak 产生死锁
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
// Clean up old value, if any.
// 如果有旧值,则调用weak_unregister_no_lock执行清空操作
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
// 如果有新值,则调用weak_register_no_lock把所有 weak 指针重新指向新的对象
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
// 设置 weakly-referenced 标志位
// 如果对象是 Tagged Pointer,则不做操作
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
// 将 location 指向新的对象
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
// 解锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
return (id)newObj;
}
store_weak
函数的执行过程如下:
- 分别获取新旧值相关联的弱引用表
- 如果有旧值,调用
weak_unregister_no_lock
函数清除旧值,移除所有指向旧值的weak
引用,而不是赋值为nil
- 如果有新值,调用
weak_register_no_lock
函数分配新值,将所有weak
指针重新指向新的对象 - 设置
isa
的weakly_referenced
弱引用标志位
weak_unregister_no_lock
weak_unregister_no_lock
用来移除弱引用对象
objc
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id)
{
// 被弱引用的对象
objc_object *referent = (objc_object *)referent_id;
// 弱引用变量的地址
objc_object **referrer = (objc_object **)referrer_id;
/**
* weak_entry_t 结构
* - referent 被弱引用的对象
* - referrers 当有超过4个弱引用对象时,则存储到 referrers 中
* - inline_referrers 存储小于4个的弱引用对象
*/
weak_entry_t *entry;
if (!referent) return;
// 调用 weak_entry_for_referent 找到 entry 弱引用指针 item
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 从内层 inline_referrers 中移除 entry
// inline_referrers 中只能存储 4 个弱引用指针,
// 多了就要存储到 referrers 中,所以要多一步 empty 判空操作
remove_referrer(entry, referrer);
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false;
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
//从 weak_table 中移除 entry 弱引用条目
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
// Do not set *referrer = nil. objc_storeWeak() requires that the
// value not change.
}
weak_unregister_no_lock
用来移除已经存在的弱引用表,一般用于弱引用对象
已经不再引用,但被弱引用对象
还没有死亡的情况,内部执行步骤为:
- 查询
weak_table
,如果有弱引用信息,则得到entry
。 remove_referrer(entry, referrer)
移除相关联的弱引用信息。
weak_register_no_lock
weak_register_no_lock
用来保存弱引用对象
scss
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
//被弱引用的对象
objc_object *referent = (objc_object *)referent_id;
//弱引用变量的地址
objc_object **referrer = (objc_object **)referrer_id;
//如果该弱引用对象是taggedPointer对象,则不做处理直接返回该对象
//taggedPointer对象是为了苹果为了性能最大化做的处理,
//针对不需要到堆中寻找的对象,可以直接从地址中通过一定的算法得到他们的值。
if (!referent || referent->isTaggedPointer()) return referent_id;
// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
// now remember it and where it is being stored
weak_entry_t *entry;//弱引用指针的条目
//判断weak_table中是否有该条目
if ((entry = weak_entry_for_referent(weak_table, referent))) {
//如果有,则把弱引用对象追加进去
append_referrer(entry, referrer);
}
else {
//如果没有,则创建一个
weak_entry_t new_entry(referent, referrer);
//如果索引已经超过原来的3/4,则给weak_table扩容
weak_grow_maybe(weak_table);
//将新的entry插入weak_table
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
weak_register_no_lock
保存弱引用对象具体流程如下:
- 判断
TaggedPointer
类型则直接返回,不需要保存弱引用信息(TaggedPointer
不需要在堆中分配内存) - 如果正在释放且对象不支持弱引用,则停止更新(
crashIfDeallocating==true && deallocating == true
) - 判断
weak_table
中是否有该对象的弱引用表,- 有就追加上
append_referrer
- 没有则创建个
weak_table
在加入
- 有就追加上
小结
weak
对象在底层的存储流程如下图所示
dealloc
dealloc
方法的函数调用栈为:
objc
// NSObject.mm
① dealloc
② _objc_rootDealloc
// objc-object.h
③ rootDealloc
// objc-runtime-new.mm
④ object_dispose
⑤ objc_destructInstance
// objc-object.h
⑥ clearDeallocating
// NSObject.mm
⑦ sidetable_clearDeallocating
clearDeallocating_slow
objc_rootDealloc&rootDealloc
objc
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
// 判断是否为 TaggerPointer 内存管理方案,是的话直接 return
if (isTaggedPointer()) return; // fixme necessary? *
if (fastpath(isa.nonpointer && // 如果 isa 为 nonpointer
!isa.weakly_referenced && // 没有弱引用
!isa.has_assoc && // 没有关联对象
!isa.has_cxx_dtor && // 没有 C++ 的析构函数
!isa.has_sidetable_rc)) // 没有额外采用 SideTabel 进行引用计数存储
{
assert(!sidetable_present());
free(this); // 如果以上条件成立,直接调用 free 函数销毁对象
}
else {
object_dispose((id)this); // 如果以上条件不成立,调用 object_dispose 函数
}
}
fastpath(x)
表示内部的x
值很可能为true
,希望编译器进行优化,如果符合5
个条件,则直接free
。
object_dispose&objc_destructInstance
objc
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
// 如果有 C++ 的析构函数,调用 object_cxxDestruct 函数
if (cxx) object_cxxDestruct(obj);
// 如果有关联对象,调用 _object_remove_assocations 函数,移除关联对象
if (assoc) _object_remove_assocations(obj);
// 调用 clearDeallocating 函数
obj->clearDeallocating();
}
return obj;
}
clearDeallocating
objc
inline void
objc_object::clearDeallocating()
{
// 如果 isa 不是 nonpointer
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
// 调用 sidetable_clearDeallocating 函数
sidetable_clearDeallocating();
}
// 如果 isa 是 nonpointer,且有弱引用或者有额外使用 SideTable 存储引用计数
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
// 调用 clearDeallocating_slow 函数
clearDeallocating_slow();
}
assert(!sidetable_present());
}
slowpath(x)
表示 x
的值很可能为false
,希望编译器进行优化。先不考虑isa
不是nonpointer
的情况,我们继续看clearDeallocating_slow
清除弱引用及引用计数的操作。
objc
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
// 获取 SideTable
SideTable& table = SideTables()[this];
table.lock();
// 如果有弱引用
if (isa.weakly_referenced) {
// 调用 weak_clear_no_lock:将指向该对象的弱引用指针置为 nil
weak_clear_no_lock(&table.weak_table, (id)this);
}
// 如果有使用 SideTable 存储引用计数
if (isa.has_sidetable_rc) {
// 调用 table.refcnts.erase:从引用计数表中擦除该对象的引用计数
table.refcnts.erase(this);
}
table.unlock();
}
weak_clear_no_lock
清除弱引用的核心方法,在对象dealloc
的时候,会调用weak_clear_no_lock
函数将指向该对象的弱引用指针置为nil
,具体实现如下
objc
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
//referent为被销毁对象的指针
objc_object *referent = (objc_object *)referent_id;
//通过被销毁对象的指针获得entry,这个entry里存着这个对象的弱引用指针数组
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
//弱引用指针数组
weak_referrer_t *referrers;
size_t count;
//判断弱引用指针数量,小于4个存在entry的inline_referrers中,大于4个存在entry的referrers中
if (entry->out_of_line()) {
//如果大于4个,则到referrers中取弱引用指针的数组
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
//循环把所有弱引用指针置为nil
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
// 一些 free和num_entries--操作
weak_entry_remove(weak_table, entry);
}
小结
dealloc
的调用流程如下:
dealloc
时首先执行dealloc
->_objc_rootDealloc()
,直接来到rootDealloc()
函数- 在
rootDealloc()
中判断5个条件
决定是否可以直接释放free()
- NONPointer_ISA // 是否是非指针类型 isa
- weakly_reference // 是否有若引用
- has_assoc // 是否有关联对象
- has_cxx_dtor // 是否有 c++ 相关内容
- has_sidetable_rc // 是否使用到 sidetable
- 不符合
5个条件
则调用objc_dispose()
,进而执行objc_destructInstance()
函数逐步释放- 先判断
hasCxxDtor
,销毁 c++ 相关内容 - 再判断
hasAssociatedObjects
,销毁关联对象 - 执行
clearDeallocating()
,用以销毁弱引用和引用计数
- 先判断
clearDeallocating()
销毁弱引用和引用计数- 执行
waek_clear_no_lock
销毁弱引用 - 获取
SideTable
执行table.refcnts.eraser()
擦除该对象的引用计数
- 执行
自动释放池AutoReleasePool
这里主要作总结笔记,更详细的内容参考iOS - 聊聊 autorelease 和 @autoreleasepool
AutoreleasePool原理

- 自动释放池(即所有的
AutoreleasePoolPage
对象)是以栈
为结点通过双向链表
的形式组合而成,每当Page
满了的时候,就会创建一个新的Page
,并设置它为hotPage
,而首个Page
为coldPage
。 - 有个属性叫
POOL_BOUNDARY
,称为哨兵对象,用来解决自动释放池嵌套的问题- 每当创建一个自动释放池,就会调用
push()
方法将一个POOL_BOUNDARY
入栈 - 当销毁一个自动释放池时,会调用
pop()
方法并传入一个POOL_BOUNDARY
,会从自动释放池中最后一个对象开始,依次给它们发送release
消息,直到遇到这个POOL_BOUNDARY
- 每当创建一个自动释放池,就会调用
- 自动释放池与线程一一对应,每个线程都会维护一个自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,
pool
销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池 - 每个
AutoreleasePoolPage
对象占用4096
字节内存,其中56
个字节用来存放它内部的成员变量,剩下的空间(4040
个字节)用来存放autorelease
对象的地址。
嵌套@autoreleasepool
嵌套的@autoreleasepool
其实就是不停的push
哨兵对象(POOL_BOUNDARY
),在pop
时,会先释放里面的,在释放外面的。
举例如下
objc
int main(int argc, const char * argv[]) {
_objc_autoreleasePoolPrint(); // print1
@autoreleasepool { //r1 = push()
_objc_autoreleasePoolPrint(); // print2
HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print3
@autoreleasepool { //r2 = push()
HTPerson *p3 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print4
@autoreleasepool { //r3 = push()
HTPerson *p4 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print5
} //pop(r3)
_objc_autoreleasePoolPrint(); // print6
} //pop(r2)
_objc_autoreleasePoolPrint(); // print7
} //pop(r1)
_objc_autoreleasePoolPrint(); // print8
return 0;
}
autoreleasePool
结构如图所示
Q:Runloop和@autoreleasePool关系
在iOS
中autorelease
对象的释放时机是由RunLoop
控制的,会在RunLoop
每次循环结束时释放。
阅读Runloop
源码其实没发现和autoreleasePool
有什么关系,但在 App 启动后,苹果在主线程RunLoop
里注册了两个Observer
,回调都是_wrapRunLoopWithAutoreleasePoolHandler()
。
- 第一个
Observer
监视的事件Entry(即将进入 Loop)
,其回调内会调用_objc_autoreleasePoolPush()
创建自动释放池。其order
是-2147483647
,优先级最高,保证创建释放池发生在其他所有回调之前。
- 第二个
Observer
监视了两个事件BeforeWaiting(准备进入休眠)
时调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit(即 将退出 Loop)
时调用_objc_autoreleasePoolPop()
来释放自动释放池。- 这个
Observer
的order
是2147483647
,优先级最低,保证其释放池子发生在其他所有回调之后。
举例验证
objc
__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad];
// str是一个autorelease对象,设置一个weak的引用来观察它
NSString *str = [NSString stringWithFormat:@"sunnyxx"];
reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", reference); // Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%@", reference); // Console: (null)
}
viewDidLoad
和viewWillAppear
是在同一个runloop
循环下调用的,因此在viewWillAppear
中,这个autorelease
的变量依然有值。
当然,我们也可以手动干预autorelease
对象的释放时机:
objc
- (void)viewDidLoad
{
[super viewDidLoad];
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"sunnyxx"];
}
NSLog(@"%@", str); // Console: (null)
这样出了@autoreleasepool
作用域,内部的autorelease
对象就被释放了。

Q:main函数的@autoreleasepool和主线程runloop的@autoreleasepool关系?
Xcode
新旧版本main
函数里面的@autoreleasepool
管理的作用域?
Xcode 新版本
objc
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Xcode 旧版本
objc
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
如果你的程序使用了AppKit
或UIKit
框架,那么主线程的RunLoop
就会在每次事件循环迭代中创建并处理@autoreleasepool
。也就是说,应用程序所有autorelease
对象的都是由RunLoop
创建的@autoreleasepool
来管理。而main()
函数中的@autoreleasepool
只是负责管理它的作用域中的autorelease
对象。
旧版本 Xcode 中 main 函数的@autoreleasepool
Xcode 旧版本的main
函数中是将整个应用程序运行(UIApplicationMain
)放在@autoreleasepool
内,而主线程的RunLoop
就是在UIApplicationMain
中创建,所以RunLoop
创建的@autoreleasepool
是嵌套在main
函数的@autoreleasepool
内的。RunLoop
会在每次事件循环中对自动释放池进行pop
和push
,但是它的pop
只会释放掉它的POOL_BOUNDARY
之后的对象,它并不会影响到外层main
函数中@autoreleasepool
。
新版本 Xcode11 以后的 main 函数发生了哪些变化?
- 旧版本是将整个应用程序运行放在
@autoreleasepool
内,由于RunLoop
的存在,要return
即程序结束后@autoreleasepool
作用域才会结束,这意味着程序结束后main
函数中的@autoreleasepool
中的autorelease
对象才会释放。 - 而在 Xcode 11 中,触发主线程
RunLoop
的UIApplicationMain
函数放在了@autoreleasepool
外面,这可以保证@autoreleasepool
中的autorelease
对象在程序启动后立即释放。正如新版本的@autoreleasepool
中的注释所写 "Setup code that might create autoreleased objects goes here.
"(如上代码),可以将autorelease
对象放在此处。
Q:什么时候需要手动添加@autoreleasepool?
AppKit 和 UIKit 框架会在RunLoop
每次事件循环迭代中创建并处理@autoreleasepool
,因此,你通常不必自己创建@autoreleasepool
,甚至不需要知道创建@autoreleasepool
的代码怎么写。但是,有些情况需要自己创建@autoreleasepool
。
例如,如果我们需要在循环中创建了很多临时的autorelease
对象,则手动添加@autoreleasepool
来管理这些对象可以很大程度地减少内存峰值。比如在for
循环中alloc
图片数据等内存消耗较大的场景,需要手动添加@autoreleasepool
。
苹果给出了三种需要手动添加
@autoreleasepool
的情况:
- ① 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
- ② 如果你编写的循环中创建了大量的临时对象;
你可以在循环内使用@autoreleasepool
在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool
有助于减少应用程序的最大内存占用。- ③ 如果你创建了辅助线程。
一旦线程开始执行,就必须创建自己的@autoreleasepool
;否则,你的应用程序将存在内存泄漏。
参考博客
iOS底层-内存分区与布局
iOS概念攻坚之路(三):内存管理
iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-一
iOS - 老生常谈内存管理(四):内存管理方法源码分析
iOS - 老生常谈内存管理(三):ARC 面世
iOS - 聊聊 autorelease 和 @autoreleasepool