本文章属于原创,转载请注明出处
参考资料
sunnyxxx
深入理解Objective C的ARC机制
Objective-C引用计数原理
基础参考 iOS高级编程
基础
内存管理的思考方式
- 自己生成的对象自己持有
- 非自己生成的对象自己也能持有
- 不需要自己持有的对象释放
- 非自己持有的对象无法释放
内存管理语义
- 以下方法表示自己生成的对象自己持有
- alloc/new/copy/mutableCopy/retain
- 非自己生成的对象可以使用retain持有
// 生成非自己持有对象
id obj = [NSMutableArray array];
// 自己持有对象
[obj retain];
// 不需要时释放
[obj release];
// autorealease 使对象超出指定范围并可以正确释放
- (id)object {
id obj = [[NSObject alloc] init];
[objc autorelease];
return obj;
}
__strong & __weak & __unsafe_unretained
引用循环容易发生内存泄露,内存泄漏是指应当废弃的对象在超过其生命周期后继续存在
- __strong
- 可以使用__strong持有非自己生成并持有的对象
- __strong完全遵循ARC有效时的内存管理语义,使用__strong,__weak,__autoreleasing修饰符不赋值时对象的值为nil
- __weak
- 两个__strong的对象互相引用对方会造成引用循环并发生内存泄露
- 使用__weak给其中一个对象赋值可以避免引用循环,废弃对象的同时__weak赋值的对象的值变成nil
- 不能使用__weak直接初始化对象,初始化的一瞬间,会变为nil
id __weak obj = [[NSObject alloc]init];
// obj = nil
- unsafe_unretained
- unsafed_unretained是不安全的,ARC有效时内存管理是编译器的工作,被此修饰符修饰的变量的内存管理不属于编译器
- 被修饰对象即不持有强引用也不持有若引用,使用unsafed_unretained时要保证赋值给__strong的对象的确存在,不然程序会崩溃
id __unsafe_unretained obj = [[NSObject alloc]init];
// 即使有 unsafed 标示,编译器还是会警告,这里的值瞬间变为 nil
- __autoreleasing
- 在ARC有效的情况下,使用@autoreleasePool代替NSAutoreleasePool类,__autoreleasing修饰符,代替autorelease方法
- 很少显示的声明__autoreleasing,非自己持有的对象自动声明为__autoreleasing,在作为返回值是自动声明为__autoreleasing
- 被声明为__weak的变量自动被声明为__autoreleasing,将其注册到autoreleasePool里,在@autoreleasePool的块结束前,都能确保对象存在
id __weak obj = obj1;
id __autoreleasing tmp = obj;
- 最后一个显示声明__autoreleasing的例子,id指针或对象指针在没有被显示声明的时候声明为__autoreleasing,主要一个用处是*error传递NSError对象的时候
id *obj = [[NSObject alloc]init];
id __autoreleasing *obj = [[NSObject alloc]init];
ARC规则
ARC有效时
- 不能使用retain/release/retainCount/autorelease/new
- 不能使用NSAllocateObject/NSDeallocateObject
- 必须遵循内存管理方法命名规则
- 不显示使用dealloc
- 使用@autoreleasePool代替NSAutoreleasePool
- 不使用区域NSZone
- 对象型变量不能作为C语言结构体变量
- 显示转换id和void*在ARC下会报错
ARC有效时的属性
属性的内存管理语义对应的所有权修饰符
- assign == __unsafe_unreatined
- copy == _strong 但赋值被复制的对象
- retain == __strong
- strong == __strong
- unsafe_unretained == __unsafe_unretained
- weak == __weak
原理
在Objective-C中,有三种类型是适用ARC的
- block
- Objective-C对象,如id,Class,NSError *等
- 以attribute作为标记的
CF开头的都是适用用ARC的
引用计数的存储方式
主要有三种:
- TaggedPointer:将其指针值作为引用计数返回
- isa:有的对象会将isa指针的一部分用于存储引用计数
- Sidetable:使用散列表来管理引用计数
TaggedPoint
判断标志位是否为一,确定对象有没有使用TaggedPoint,id的本质就是objc_object结构体的指针类型,可以使用id的方法isTaggedPoint()方法来判读其是否使用TaggedPoint来存储引用计数
#if SUPPORT_MSB_TAGGED_POINTERS
# define TAG_MASK (1ULL<<63)
#else
# define TAG_MASK 1
inline bool
objc_object::isTaggedPointer()
{
#if SUPPORT_TAGGED_POINTERS
return ((uintptr_t)this & TAG_MASK);
#else
return false;
#endif
}
isa
在64位环境并且是Objective-C 2.0的情况下,有些对象会使用isa的一部分作为引用计数存储
因为64位环境存储一个指针地址很浪费,所以作为优化,使用isa指针的一部分来存储,
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 14;
# define RC_ONE (1ULL<<50)
# define RC_HALF (1ULL<<13)
};
# else
// Available bits in isa field are architecture-specific.
# error unknown architecture
# endif
// SUPPORT_NONPOINTER_ISA
#endif
};
SUPPORT_NONPOINTER_ISA用来标记是否使用isa指针存储引用计数
#if !__LP64__ || TARGET_OS_WIN32 || TARGET_IPHONE_SIMULATOR || __x86_64__
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
isa指针中一些变量的定义如下
- indexed:0 表示普通的 isa 指针,1 表示使用优化,存储引用计数
- has_assoc:表示该对象是否包含 associated object,如果没有,则析构时会更快
- shiftcls:类的指针
- weakly_referenced:表示该对象是否有过 weak 对象,如果没有,则析构时更快
- has_sidetable_rc:表示该对象的引用计数值是否过大无法存储在 isa 指针
- extra_rc:存储引用计数值减一后的结果
isa指针不一定会存储引用计数,has_sideable_rc中,如果为1,就是用Sidetable来存储
散列表 sidetable
sidetable里存储的引用计数,总是为真正的引用计数减1。使用sidetable来存储引用计数,当调试时,即使对象内存地址损坏,也能使用散列表来找到损坏的对象的地址
sidetable用于管理引用计数表和weak表,使用 spinlock_t slock来保证可能发生的竞态条件
spinlock_t slock;//保证原子操作的自选锁
RefcountMap refcnts;//保存引用计数的散列表
weak_table_t weak_table;//保存 weak 引用的全局散列表
weak表的作用是在对象全部dealloc的时候,将weak的对象都设置为nil,避免悬挂指针
weak表是一个全局的表,其中对象为键,weak_entry_t 为值,weak_entry_t 中保存了所有指向该对象的weak指针
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
获取引用计数
- taggedPoint可以直接获取引用计数的值,isa指针使用其中一个变量保存
- sidetbale:使用sidetable_retainCount()方法,其实现是获取当前sidetable对象的实例,其refcnts属性是存储引用计数的散列表,使用迭代器匹配到当前实例的键值对,返回其 值 + 1
retain方法
- (id)retain {
return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}
可以看出retain先判断是否支持isTaggedPoinnter,然后调用sidetable_retain()方法操作sidetable的refcnts属性,将引用计数加1,
再看看sidetable的实现
id objc_object::sidetable_retain()
{
//获取table
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;
}
release
sidetable_release()返回是否执行dealloc方法
bool
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable *table = SideTable::tableForPointer(this);
bool do_dealloc = false;
if (spinlock_trylock(&table->slock)) {
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;
}
spinlock_unlock(&table->slock);
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
return sidetable_release_slow(table, performDealloc);
}
在执行release方法前,存储引用计数会先预留一个1,这也是为什么总返回真正的引用计数减1,因为release方法总会将引用计数减1,在减1之前先看存储引用值是否为0,当为0时,直接进行dealloc操作,否则就减1,这样避免了产生负数
Autorelease
autorelease对象什么时候释放
- 在手动加入autoreleasePool的情况下,ARC有效时,在@autoreleasePool的大括号结束前释放对象,MRC下,调用[pool drain]时释放对象
- 在没有手动加入autoreleasePool的情况下,Autorelease对象是在当前Runloop迭代结束时释放的,能释放的正确的原因是,系统在每个runloop迭代中,都加入自动释放池的push和pop
小实验
__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = [NSString stringWithFormat:@"xxx"];
// str是一个autorelease对象,设置一个weak的引用来观察它
reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", reference);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%@", reference);
}
由于viewController在loadview之后便加到了windom视图的层级上,所以viewDidLoad和viewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个对象依然有值
Autorelease原理
- autoreleasePoolPage
在ARC下,使用@autoreleasePool {} 来使用一个AutoreleasePool,随后编译器将其改写为
void *context = objc_autoreleasePoolPush();
// 括号里的代码
objc_autoreleasePoolPop(context);
这两个函数都是对autoreleasePoolPage的封装,因此关键在这个类
// AutoreleasePoolPage
{
magic_t const magic;
id *next;
phread_t const thread;
AutoreleasePoolPage *const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwait;
}
- autoreleasePool并没有单独的数据结构,而其是有若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应上面的child和parent指针
- autoreleasePool是按线程一一对应的
- autoreleasePoolPage会为对象申请4096的内存空间(一页的大小),除了上面实例变量所占的空间,剩下的空间全部用来存储autorelease对象的地址
- id *next指针作为标记新加进来(add)的autorelease对象的下一个
- 一个AutoreleasePoolPage的空间被占满,会创建一个新的AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的autoreleasePoolPage中加入
// end 栈顶
// <-- next指针指向
// id objN 最新的autorelease对象的位置
// ...
// id obj2
// id obj1(begin)栈底
// 类实例所占内存
如上,当这一页再加入一个autorelease对象就要满了的时候(next指针马上指向栈顶),这时执行上面的操作,新建下一页Page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底,然后继续向新的栈顶添加对象
所以向一个对象发送autorelease消息就相当于把对象加到autoreleasePoolPage栈顶的next指针指向的位置
释放时刻
// AutoreleasePoolPage
// (end) 栈顶
// <-- next指针指向
// id objN 最新的autorelease对象的地址
// ...
// obj3
// 0 哨兵位置
// id obj2
// id obj1 (begin)栈底
// 类实例所占内存
每当进行AutoreleasePoolPush调用时,runtime就向当前的AutoreleasePoolPage中add一个哨兵对象,值为0(nil),Page就变成上面的样子
objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是
- 根据传入的哨兵对象找到哨兵对象所处的page
- 在当前page中,在晚于哨兵对象插入的所有autorelease对象都发送一次release消息,并移动next指针到正确位置
- 从最新加入的对象一直向前清理,可以跨越若干个page,直到哨兵所处的page
刚才的objc_autoreleasePoolPop执行后,变成如下
// (end)栈顶
// ...
// <-- next指针指向
// id obj2
// id obj1 (栈底)
//
嵌套的autoreleasrPool
- pop的时候总是释放到上次的push为止,多层pool是多个哨兵对象,释放时每次一层,互补影响