iOS内存管理
iOS内存管理需要了解这几个方面:
- 内存布局
- 引用计数
- 自动释放池
- 循环引用和core foundation对象的内存管理
一、内存布局
程序在内存空间分布为:
- 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放。
- 文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放。
- 程序代码区:存放函数体的二进制代码。
二、引用计数
引用计数规则
- 每个对象都有一个『被引用计数』
- 引用计数器:一个简单而有效的管理生命周期的方式。
- 对象被持有,『被引用计数』+1 (alloc/new/copy/mutableCopy这些方法在对象创建的时候引用计数器自动+1)
- 对象被放弃持有,『被引用计数』-1
- 『引用计数』=0,释放对象
引用计数存储
在runtime的源码NSobject.mm 中找到函数
id objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
点击进去可以看到
objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
//在次点击以下函数
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
从源码中可以看出,先判断是不是taggerPointer。
taggerPointer是64位开始引入的,用于优化NSNumber、NSDate、NSString等小对象的存储。
在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值;使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中;当指针不够存储数据时,才会使用动态分配内存的方式来存储数据;objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销;
在64位iOS平台中,通过最高有效位是否是1 来判断是不是taggerPointer;
在继续往下查看源码之前,大家都知道,
64位之前isa就是个普通的指针,存储着类/元类对象的内存地址,64位之后isa 进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息;
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;//是一个存放着对象引用计数的散列表
weak_table_t weak_table;
}
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;
}
说到内存管理,不得不提“僵尸对象+ 空指针+野指针”
1.僵尸对象:一个OC对象引用计数器为0,对象内存已经被回收但是数值(对象)还存在的对象被成为僵尸对象,该对象不能被引用也不可以访问
2.空指针:空指针是有效指针,值为nil,NULL ,0,给空指针发消息,不会报错,只是不响应而已。他是一个没有指向任何东西的指针。
3.野指针:指针指向了一个已经被销毁的对象的内存地址,向野指针发消息会报EXC_BAD_ACCESS,而导致程序崩溃
所有权
-
__strong
-
__weak
-
__unsafe_unretaied
-
__autoreleasing
属性修饰符与所有权的关系:
-
assign 对应的所有权类型是 __unsafe_unretained。
-
copy 对应的所有权类型是 __strong。
-
retain 对应的所有权类型是 __strong。
-
strong 对应的所有权类型是 __strong。
-
unsafe_unretained对应的所有权类型是__unsafe_unretained。
-
weak 对应的所有权类型是 __weak。
这里重点说下weak。
weak
对于__weak内存管理也借助了类似于引用计数表的散列表,它通过对象的内存地址做为key,而对应的__weak修饰符变量的地址作为value注册到weak表中,在上述代码中objc_initweak就是完成这部分操作,而objc_destroyWeak
则是销毁该对象对应的value。当指向的对象被销毁时,会通过其内存地址,去weak表中查找对应的__weak修饰符变量,将其从weak表中删除。所以,weak在修饰只是让weak表增加了记录没有引起引用计数表的变化.
对象通过objc_release释放对象内存的动作如下:
- 从weak表中获取已废弃对象内存地址对应的所有记录
- 将已废弃对象内存地址对应的记录中所有以weak修饰的变量都置为nil
- 从weak表删除已废弃对象内存地址对应的记录
- 根据已废弃对象内存地址从引用计数表中找到对应记录删除
StripedMap
runtime有一张表,StripedMap,用来存储sidetable,key值为object对象
//sidetable表
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
//取sidetable
oldTable = &SideTables()[oldObj];
sidetable
sidetable,用来存储weak_table_t表和引用计数表
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
执行weak操作时,会先查询是否有旧值,如果有执行weak_unregister_no_lock操作,
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
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.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
然后执行weak_register_no_lock操作生成一个新的地址。
绑定location的地址和newobject的地址。并将新地址添加进weak_table_t的weak_entry_t中
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
weak_entry_t是个指针数组,在weak_table_t存储空间不够时,自动扩容。
,如果这个entry存储的个数大于4,则使用outline模式,
if (! entry->out_of_line()) {
// Try to insert inline.
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
// Couldn't insert inline. Allocate out of line.
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// This constructed table is invalid, but grow_refs_and_insert
// will fix it and rehash it.
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
new_referrers[i] = entry->inline_referrers[i];
}
entry->referrers = new_referrers;
entry->num_refs = WEAK_INLINE_COUNT;
entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
entry->mask = WEAK_INLINE_COUNT-1;
entry->max_hash_displacement = 0;
}
assert(entry->out_of_line());
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
return grow_refs_and_insert(entry, new_referrer);
}
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != nil) {
hash_displacement++;
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
}
if (hash_displacement > entry->max_hash_displacement) {
entry->max_hash_displacement = hash_displacement;
}
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
entry->num_refs++;
函数的后半部分处理使用outline数组的情况,如果outline数组的使用率在75%及以上,那么调用grow_refs_and_insert函数进行扩充,并且插入新的弱引用。否则就直接进行hash运算插入,过程和weak_table_t的插入过程基本相同
销毁:
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
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();
}
}
}
weak_entry_remove(weak_table, entry);
}
自动释放池
在iOS程序启动后,主线程会自动创建一个RunLoop,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不一定非得显示创建 Pool 了。
Toll-Free Bridging
MRC 下的 Toll-Free Bridging 因为不涉及内存管理的转移,相互之间可以直接交换使用,当使用 ARC 时,由于Core Foundation 框架并不支持 ARC,此时编译器不知道该如何处理这个同时有 ObjC 指针和 CFTypeRef 指向的对象,所以除了转换类型,还需指定内存管理所有权的改变,可通过 __bridge、__bridge_retained 和 CFBridgingRetain、__bridge_transfer 和 CFBridgingRelease。
__bridge
只是声明类型转变,但是不做内存管理规则的转变
__bridge_retained or CFBridgingRetain
表示将指针类型转变的同时,将内存管理的责任由原来的 Objective-C 交给Core Foundation 来处理
__bridge_transfer or CFBridgingRelease
这个修饰符和函数的功能和上面那个 __bridge_retained 相反,它表示将管理的责任由 Core Foundation 转交给 Objective-C,即将管理方式由 MRC 转变为 ARC。