我们都知道OC是通过引用计数来管理对象的生命周期的.一个新创建的OC对象的默认引用计数是1,调用retain
会让对象的引用计数+1,调用release
会让对象的引用计数-1.当引用对象为0时,OC对象就会销毁并释放其占用的内存空间.那么这个引用计数是存放在哪里的呢?
从arm64
(5S) 架构开始,引用计数就直接存储在对象的isa
指针中:
SideTable
的结构如下:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;//引用计数
weak_table_t weak_table;
}
其中有个散列表RefcountMap
就存储着引用计数.RefcountMap
中以当前对象的地址作为key
,引用计数作为value
.
下面我们就从runtime
源码中验证一下:
步骤:NSObject.mm
中搜索- (NSUInteger)retainCount
:
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
进入rootRetainCount
:
objc_object::rootRetainCount()
{
//如果是 TaggedPoint 类型就直接返回
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//判断指针是否优化过: 0:普通指针 ; 1:优化过指针
if (bits.nonpointer) {
//取出 extra_rc 的值然后 加1, extra_rc存储的值是引用计数减1
uintptr_t rc = 1 + bits.extra_rc;
//判断引用计数是否过大,无法存储在 isa 中
// has_sidetable_rc 如果为1,那么引用计数就存储在 SideTable 中.
if (bits.has_sidetable_rc) {
//取出 SideTable 中的引用计数
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
如果has_sidetable_rc
为1就说明引用引用计数存储在SideTables
中,我们进入sidetable_getExtraRC_nolock
看看是如何从SideTables
中获取引用计数的:
objc_object::sidetable_getExtraRC_nolock()
{
assert(isa.nonpointer);
//把自身 this 当做 key 取出 table
SideTable& table = SideTables()[this];
// Map 说明这是一个 散列表
//refcnts 是 RefcountMap 类型,也是一个 散列表
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
//取出 second 然后 位运算 得到 引用计数
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
weak实现的原理
运行下面的代码,可以看到局部变量person
再离开其作用域后就销毁了:
如果我们用一个
__strong
修饰的强指针指向这个person
会发现,过了person
的作用域后还不会销毁:__strong
再用weak
修饰的person2
指向person
看看结果:
发现
weak
没有对person
产生强引用.再用
__unsafe_unretained
修饰的person3
指向person
:__unsafe_unretained
会发现崩溃了,但是
person
对象依然在出了自身作用域后就销毁了,说明__unsafe_unretained
同样没有对person
产生强引用.既然
__unsafe_unretained
和weak
都没有对person
产生强引用,那他们有什么区别呢?区别就是:
wea
不会对对象产生强引用,并且当weak
指向的对象释放后,weak
会把指针置为nil
.防止野指针错误.而__unsafe_unretained
指向的对象销毁后,__unsafe_unretained
并不会把指针置为nil
.所以__unsafe_unretained
是不安全的,容易出现野指针访问
那么weak
内部是如何实现的把指针置为nil
的呢?我们还是从runtime
源码中探寻答案:
步骤:在NSObject.mm
中搜索- (void)dealloc
:
- (void)dealloc {
_objc_rootDealloc(self);
}
进入_objc_rootDealloc
:
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
进入rootDealloc
:
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer && //如果是优化过指针
!isa.weakly_referenced && //是否被弱引用指向过
!isa.has_assoc && //是否设置过关联对象
!isa.has_cxx_dtor && //是否有c++析构函数
!isa.has_sidetable_rc))//引用计数是否存储在SideTable中
{
assert(!sidetable_present());
//直接释放对象,速度最快
free(this);
}
else {
//会走另外的流程释放,速度会慢一些
object_dispose((id)this);
}
}
进入object_dispose
:
object_dispose(id obj)
{
if (!obj) return nil;
//释放前:做一些事情
objc_destructInstance(obj);
//释放
free(obj);
return nil;
}
进入objc_destructInstance
:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();//如果有c++析构函数
bool assoc = obj->hasAssociatedObjects();//如果有关联对象
// This order is important.
if (cxx) object_cxxDestruct(obj);//清除成员变量
if (assoc) _object_remove_assocations(obj);//移除关联对象
obj->clearDeallocating();//将指向当前对象的弱指针置为 nil
}
return obj;
}
进入clearDeallocating
看看它内部是如何把对象置为nil
的:
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//普通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.
//优化过isa指针
clearDeallocating_slow();
}
assert(!sidetable_present());
}
进入优化过的isa
指针处理方法clearDeallocating_slow
:
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
//传入 this ,取出 SideTable 类型的 table
SideTable& table = SideTables()[this];
/**
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;//散列表,以当前对象地址作为key,retainCount作为value
weak_table_t weak_table;//散列表,以当前对象地址作为key,weak修饰的指向此对象的指针作为value
}
*/
table.lock();
if (isa.weakly_referenced) {
//清除 weak 指针
weak_clear_no_lock(&table.weak_table, (id)this);
}
/**
清除完weak指针后,也会把引用计数表清除
因为当前对象要销毁了
*/
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}
进入weak_clear_no_lock
看看是如何清除的:
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);
}
weak_entry_for_referent(weak_table, referent)
把弱引用表 和 当前对象地址值传进去:
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
assert(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
//利用当前对象的地址值 & 一个值 得到一个索引 begin
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
//根据索引值得到对应的weak指针
return &weak_table->weak_entries[index];
}
最后清除:
//拿到需要清除的weak指针,清除
weak_entry_remove(weak_table, entry);
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
// remove entry
//释放
if (entry->out_of_line()) free(entry->referrers);
//置为nil
bzero(entry, sizeof(*entry));
weak_table->num_entries--;
weak_compact_maybe(weak_table);
}
ARC帮我们做了什么?
- ARC是 LLVM 和 Runtime 的结合
- 利用LLVM自动生成 release 和 retain autorelease 代码
- 像弱引用的实现就需要 Runtime 运行时处理
autorelease
调用了autorelease
的对象不需要我们手动调用release
,系统会在适当的时候自动调用release
去释放对象.那么系统是怎么做到这点的呢?这就牵扯到了自动释放池@autoreleasepool
.我们以一下代码为实例,并且转换为c++代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[[Person alloc]init]autorelease];
}
return 0;
}
c++代码:
{ __AtAutoreleasePool __autoreleasepool;
//person的初始化代码
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
的初始化代码去掉:
{ __AtAutoreleasePool __autoreleasepool;
Person *person = [[[Person alloc]init]autorelease];
}
会发现autoreleasepool
代码就被转成了上面这种样式.__AtAutoreleasePool
其实就是结构体:
struct __AtAutoreleasePool {
//构造函数,生成结构体变量的时候调用
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析构函数,销毁结构体变量的时候调用
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
c++中的结构体和OC中的类很像,可以把c++的结构体看做是OC中的类,所以可以在结构体中声明函数.
也就是说执行__AtAutoreleasePool __autoreleasepool;
代码的时候就会去调用__AtAutoreleasePool
的构造函数:
//构造函数,生成结构体变量的时候调用
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
一旦走完autoreleasepool
的大括号就会调用__AtAutoreleasePool
的析构函数:
//析构函数,销毁结构体变量的时候调用
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
所以我么写的代码最后实际上的效果就是这样:
调用构造函数 返回 atautoreleasepoolobj
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc]init]autorelease];
调用析构函数,传入 atautoreleasepoolobj
objc_autoreleasePoolPop(atautoreleasepoolobj);
也就是autoreleasepool
大括号的开始会调用objc_autoreleasePoolPush
,大括号的结束会调用objc_autoreleasePoolPop
.所以以后只要看到autoreleasepool
就代表被objc_autoreleasePoolPush
和objc_autoreleasePoolPop
包围:
@autoreleasepool {
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc]init]autorelease];
@autoreleasepool {
atautoreleasepoolobj = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
所以我们只要搞清楚objc_autoreleasePoolPush()
和objc_autoreleasePoolPop ()
这两个函数内部做了什么,就能搞清楚autoreleasepool
的本质了.
在runtime
源码中搜索objc_autoreleasePoolPush
和objc_autoreleasePoolPop
函数会发现他们长这样:
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
发现他们都用到了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;
{
- 一旦调用
objc_autoreleasePoolPush ()
函数,就会创建一个AutoreleasePoolPage
对象.这个对象占用4096个字节的内存,除了用来存放它内部的成员变量以外,剩下的空间用来存放autorelease
对象的地址.比如说person
对象调用了autorelease
,那么person
对象的地址就存在了AutoreleasePoolPage
对象中. - 所有的
AutoreleasePoolPage
对象通过双向链表的形式链接在一起.
比如说现在有一个AutoreleasePoolPage
对象,内存地址在0x1000
,那么它的结束内存地址就是0x2000
.因为十六进制0x1000
的十进制就是4096
.person
又调用了autorelease
方法,那么person
对象的地址就会被存放在AutoreleasePoolPage
对象内部的0x1038
这个位置,因为AutoreleasePoolPage
对象内部有7个成员变量,每个成员变量占用8个字节,一共占用56个字节,用十六进制表示就是0x38
,画图表示如下:
在
AutoreleasePoolPage
对象内部有两个函数:
//返回开始存放 autorelease 对象的地址
id * begin() {
算法:自身的地址 + 自身所占空间大小: 0x1000 + 0x38
return (id *) ((uint8_t *)this+sizeof(*this));
}
//返回存放 autorelease 对象的结束地址
id * end() {
SIZE = 4096 , 自身地址 + 4096 个字节 : 0x1000 + 0x1000
return (id *) ((uint8_t *)this+SIZE);
}
如图所示:
如果一个
AutoreleasePoolPage
对象不够存储autorelease
对象,那么就会再创建AutoreleasePoolPage
对象.每个AutoreleasePoolPage
对象的parent
指向它的上一个AutoreleasePoolPage
对象,如果是第一个AutoreleasePoolPage
对象,它的parent
就为nil
;每个AutoreleasePoolPage
对象的child
指向它的下一个AutoreleasePoolPage
对象,如果是最后一个AutoreleasePoolPage
对象,child
也为nil
.关系如下:关系图
现在我们知道了objc_autoreleasePoolPush()
会将autorelease
对象的地址存放到AutoreleasePoolPage
对象中;objc_autoreleasePoolPop
会将AutoreleasePoolPage
中存放的autorelease
对象释放.那么他们底层是怎么存储和释放的呢?
其实一旦调用objc_autoreleasePoolPush()
函数,它的内部就会将POOL_BOUNDARY
入栈,并且返回其内存地址,也就是0x1038
.这里的入栈并不是内存中的堆区和栈区,而是数据结构的那种栈,先进后出
;POOL_BOUNDARY
其实就是nil
,它的底层就是个宏:(define POOL_BOUNDARY nil)
;也就是说AutoreleasePoolPage
存放的第一个对象并不是autorelease
对象person
,而是POOL_BOUNDARY
.其次才是一个个autorelease
对象,如图:
而
objc_autoreleasePoolPop (0x1038)
传入的地址就是objc_autoreleasePoolPush ()
返回的地址,也就是0x1038
.objc_autoreleasePoolPop
拿到这个地址后会从最后一个加入到AutoreleasePoolPage
的autorelease
对象开始一个一个调用它们的release
方法,直到POOL_BOUNDARY
为止.BOUNDARY
就是边界的意思,可见push
函数和pop
函数结合的非常巧妙.
-
AutoreleasePoolPage
的next
成员变量永远指向下一个能存放autorelease
对象的地址. - 使用
_objc_autoreleasePoolPrint
私有函数查看autoreleasepool
自动释放池的情况:
//使用 extern 关键字声明这个函数,即使这个函数在 Foundation 框架内部
//编译器会自动去查找这个方法并调用,这是c语言语法
extern void _objc_autoreleasePoolPrint(void);
我们使用一下:
person autorelease之前 person autorelease之后 多个autoreleasepool 超出一个 page 容量
先面我们将从源码上验证上诉结论:
push
源码分析:
// 第一步:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
//如果没有 page 就创建一个 page,把 POOL_BOUNDARY 传进去
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//有page
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
//第二步
id *autoreleaseNewPage(id obj)
{
//调用hotPage,创建一个page
AutoreleasePoolPage *page = hotPage();
//判断 page 是不是满了
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);
}
//第三步
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
//往 page 中添加POOL_BOUNDARY
return page->add(obj);
}
autorelease
源码分析:
//第一步:
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
//将调用了 autorelease 的对象传进去
return AutoreleasePoolPage::autorelease((id)this);
}
//第二步:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
//进入 autoreleaseFast
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
//第三步:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
//把调用了 autorelease 的对象 加入到 page
return page->add(obj);
} else if (page) {
//如果有page,并且 page 满了
return autoreleaseFullPage(obj, page);
} else {
//如果没有 page,就创建 page
return autoreleaseNoPage(obj);
}
}
pop
源码分析:
//第一步:
static inline void pop(void *token) {
//token 就是 POOL_BOUNDARY
page = pageForPointer(token);
stop = (id *)token;
//释放对象,直到遇到 POOL_BOUNDARY
page->releaseUntil(stop);
}
//第二步:
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
while (this->next != stop) {
//如果 不是 POOL_BOUNDARY 就一直释放对象
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
autorelease 对象什么时候释放?
我们知道调用了autorelease
的对象会在适当的时候由系统去调用release
释放,但是这个适当的时候是什么时候呢?思考一下以下代码的person
对象什么时候会被释放:
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[[Person alloc]init]autorelease];
NSLog(@"viewDidLoad");
}
- (void)viewWillAppear:(BOOL)animated{
NSLog(@"viewWillAppear");
}
- (void)viewDidAppear:(BOOL)animated{
NSLog(@"viewDidAppear");
}
有人可能会觉得是走完viewDidLoad
的大括号释放的,我们运行一下代码看看结果:
AutoRelease对象和runloop[18097:3714897] viewDidLoad
AutoRelease对象和runloop[18097:3714897] viewWillAppear
AutoRelease对象和runloop[18097:3714897] -[Person dealloc]
AutoRelease对象和runloop[18097:3714897] viewDidAppear
事实上是走完viewDidLoad
和viewWillAppear
才释放的,为什么会这样呢?其实这和runloop
有关系.
iOS在主线程中注册了两个Observer
用来处理自动释放池相关的工作.Observer
是用来监听runloop
的状态的,比如进入runloop
,runloop
即将处理timer
,runloop
即将休眠等等.
我们打印一下NSLog(@"%@",[NSRunLoop currentRunLoop]);
看看:
// 监听状态为 1
"<CFRunLoopObserver 0x6000021b0960 [0x7fff80615350]>{valid = Yes, activities =
0x1, repeats = Yes, order = -2147483647, callout =
_wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray
0x600001ec6610 [0x7fff80615350]>{type = mutable-small, count = 1, values = (\n\t0 :
<0x7fdbda801038>\n)}}",
//监听状态为160 相当于 kCFRunLoopBeforeWaiting | kCFRunLoopExit
"<CFRunLoopObserver 0x6000021b0a00 [0x7fff80615350]>{valid = Yes, activities =
0xa0, repeats = Yes, order = 2147483647, callout =
_wrapRunLoopWithAutoreleasePoolHandler (0x7fff47848c8c), context = <CFArray
0x600001ec6610 [0x7fff80615350]>{type = mutable-small, count = 1, values = (\n\t0 :
<0x7fdbda801038>\n)}}"
会发现有两个Observer
会调用_wrapRunLoopWithAutoreleasePoolHandler
函数,并且这两个Observer
分别监听了两个状态:
-
activities = 0x1
: 0x1 = 1 -
activities = 0xa0
=xa0 = 160
我们再来看看runloop
中有哪些状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 4
kCFRunLoopBeforeSources = (1UL << 2), 8
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
对照runloop
的状态我们知道:
-
activities = 0x1
: 就是监听kCFRunLoopEntry
进入runloop
. -
activities = 0xa0
就是监听kCFRunLoopBeforeWaiting | kCFRunLoopExit
即将休眠和退出.
那么这两个Observer
做了什么事情呢?
- 第一个
Observer
监听kCFRunLoopEntry
会调用objc_autoreleasePoolPush ()
. - 第二个
Observer
:
监听kCFRunLoopBeforeWaiting
时间会调用objc_autoreleasePoolPop ()
,然后再调用objc_autoreleasePoolPush ()
;
监听kCFRunLoopExit
事件会调用objc_autoreleasePoolPop ()
也就是说person
对象什么时候调用release
,是由runloop
来控制的,它可能是在某次runloop
循环中,runloop
休眠之前调用了release
;并且从上面的打印结果可以分析出来,viewDidLoad
和viewWillAppear
是在同一次runloop
循环中.
ARC环境下,方法中局部变量出了方法会立即释放吗?
我们可以试一下:
从打印结果可以看到,
person
对象在viewDidLoad
之后,viewWillAppear
之前就释放了.说明了ARC自动生成的并不是autorelease
,而是在viewDidLoad
大括号之前生成了person release
.所以我们应该分两种情况分析:
- 如果ARC自动生成的是
autorelease
,那么局部对象是在runloop
某次循环即将休眠的时候释放. - 如果ARC自动生成的是
release
,那么局部对象就会出了方法就释放.
网友评论