从这篇文章开始探索iOS的内存管理,主要涉及的内容有
1. 内存布局;
2. 内存管理方案:Tagged Pointer、NONPOINTER_ISA、SiddeTables
3. ARC&MRC:retain和release以及retainCount
4. 自动释放池:autoreleasepool
5. 弱引用weak的实现原理
1、内存布局
iOS中内存布局区域大概分为五个区域:栈区、堆区、BSS段、数据段、代码段,他们在内存的分布如下图所示:
- 栈区:编译器自动分配,由系统管理,在不需要的时候自动清除。局部变量、函数参数存储在这里。栈区的内存地址一般是0x7开头。
- 堆区:那些由
new
,alloc
、block copy
创建的对象存储在这里,是由开发者管理的,需要告诉系统什么时候释放内存。ARC下编译器会自动在合适的时候释放内存,而在MRC下需要开发者手动释放。堆区的内存地址一般是0x6开头。- BSS段:BSS段又称静态区,未初始化的全局变量,静态变量存放在这里。程序运行过程中内存中的数据一直存在,程序结束后由系统释放。
- 数据段:数据段又称常量区,专门存放常量,程序结束后由系统释放。
- 代码段:用于存放程序运行时的代码,代码会被编译成二进制存进内存的程序代码区。
这里有点值得一提的是静态变量的作用域与对象、类、分类没关系,只与文件有关系。
static int age = 10;
@interface Person : NSObject
-(void)add;
+(void)reduce;
@end
@implementation Person
- (void)add {
age++;
NSLog(@"Person内部:%@-%p--%d", self, &age, age);
}
+ (void)reduce {
age--;
NSLog(@"Person内部:%@-%p--%d", self, &age, age);
}
@end
@implementation Person (DS)
- (void)ds_add {
age++;
NSLog(@"Person (DS)内部:%@-%p--%d", self, &age, age);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"vc:%p--%d", &age, age);
age = 40;
NSLog(@"vc:%p--%d", &age, age);
[[Person new] add];
NSLog(@"vc:%p--%d", &age, age);
[Person reduce];
NSLog(@"vc:%p--%d", &age, age);
[[Person new] ds_add];
}
打印结果:
2020-03-23 16:53:35.671470+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--10
2020-03-23 16:53:35.671611+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.671809+0800 ThreadDemo[40300:1619888] Person内部:<Person: 0x60000239c640>-0x103688d88--11
2020-03-23 16:53:35.671926+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.672071+0800 ThreadDemo[40300:1619888] Person内部:Person-0x103688d88--10
2020-03-23 16:53:35.672183+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.672332+0800 ThreadDemo[40300:1619888] Person (DS)内部:<Person: 0x6000023a7820>-0x103688cc4--11
从上面运行结果可以知道,在Person类、Person分类、Controller中针对静态变量age的操作,其值并不相互影响。
2、内存管理方案
OC中对内存优化管理的方案有如下几种形式:Tagged Ponter、NONPOINTER_ISA 、SideTable
。下面对着三种方案逐一解释。
2.1、Tagged Ponter
在 2013 年 9 月,苹果推出了 iPhone5s
,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器
,为了节省内存和提高执行效率,苹果提出了Tagged Pointer
的概念。
Tagged Pointer
是专⻔⽤来存储⼩的对象,例如NSNumber
,NSDate
等。Tagged Pointer
指针的值不再是地址了,⽽是真正的值。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。所以,它的内存并不存储在堆中,也不需要malloc
和free
。- 在内存读取上有着3倍的效率,创建时⽐以前快106倍。
那么Tagged Ponter
对于内存优化的点在哪里呢?
2.1.1、Tagged Ponter内存优化
对于一个NSNumber
对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger的普通变量,那么它在32位CPU下占 4 个字节,在 64 位CPU下占 8 个字节的。而NSNumber对象还有一个isa
指针,它在32位CPU下为4个字节,在 64 位 CPU 下也是 8 个字节。所以从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种 NSNumber、NSDate 一类的对象所占用的内存会翻倍。如下图所示(图片摘自唐巧博客):
而实际上一个NSNumber、NSDate这一类的变量的值需要的内存空间常常不需要8个字节,那么如上述来进行数据的存储,内存空间的浪费是很大的。Tagged Ponter
恰恰解决了这一块的问题。
Tagged Ponter
将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber 的内存图变成了以下这样:
2.1.2、Tagged Ponter的底层探索
先来看一下关于Tagged Ponter
的底层源码。
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
if (tag <= OBJC_TAG_Last60BitPayload) {
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
从上面的这代码可以看出来,系统调用了_objc_decodeTaggedPointer
和_objc_taggedPointersEnabled
这两个方法对于taggedPointer对象的指针
进行了编码和解编码,这两个方法都是将指针地址和objc_debug_taggedpointer_obfuscator
进行异或操作,我们都知道将a和b异或操作得到c再和a进行异或操作便可以重新得到a的值,通常可以使用这个方式来实现不用中间变量实现两个值的交换。Tagged Pointer
正是使用了这种原理。
在上面讲过,Tagged Pointer
对象指针的值不再是地址了,⽽是真正的值,那我们需要知道的是Tagged Pointer
的值的存储方式。看如下代码:
#define _OBJC_TAG_MASK (1UL << 63)
NSMutableString *mutableStr = [NSMutableString string];
NSString *immutable = nil;
char c = 'a';
do {
[mutableStr appendFormat:@"%c", c++];
immutable = [mutableStr copy];
NSLog(@"0x%lx %@ %@", _objc_decodeTaggedPointer_(immutable), immutable, immutable.class);
} while (((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);
打印结果:
2020-03-25 17:05:22.784213+0800 taggedPointer[76305:3706620] 0xa000000000000611 a NSTaggedPointerString
2020-03-25 17:05:22.784368+0800 taggedPointer[76305:3706620] 0xa000000000062612 ab NSTaggedPointerString
2020-03-25 17:05:22.784481+0800 taggedPointer[76305:3706620] 0xa000000006362613 abc NSTaggedPointerString
2020-03-25 17:05:22.784594+0800 taggedPointer[76305:3706620] 0xa000000646362614 abcd NSTaggedPointerString
2020-03-25 17:05:22.784698+0800 taggedPointer[76305:3706620] 0xa000065646362615 abcde NSTaggedPointerString
2020-03-25 17:05:22.784791+0800 taggedPointer[76305:3706620] 0xa006665646362616 abcdef NSTaggedPointerString
2020-03-25 17:05:22.784874+0800 taggedPointer[76305:3706620] 0xa676665646362617 abcdefg NSTaggedPointerString
2020-03-25 17:05:22.784955+0800 taggedPointer[76305:3706620] 0xa0022038a0116958 abcdefgh NSTaggedPointerString
2020-03-25 17:05:22.785044+0800 taggedPointer[76305:3706620] 0xa0880e28045a5419 abcdefghi NSTaggedPointerString
2020-03-25 17:05:22.785173+0800 taggedPointer[76305:3706620] 0x409bac70e6d7a14b abcdefghij __NSCFString
从上面这段代码的运行结果可以看出当字符串的长度增加到10时,字符串的类型输出是__NSCFString
,当长度小于10时,字符串类型输出是NSTaggedPointerString
,而且其地址都是0xa
开头。回过头来看上述的代码,while中循环条件是为了判断在64位数据中最高是否是1,以此来判断当前的对象是否是一个Tagged Pointer
对象。我们将0xa
转换为二进制1010
,其中最高位1表示是对象是一个Tagged Pointer
对象,余下010(十进制2)
表示的是对象是一个NSString类型。那么对象的值存在哪里呢,拿0xa000000000000611
来说,其中的61就是对应的ASII码中的a。其他的可以照此类推。
如下是系统提供的各种标志位的定义。
enum objc_tag_index_t : uint16_t
{
// 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,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 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_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
系统提供了判断是否是Tagged Pointer的方法
# define _OBJC_TAG_MASK (1UL<<63)
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
更加详细的资料请参阅Tagged Pointers
2.2、NONPOINTER_ISA
NONPOINTER_ISA
同样是苹果公司对于内存优化的一种方案。用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容。isa
指针第一位为 1 即表示使用优化的 isa 指针,这里列出在__x86_64__
架构下的 64 位环境中 isa 指针结构,__arm64__
的架构会有所差别。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
};
#endif
};
nonpointer
:示是否对isa开启指针优化。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等has_assoc
:关联对象标志位,0没有,1存在。has_cxx_dtor
:该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象。shiftcls
:存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针magic
:判断当前对象是真的对象还是一段没有初始化的空间weakly_referenced
:是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快。deallocating
:标志是否正在释放内存。has_sidetable_rc
:是否有辅助的引用计数散列表。当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位。extra_rc
:表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。
其结构如下图所示:
2.3、SideTable
SideTable
在OC中扮演这一个很重要的角色。在runtime中,通过SideTable
来管理对象的引用计数以及weak引用。同时,系统中维护了一个全局的SideTables
,这是一个SideTable
的集合。
来看看SideTable的定义:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
SideTable的定义很清晰,有三个成员:
- spinlock_t slock:自旋锁,用于上锁/解锁 SideTable。
- RefcountMap refcnts:用来存储OC对象的引用计数的
hash表
(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。- weak_table_t weak_table:存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。
关于更多的SideTable的内容请移步我之前的文章iOS底层原理:weak的实现原理,在这篇文章中详细介绍了SideTable。
3、引用计数
3.1、什么是引用计数
摘自百度百科引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。
当一个对象创建并在堆区申请内存时,对象的引用计数为1;当其他的对象需要持有这个对象时,就需要将这个对象的引用计数加1;当其他的对象不再需要持有这个对象时,需要将对象的引用计数减1;当对象的引用计数为0时,对象的内存就会立即释放,对象销毁。
- 调用
alloc、new、copy、mutableCopy
名称开头的方法创建的对象,该对象的引用计数加1。 - 调用
retain
方法时,该对象的引用计数加1。 - 调用
release
方法时,该对象的引用计数减1。 -
autorelease
方法不改变该对象的引用计数器的值,只是将对象添加到自动释放池中。 -
retainCount
方法返回该对象的引用计数值。
3.2、对象持有规则
对象的持有规则如下:
- 自己生成的对象,自己持有。
- 非自己生成的对象,自己也能持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象无法释放。
对象的持有标准在于对象的引用计数的值,那么结合对象创建方式,对象的引用计数加减,对象的销毁大致如下的关系:
| 对象操作 | Objective-C方法 |
|:------:|:------------:|:------------:|:------------:|:------------:|
| 生成并持有对象 | alloc/new/copy/mutableCopy等方法 |
| 持有对象 | retain方法 |
| 释放对象 | release方法 |
|废弃对象 |dealloc方法 |
3.2.1、自己生成的对象,自己持有
使用以下名称开头的方法名意味着自己生成的对象只有自己持有:alloc、new、copy、mutableCopy
。
在OC中对象的创建可以通过alloc
和new
这两种方式来创建一个对象。
NSObject *obj = [NSObject alloc];
NSObject *obj1 = [NSObject new];//等价于 NSObject *obj1 = [[NSObject alloc]init];
关于alloc和new的相关知识请移步之前的文章IOS底层原理之alloc、init和new,在这里就不多加描述。这里着重讲解一下copy
和mutableCopy
,它们意味着对象的拷贝。对象的拷贝需要遵循NSCopying
协议和NSMutableCopying
协议。
@interface Person : NSObject<NSCopying,NSMutableCopying>
@end
@implementation Person
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
Person *person = [[self class] allocWithZone:zone];
return person;
}
- (id)mutableCopyWithZone:(NSZone *)zone{
Person *person = [[self class] allocWithZone:zone];
return person;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];
Person *person1 = [person copy];
Person *person2 = [person mutableCopy];
NSLog(@"person:%p--person1:%p--person2:%p",person,person1,person2);
}
打印结果:
2020-03-26 15:56:26.666859+0800 taggedPointer[89806:4395707] person:0x6000038342c0 retainCount:1
2020-03-26 15:56:26.667011+0800 taggedPointer[89806:4395707] person1:0x6000038342f0 retainCount:1
2020-03-26 15:56:26.667113+0800 taggedPointer[89806:4395707] person2:0x600003834300 retainCount:1
从上面的代码运行的结果可以看出使用copy
和mutableCopy
生成的person1和person2对象以及person对象,三者之间地址是不一样的,说明创建了新的对象。而且它们的引用计数都为1。copy
和mutableCopy
的区别在于,前者生成不可变更的对象,而后者生成可变更的对象。
需要说明的是alloc方法并没有对retainCount进行操作,这里的引用计数之所以为1,那是因为retainCount方法的底层是默认+1的。
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();
}
3.2.1.1、浅拷贝和深拷贝
既然已经说到了copy
和mutableCopy
,那么就来说说浅拷贝和深拷贝。
浅拷贝:对象的指针拷贝,不会开辟新的内存。
深拷贝:拷贝对象本身,会创建一个新的对象,指向不同的内存地址。
对于不可变对象(如NSString、NSArray、NSDictionary)和可变对象(如NSMutableString、NSMutableArray、NSMutableDictionary)用copy和mutableCopy会有一些差别,大致如下表所示:
对于集合类的可变对象来说,深拷贝并非严格意义上的深复制,虽然新开辟了内存,但是对于存放在数组里面的元素来说仍然是浅拷贝。
3.2.2、非自己生成的对象,自己也能持有
用alloc、new、copy、mutableCopy
之外的方法获得的对象,因为并非自己生产持有,所以自己不是该对象的持有者。
//非自己生成的对象,暂时没有持有
id obj = [NSMutableArray array];
//通过retain持有对象
[obj retain];
上述代码中NSMutableArray
通过类方法array
生成了一个对象赋给变量obj
,但变量obj
自己并不持有该对象。使用retain
方法可以持有对象。
3.2.3、不再需要自己持有的对象时释放
自己持有的对象,一旦该对象不再需要时,持有者有义务调用release
方法释放该对象。当然在ARC环境下并不需要开发者主动调用方法,系统会自动调用该方法,但是在MRC环境下需要开发者手动在合适的地方做对象的retain
方法和release
方法的调用。
3.2.4、非自己持有的对象无法释放
对于用alloc、new、copy、mutableCopy
方法生成并持有的对象,或是用retain
方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。而由此以外所得到的对象绝对不能释放。倘若在程序中释放了非自己所持有的对象就会造成崩溃。
// 自己生成并持有对象
id obj = [[NSObject alloc] init];
//释放对象
[obj release];
//再次释放已经非自己持有的对象,应用程序崩溃
[obj release];
释放了非自己持有的对象,肯定会导致应用崩溃。因此绝对不要去释放非自己持有的对象。
3.3、alloc、retain、release、dealloc、autorelease实现
3.3.1、alloc实现
总结一句话就是alloc
创建了对象并且申请了一块不少于16字节的内存空间。关于alloc的实现请移步之前的文章IOS底层原理之alloc、init和new,在这里就不多加描述。
3.3.2、retain实现
在前面的小节内容中,讲到在isa
的bits
中的extra_rc
字段和SideTable
结构中的RefcountMap refcnts
都有存储引用计数,那么在这两者之间会有什么联系呢?下面通过retain的源码来分析引用计数的存储。
id objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
首先是objc_retain
方法,在该方法内部会现有一个判断当前对象是否是TaggedPointer
,如果是则返回,否则调用retain
方法。通过这里我们也可以看到 TaggedPointer
对象并不做引用计数处理。
inline id objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
retain
方法内部其实很简单,就是一个判断,然后调用rootRetain
方法。其中fastpath
是大概率发生的意思。
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
//如果是TaggedPointer 直接返回
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable =false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
// 如果isa未经过NONPOINTER_ISA优化
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();//引用计数存储于SideTable中
}
// don't check newisa.fast_rr; we already called any RR overrides
//检查对象是都正在析构
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
//isa的bits中的extra_rc进行加1
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//如果bits的extra_rc已经存满了,则将其中的一半存储到sidetable中
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;//extra_rc置空一半的数值
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//将另外的一半引用计数存储到sidetable中
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
rootRetain
方法是retain引用计数的核心方法。我们可以看到方法做了如下几方面的工作:
- 判断当前对象是否一个
TaggedPointer
,如果是则返回。- 判断
isa
是否经过NONPOINTER_ISA
优化,如果未经过优化,则将引用计数存储在SideTable
中。64位的设备不会进入到这个分支。- 判断当前的设备是否正在析构。
- 将
isa
的bits
中的extra_rc
进行加1操作。- 如果在
extra_rc
中已经存储满了,则调用sidetable_addExtraRC_nolock
方法将一半的引用计数移存到SideTable
中。
3.3.3、release实现
在上一章节中我们分析了引用计数的存储在bits
和SideTable
中的存储,那么作为释放对象的release
又是怎么对引用计数进行减1操作的呢?
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
首先是objc_release
方法,在该方法内部会现有一个判断当前对象是否是TaggedPointer
,如果是则返回,否则调用release
方法。
inline void
objc_object::release()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
rootRelease();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
release
方法内部其实很简单,就是一个判断,然后调用rootRelease
方法。其中fastpath
是大概率发生的意思。
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
//判断是否是TaggedPointer
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//如果isa是未经过NONPOINTER_ISA优化,则对SideTable中的引用计数进行清理
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
//isa的bits的extra_rc减1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
//extra_rc已经置空
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
//isa的has_sidetable_rc表示是否有辅助的引用计数散列表
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}
// Try to remove some retain counts from the side table.
//
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.
if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}
// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
// Really deallocate.
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
if (slowpath(sideTableLocked)) sidetable_unlock();
__sync_synchronize();
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return true;
}
rootRelease
方法是release引用计数的核心方法。我们可以看到方法做了如下几方面的工作:
- 判断当前对象是否一个
TaggedPointer
,如果是则返回。- 判断
isa
是否经过NONPOINTER_ISA
优化,如果未经过优化,则清理在SideTable
中的引用计数。64位的设备不会进入到这个分支。- 将
isa
的bits
中的extra_rc
进行减1操作。- 如果
extra_rc
已经置空,则清理SideTable
中的引用计数。- 尝试将
SideTable
中的引用计数移存到isa
的bit
中。
3.3.4、autorelease实现
说到Objective-C内存管理,就不能不提autorelease
。 顾名思义,autorelease
就是自动释放。这看上去很像ARC,但实际上它更类似于C语言中自动变量(局部变量)的特性。autorelease
会像C语言的局部变量那样来对待对象实例。当其超出作用域时,对象实例的release
实例方法被调用。另外,同C语言的局部变量不同的是,编程人员可以设置变量的作用域。
autorelease
的具体使用方法如下:
- 生成并持有
NSAutoreleasePool
对象。 - 调用已分配对象的
autorelease
实例方法。 - 废弃
NSAutoreleasePool
对象。
来看autorelease的代码实现。
id objc_autorelease(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
首先是objc_autorelease
方法,在该方法内部会现有一个判断当前对象是否是TaggedPointer
,如果是则返回,否则调用autorelease
方法。
inline id
objc_object::autorelease()
{
if (isTaggedPointer()) return (id)this;
if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}
autorelease
方法内部会再次判断当前对象是否是TaggedPointer
,如果是则返回,否则调用rootAutorelease
方法。其中fastpath
是大概率发生的意思。
inline id objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
在rootAutorelease
的代码核心就是将当前对象添加到AutoreleasePool
自动释放池中。
3.3.5、dealloc对象销毁
当对象的引用计数为0时,底层会调用_objc_rootDealloc
方法对对象进行释放,而在_objc_rootDealloc
方法里面会调用rootDealloc
方法。如下是rootDealloc
方法的代码实现。
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
- 首先判断对象是否是
Tagged Pointer
,如果是则直接返回。- 如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用
!isa.weakly_referenced
、没有关联对象!isa.has_assoc
、没有自定义的C++析构方法!isa.has_cxx_dtor
、没有用到SideTable来引用计数!isa.has_sidetable_rc
则直接快速释放。- 如果不能满足2中的条件,则会调用
object_dispose
方法。
object_dispose
方法很简单,主要是内部调用了objc_destructInstance
方法。
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.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
上面这一段代码很清晰,如果有自定义的C++析构方法,则调用C++析构函数。如果有关联对象,则移除关联对象并将其自身从AssociationManager
的map中移除。调用clearDeallocating
方法清除对象的相关引用。
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
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();
}
assert(!sidetable_present());
}
clearDeallocating
中有两个分支,先是判断对象是否采用了优化isa引用计数,如果没有的话则需要清理对象存储在SideTable
中的引用计数数据。如果对象采用了优化isa引用计数,则判断是都有使用SideTable
的辅助引用计数(isa.has_sidetable_rc
)或者有weak引用(isa.weakly_referenced
),符合这两种情况中一种的,调用clearDeallocating_slow
方法。
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this]; // 在全局的SideTables中,以this指针为key,找到对应的SideTable
table.lock();
if (isa.weakly_referenced) { // 如果obj被弱引用
weak_clear_no_lock(&table.weak_table, (id)this); // 在SideTable的weak_table中对this进行清理工作
}
if (isa.has_sidetable_rc) { // 如果采用了SideTable做引用计数
table.refcnts.erase(this); // 在SideTable的引用计数中移除this
}
table.unlock();
}
clearDeallocating_slow
方法中有两个分支,一是如果对象被弱引用,则调用weak_clear_no_lock
方法在SideTable
的weak_table
中对this进行清理工作。二是如果采用了SideTable
做引用计数,则在 SideTable
的引用计数中移除this。
3.4、ARC下的规则
- 不能显式的调用retain、release、retainCount、autorelease。
- 不能使用NSAllocateObject和NSDeallocateObject。
- 必须遵守内存管理的命名规则。
- 不要显式的调用dealloc。
- 使用@autoreleasepool代替NSAutoreleasePool。
- 不能使用区域NSZone。
- 对象变量不能作为C语言结构体的成员。
- 显式转换"id"和"void *"。
4、总结
- IOS中内存布局区域大概分为五个区域:
栈区、堆区、BSS段、数据段、代码段
。 - OC中对内存优化管理的方案有如下几种形式:
Tagged Ponter、NONPOINTER_ISA 、SideTable
。 -
Tagged Pointer
是专⻔⽤来存储⼩的对象,例如NSNumber,NSDate
等。其指针的值不再是地址,而是真正的值。所以,它的内存并不存储在堆中,也不需要malloc
和free
。在内存读取上有着3倍的效率,创建时⽐以前快106倍。 -
NONPOINTER_ISA
就是用一部分额外空间存储其他内容,这样提高了内存的利用率。 -
SideTable
是一个hash表结构,主要是针对引用计数和弱引用表进行相关操作。 - 对象的持有规则:自己生成的对象,自己持有;非自己生成的对象,自己也能持有;不再需要自己持有的对象时释放;非自己持有的对象无法释放。
-
alloc/new/copy/mutableCopy
等方法生成并持有对象,retain
方法引用计数加1,release
方法引用计数减1,dealloc
方法销毁对象。 -
autorelease
方法不改变该对象的引用计数器的值,只是将对象添加到自动释放池中。 - ARC下不能显式的调用
retain、release、retainCount、autorelease
。