内存布局
- stack 临时变量
- heap 通过alloc等分配的对象
- .bss 未初始化的全局变量、静态变量
- .data 已初始化的全局变量、静态变量
- .text 程序代码
内存管理
不同场景的内存管理方案:
TaggedPointer
- 对象如果支持使用 TaggedPointer,苹果会直接将其指针值作为引用计数返回
- NSString\NSNumber\NSDate 支持使用TaggedPointer
NONPOINTER_ISA (64位架构下)
散列表
TaggedPointer
- 苹果的64位Objective-C实现中,若对象指针的最低有效位为奇数,则该指针为Tagged Pointer。
- Tagged Pointer专门用来存储小的对象,例如NSNumber/NSDate和NSString。
- Tagged Pointer指针的值不是地址,包含真正的值和对象类型信息。所以,实际上它不是一个对象,而是一个披着对象皮的普通变量。它的内存并不存储在堆中,也不需要malloc和free。
- Tagged Pointer因为并不是真正的对象,没有isa 指针。
- 内存读取和对象创建效率高。
1 | NSNumber *number = @25; //number地址:0xb000000000000192,解释:b:NSNumber类型,25的16进制是19,2:整型 |
- NSNumber类型:最高4位的“b”表示是NSNumber类型,最低4位(Int为2,long为3,float为4,double为5)表示基本数据类型,其余56位则用来存储数值本身内容。存储用的数值超过56位存储上限的时候,那么NSNumber才会用真正的64位内存地址存储数值,然后用指针指向该内存地址。
- NSString类型:最高位表示类型,最低位表示字符串长度。而其余的56位也是用来存储数据内容。
- NSString类型:当字符串内存长度超过了56位的时候,Tagged Pointer并没有立即用指针转向,而是用了一种算法编码,把字符串长度进行压缩存储,当这个算法压缩的数据长度超过56位了才使用指针指向。
- NSString类型:当String的内容有中文或者特殊字符(非 ASCII 字符)时,那么就只能存储为String指针。
- NSString类型:字面型字符串常量却从不存储为Tagged Pointer,因为字符串常量必须在不同的操作系统版本下保持二进制兼容,而Tagged Pointer在运行时总是由Apple的代码生成。
NONPOINTER_ISA
64 bit存储一个内存地址显然是种浪费。于是可以优化存储方案,用一部分额外的存储空间存储其他内容。isa是objc_object的一个私有成员,它的结构如下:
1 | union isa_t |
散列表(哈希表)
为什么不是一个SideTable?
如果只有一张SideTable,那么所有的对象都放在这张表中,这时候如果操作表是,在不同线程中,那么就需要加锁,从而引发效率问题。所以引入分离锁的。
SideTable
Spinlock_t (自旋锁)
- 是“忙等”的锁
- 适用于轻量访问
引用计数表(RefcountMap)
- 弱引用表(weak_table_t)
Spinlock_t (自旋锁)
- 是“忙等”的锁
- 适用于轻量访问
引用计数表(RefcountMap)
RefcountMap 结构
1 | typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap; |
根据传入对象的指针查找到size_t(结构中存储着引用计数的值)
1 | size_t |
弱引用表(weak_table_t)
根据传入对象的指针查找到weak_entry_t
(一个数组结构,数组中的每一项存储着这个对象对应的弱引用指针)
retainCount
的实现
1 | uintptr_t |
retain
实现讲解
1 | id |
release
实现讲解
1 | void |
Core Foundation
库中也提供了增减引用计数的方法。比如在使用 Toll-Free Bridge
转换时使用的 CFBridgingRetain
和 CFBridgingRelease
方法,其本质是使用 __bridge_retained
和 __bridge_transfer
告诉编译器此处需要如何修改引用计数:
1 | NS_INLINE CF_RETURNS_RETAINED CFTypeRef __nullable CFBridgingRetain(id __nullable X) { |
dealloc
实现流程
1 | void |
dealloc过程中,出了会将该对象的关联对象移除,还会将指向该对象的弱引用指针置为nil。
弱引用被添加到弱应用表的流程如下:
id __weak weakObj = obj;
经过编译变为如下代码,这便是弱引用指针加入到弱引用表的入口
1 | id weakObj; |
1 | //弱引用指针被添加到弱引用表的流程入口 |
autoreleasepool
autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage 为结点的双向链表来实现的。我们打开 runtime 的源码工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的实现源码。通过阅读源码,我们可以知道:
- 每一个线程的 autoreleasepool 其实就是一个指针的堆栈;
- 每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表一个 autoreleasepool 的边界);
- 一个 pool token 就是这个 pool 所对应的 POOL_SENTINEL 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release ;
- 这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;
- Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。
一个空的 AutoreleasePoolPage 的内存结构如下图所示:
1 | magic_t const magic; |
当 next == begin() 时,表示 AutoreleasePoolPage 为空;当 next == end() 时,表示 AutoreleasePoolPage 已满。
这段代码
1 | @autoreleasepool{ |
经过clang编译后
1 | /* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool; |
不得不说,苹果对 @autoreleasepool {}
的实现真的是非常巧妙,真正可以称得上是代码的艺术。苹果通过声明一个 __AtAutoreleasePool
类型的局部变量 __autoreleasepool
来实现 @autoreleasepool {}
。当声明 __autoreleasepool
变量时,构造函数 __AtAutoreleasePool()
被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush();
;当出了当前作用域时,析构函数~__AtAutoreleasePool()
被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj);
。也就是说 @autoreleasepool {}
的实现代码可以进一步简化如下:
1 | /* @autoreleasepool */ { |
因此,单个autoreleasepool
的运行过程可以简单地理解为 objc_autoreleasePoolPush()
、[对象 autorelease]
和 objc_autoreleasePoolPop(void *)
三个过程。
push 操作
提到的 objc_autoreleasePoolPush()
函数本质上就是调用的 AutoreleasePoolPage
的 push
函数。
1 | void * |
一个 push
操作其实就是创建一个新的 autoreleasepool
,对应 AutoreleasePoolPage
的具体实现就是往 AutoreleasePoolPage
中的 next
位置插入一个 POOL_SENTINEL
,并且返回插入的 POOL_SENTINEL
的内存地址。这个地址也就是我们前面提到的 pool token
,在执行 pop
操作的时候作为函数的入参。
1 | static inline void *push() |
autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:
- 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
- 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;
- 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中。
每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。
autorelease 操作
AutoreleasePoolPage 的 autorelease 函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。
1 | static inline id autorelease(id obj) |
pop 操作
pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址,即 pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。
每一个 autoreleasepool 只对应一个线程