iOS底层-内存管理(二)
OC对象的内存管理
- 在iOS中,使用引用计数来管理OC对象的内存
- 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
- 内存管理的经验总结
- 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
- 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
- 以上的示例以及详解请到OC语法-内存管理部分,这里不再详细讲
- 可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
引用计数的存储
-
前面讲runtime的isa的本质的时候讲过,isa使用位域技术存储一些数据,其中就有引用计数器的值
uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 19;
- 里面存储的值是引用计数器减1
- 当引用计数器是否过大无法存储在isa中时,has_sidetable_rc至1,引用计数会存储在一个叫SideTable的类的属性中
-
在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中
struct SideTable { spinlock_t slock; //map容器,相当于字典,用于存储引用计数器的值 RefcountMap refcnts; //用于存储当前对象的弱引用计数器值 weak_table_t weak_table; ... }
- refcnts是一个存放着对象引用计数的散列表
-
retainCount方法的源码
//返回引用计数器的值 - (NSUInteger)retainCount { return ((id)self)->rootRetainCount(); } //获取引用计数器的值 inline uintptr_t objc_object::rootRetainCount() { //1. 如果是TaggedPointer(当前不是对象类型),直接返回自己 if (isTaggedPointer()) return (uintptr_t)this; //可能会多线程同时访问一个容器,因此需要加锁 //加锁 sidetable_lock(); //2. 拿到isa的bits isa_t bits = LoadExclusive(&isa.bits); ClearExclusive(&isa.bits); //3. 判断是否采用位域技术优化,即isa位域存储优化 if (bits.nonpointer) { //3.1 是isa优化,那么就到isa的位中分离出来 // extra_rc+1 = 引用计数器的值 uintptr_t rc = 1 + bits.extra_rc; //3.2 判断extra_rc是否不够存储,即是否存满,足够存储,直接返回r if (bits.has_sidetable_rc) { //3.3 不够存储,到sidetable找,此时的引用计数器=extra_rc+1 + sidetable存储的值 rc += sidetable_getExtraRC_nolock(); } //解锁 sidetable_unlock(); return rc; } //解锁 sidetable_unlock(); //4. 没有采用位域技术,即isa仅仅存储的是地址,那么就直接到sidetable类中找 return sidetable_retainCount(); } /** 额外的引用计数器值 当前isa的extra_rc位不够存储,即has_sidetable_rc为1时,多余的计数器值会存储到这里 @return 额外的计数器值 */ size_t objc_object::sidetable_getExtraRC_nolock() { assert(isa.nonpointer); SideTable& table = SideTables()[this]; //map容器查找,采用指示器 RefcountMap::iterator it = table.refcnts.find(this); //没有找到 if (it == table.refcnts.end()) return 0; //找到了,返回: it->second值,右移2位 else return it->second >> SIDE_TABLE_RC_SHIFT; } /** isa是非位域优化指针时 直接到sidetable类中查找引用计数器的值 @return 值 */ uintptr_t objc_object::sidetable_retainCount() { //拿到table SideTable& table = SideTables()[this]; size_t refcnt_result = 1; table.lock(); RefcountMap::iterator it = table.refcnts.find(this); if (it != table.refcnts.end()) {//指示器不是最后一位,即计数器的值不为0 //返回引用计数器的值:it->second 右移2位,然后+1 // this is valid for SIDE_TABLE_RC_PINNED too refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT; } table.unlock(); return refcnt_result; }
weak指针的原理
-
示例
__strong Person *person1; //弱引用,安全,一旦指向的对象销毁了,会将这个指针设置为nil __weak Person *person2; //弱引用,不安全,一旦指向的对象销毁了,这个指针仍然指向那个已经销毁的内存,会出现野指针错误 __unsafe_unretained Person *person3; NSLog(@"1111"); { Person *person = [[Person alloc] init]; person3 = person; } //当为person3时会崩溃 NSLog(@"222222==%@",person3);
-
打印:
//person1指向 1111 222222 -[Person dealloc] //person2指向 1111 -[Person dealloc] 222222 //person3指向 1111 -[Person dealloc] //崩溃
-
weak的实现原理:就是想知道,weak指针如何办到,当他指向的对象销毁时,将当前的指针至nil的
- 从上面的代码可以看到,当weak时,对象会调用dealloc,那么我们分析一下dealloc的原理
- 当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是
- 在objc源码中的NSObject.mm文件中
-
dealloc方法
//对象的dealloc方法 // Replaced by NSZombies - (void)dealloc { _objc_rootDealloc(self); }
-
_objc_rootDealloc
void _objc_rootDealloc(id obj) { assert(obj); obj->rootDealloc(); }
-
rootDealloc
/** 对象dealloc方法的本质 */ inline void objc_object::rootDealloc() { //如果是TaggedPointer直接返回,不需要dealloc if (isTaggedPointer()) return; // fixme necessary? //如果是isa位域优化指针 && 没有被弱引用指向过(!isa.weakly_referenced)&& 没有设置关联对象(!isa.has_assoc) && 没有C++析构函数(!isa.has_cxx_dtor)&& 引用计数器没有溢出(isa内部够存储)(!isa.has_sidetable_rc) 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); } }
-
object_dispose
id object_dispose(id obj) { if (!obj) return nil; objc_destructInstance(obj); free(obj); return nil; }
-
objc_destructInstance
/** dealloc的本质 */ void *objc_destructInstance(id obj) { if (obj) { //是否有C++析构函数 // Read all of the flags at once for performance. bool cxx = obj->hasCxxDtor(); //是否有关联对象 bool assoc = obj->hasAssociatedObjects(); // This order is important. //调用C++析构函数,消除成员变量 if (cxx) object_cxxDestruct(obj); //移除关联对象 if (assoc) _object_remove_assocations(obj); //将指向当前对象的弱指针至为nil obj->clearDeallocating(); } return obj; }
-
clearDeallocating
- 这个方法内部会找到当前对象的sidetable属性的weak_table集合存储的弱引用指针
- 然后将这个弱引用指针设置为nil
-
自动释放池
- autorelease对象在什么时机会被调用release ?
-
示例代码
#import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *person = [[[Person alloc] init] autorelease]; } return 0; }
-
编译成C++后
{ //1. 定义一个__AtAutoreleasePool类型的变量,会调用他的构造函数 __AtAutoreleasePool(); __AtAutoreleasePool __autoreleasepool; //2. 调用 Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease")); //等价于 //Person *person = [[[Person alloc] init] autorelease]; //3. 这时__autoreleasepool这个局部变量将要销毁,会调用__AtAutoreleasePool的析构函数~__AtAutoreleasePool() } //__AtAutoreleasePool结构体对象 struct __AtAutoreleasePool { //构造函数 __AtAutoreleasePool() { atautoreleasepoolobj = objc_autoreleasePoolPush(); } //析构函数 ~__AtAutoreleasePool() { objc_autoreleasePoolPop(atautoreleasepoolobj); } //成员变量 void * atautoreleasepoolobj; };
-
代码简化后,第2步源码的本质就是如下:
//等价于: @autoreleasepool { oatautoreleasepoolobj = objc_autoreleasePoolPush(); Person *person = [[[Person alloc] init] autorelease]; //等价于: } objc_autoreleasePoolPop(atautoreleasepoolobj);
- 源码分析(objc4源码:NSObject.mm)
-
搜索
objc_autoreleasePoolPush
方法,会看到void * objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); }
- 本质是调用AutoreleasePoolPage类的成员函数push();
- 结论:
- 自动释放池的主要底层数据结构是:
__AtAutoreleasePool、AutoreleasePoolPage
- 调用了
autorelease
的对象最终都是通过AutoreleasePoolPage
对象来管理的
- 自动释放池的主要底层数据结构是:
-
AutoreleasePoolPage类:
class AutoreleasePoolPage { magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; ...略 }
-
AutoreleasePoolPage的结构
- 每个
AutoreleasePoolPage
对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease
对象的地址 - 所有的
AutoreleasePoolPage
对象通过双向链表的形式连接在一起- 因为一个pool中可以有n个对象调用Autorelease,那么一个AutoreleasePoolPage对象是存放不完的,那么就需要新增AutoreleasePoolPage对象存储
- 新增的对象必须之间有关联,那就是双向链表
- 调用push方法会将一个
POOL_BOUNDARY
入栈,并且返回其存放的内存地址 - 调用pop方法时传入一个
POOL_BOUNDARY
的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
id *next
指向了下一个能存放autorelease
对象地址的区域
Runloop和Autorelease
- iOS在主线程的Runloop中注册了2个Observer
- 第1个
Observer
监听了kCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
- 第2个Observe
- 监听了kCFRunLoopBeforeWaiting事件,会调用
objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
- 监听了
kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
- 监听了kCFRunLoopBeforeWaiting事件,会调用
- 第1个
- 分析:
-
情况1:手动创建自动释放池
- (void)test1 { NSLog(@"111111"); //情况1: 手动创建自动释放池 @autoreleasepool { Person *person = [[[Person alloc] init] autorelease]; } //会在大括号即“}”处释放,即底层调用objc_autoreleasePoolPop()释放 NSLog(@"222222"); } //打印 111111 -[Person dealloc] 222222
- 会在
autoreleasepool
大括号即“}”处释放,即底层调用objc_autoreleasePoolPop()释放
- 会在
-
情况2: 非手动创建自动释放池
- (void)viewDidLoad { [super viewDidLoad]; [self test2]; NSLog(@"%s",__func__); } -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; NSLog(@"%s",__func__); } //在这打断点,发现Person已经执行dealloc -(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; NSLog(@"%s",__func__); } - (void)test2 { NSLog(@"111111"); Person *person = [[[Person alloc] init] autorelease]; NSLog(@"222222"); NSLog(@"%s",__func__); } //打印 111111 222222 -[ViewController test2] -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[Person dealloc] -[ViewController viewDidAppear:]
- 那么这种事什么时候释放的呢? 是test2的”}”结束时刻?
- 从上面打印我们可以看到,person对象是在viewWillAppear执行完毕后释放的
- 那不可能每一个对象都是在这时刻释放的,那具体是什么时候释放的呢?
- runloop监听释放
- 还是main函数内部的
@autoreleasepool
?- 这个很显然不是,因为有
runloop
,只要程序没有退出,就一直不会执行main函数@autoreleasepool
的结尾”}”;
- 这个很显然不是,因为有
- 那么这种事什么时候释放的呢? 是test2的”}”结束时刻?
-
- 面试题
- autorelease 对象什么时候会被释放?
- 情况1: 手动创建自动释放池
- 会在autoreleasepool大括号即“}”处释放,即底层调用objc_autoreleasePoolPop()释放
- 非手动创建autoreleasepool
- 会在runloop的状态是即将休眠`kCFRunLoopBeforeWaiting释放
- 会在runloop即将退出时释放
kCFRunLoopBeforeExit
- 情况1: 手动创建自动释放池
- autorelease 对象什么时候会被释放?