iOS中的内存管理
堆栈区
堆heap:内存由程序员分配、释放,地址不连续,空间大,在OC中一般用来对对象的内存管理。
栈stack:编译器自动分配释放,线性结构,连续的内存区域,速度较快,空间小,一般用来管理基础数据类型。
全局区static:存储全局变量和静态变量(int a = 0,初始化区;char *p,未初始化区)。
NSObject *obj = [[NSObject alloc] init];
//*obj指针在栈内存中,会被系统自动回收;而指针指向的NSObject对象是在堆内存中,需要手动回收。
ARC
代码编译阶段,在上下文中自动成对插入MRC下的retain和release方法,保证通过引用计数正确的管理内存。
引用计数
NSObject *obj = [[NSObject alloc] init];
// 对象的引用计数为1,被obj指针引用。
注:打印出的引用计数永远不为0,因为标记析构后直接释放不再设置为0,减少一次内存的操作,加速释放。
iOS中引用计数的存储方案
1、TaggedPointer下的小内存对象,直接返回指针值作为引用计数。NSString会根据长度(小于60字节)决定是否使用。但深拷贝后一定变为普通指针。
2、OC2.0+64位,使用对象的isa指针的第一位标记是否使用了优化后的isa,后8位来存储引用计数(溢出后移出部分正常管理)。
3、Runtime使用一张散列hash表(SideTables)来管理,SideTable中的RefcountMap属性。
注:objc_object::isTaggedPointer() 获取TAG_MASK标识位以判断是否使用了TaggedPoint。
SideTable
SideTables全局hash数组长度64,实际是StripedMap类型,里面储存了64个SideTable,许多obj共用一个SideTable来存储引用计数和弱引用表相关信息。
SideTable结构体
自旋锁spinlock_t slock// 保证原子操作防止多线程读取问题
引用计数表RefcountMap refcnts// 散列表结构存储对象的持有者地址和引用计数,Zombie异常时也能定位对象地址信息。
弱引用表weak_table_t weak_table// 保存了许多对象的,所有的weak引用,对象地址作为key,weak_entries作为值保存所有指向该对象的weak指针,dealloc时把所有weak指针设为nil,避免野指针。
注1:static SideTable *tableForPointer(const void *p);
// 获取对象地址的sidetable
注2:一个sidetable对应一个weaktable,一个weaktable对象中有无数个weakentry(通过对象地址作为key获取对应的),每个weak_entry_t保存了这个对象的所有弱引用。
获取引用计数:retainCount
RunTime会调用objc_object的rootRetainCount()方法
1、判断储存逻辑(TaggedPointer直接获取 / 优化后的isa,指针的后19位即extra_rc变量 / 散列表中获取)
2、sidetable_retainCount(),先SideTable::tableForPointer(this)获取SideTable对象,table.refcnts即引用计数的hash表
3、it != table->refcnts.end()(如果相等则引用计数返回1)根据键值对以对象为key获取引用计数的值并+1返回,所以实际计数应该为retainCount-1。
注:refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT(=2);
// 返回时做了向右位移两位的操作,因为前两个bit,WEAKLY_REFERENCED标识是否有weak对象;DEALLOCATING是否正在析构。
修改引用计数:retain和release
retain方法
底层调用_objc_rootRetain、objc_object::rootRetain()、objc_object::sidetable_retain()、_slow
四步方法其中最重要的是增加引用计数的id objc_object::sidetable_retain()虚函数,详细实现摘要如下:
SideTable& table = SideTables()[this];
// 传入对象地址获取对应的SideTable对象。
size_t& refcntStorage = table.refcnts[this];
// 获取 引用计数 的引用
!(refcntStorage & SIDE_TABLE_RC_PINNED) // 没有越界
refcntStorage += SIDE_TABLE_RC_ONE;
// 引用计数增加(实际增加了 1UL<<2 == 4)
注1:refcntStorage后两位被weak和析构状态占领,首位标识越界,所以不是增加1。
注2:refcnts为散列表,可能存了多个引用计数以处理引用计数越界情况,retainCount方法可以证明。
uintptr_t objc_object::sidetable_retainCount()// 引用计数总返回1+计数表,所以总不为0
{ it != table.refcnts.end()refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
release方法
四步方法与retain类似,其中减少引用计数的函数uintptr_t objc_object::sidetable_release(bool performDealloc)实现总结如下:
1、内部判断是否dealloc:it == table.refcnts.end()(引用计数为表中最后一个,直接标记析构,table.refcnts[this] = SIDE_TABLE_DEALLOCATING)
2、it->second < SIDE_TABLE_DEALLOCATING(在-1前验证引用计数是否为0,如果是,标记正在析构并发送dealloc消息,否则才-1。do_dealloc=true;
it->second |= SIDE_TABLE_DEALLOCATING;
)
3、it->second -= SIDE_TABLE_RC_ONE(引用计数-1,实际偏移两位)
注:SIDE_TABLE_DEALLOCATING作为引用计数归0的判断,减少了标记变量内存的额外占用,也避免负数产生。
注:为什么isa中的extra_rc、sidetable中的refcnts中,保存的值都是真正的引用计数-1?因为获取时是+1后返回的,保证了释放时-1不会出现负数。
alloc、new、copy、mutableCopy
都会调用retain,让引用计数+1class_createInstance()_class_createInstanceFromZone()calloc() //相当于malloc() + memset(),使内存区域初始化为0
注1:new = alloc + init,但alloc的底层使用了zone把关联对象分配到相邻内存,降低了耗时。
注2:copy不可变下是浅拷贝,对可变对象是深拷贝(NSArray会使用懒拷贝策略,先增加引用计数,内容变化时再真正拷贝)
注3:无论可变不可变,mutableCopy都是深拷贝。
Autorelease
即手动把对象放入AutoreleasePool自动释放池中。当前runloop迭代结束释放,每个RunLoop中都在Entry监听中加入了自动释放池的push,在beforeWaiting的监听中加入了pop。
注1:比如viewDidLoad和viewWillAppear是同一个RunLoop,所以局部autorelease变量在花括号结束时自动释放是不准确的。
注2:@autoreleasepool {} 可以手动干预释放时机。
dealloc
_objc_rootDealloc(self) → objc_object::rootDealloc()虚函数 → isTaggedPointer + object_dispose(this);
→ objc_destructInstance(obj)销毁了内存实例,并析构 → obj->clearDeallocating()清理关联对象、清除weak引用、清除多余的retain count →free(obj)释放内存空间。
【iOS中的内存管理】注:bool assoc = obj->hasAssociatedObjects() + if (assoc) _object_remove_assocations(obj)清理关联对象。
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- JS中的各种宽高度定义及其应用
- 我眼中的佛系经纪人
- 《魔法科高中的劣等生》第26卷(Invasion篇)发售
- 2020-04-07vue中Axios的封装和API接口的管理
- Android中的AES加密-下
- 放下心中的偶像包袱吧
- C语言字符函数中的isalnum()和iscntrl()你都知道吗
- C语言浮点函数中的modf和fmod详解