内存管理(三)

引用计数的存储

在64位中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中。

isa指针中存储的什么东西,具体可以参考Runtime的本质(一)

在这里插入图片描述
在isa里面,有一个extra_rc参数
其中:rc就是retainCount引用计数的意思。

引用计数器太大,extra_rc中存储不下,则has_sidetable_rc=1,引用计数器会存储在一个名为SideTable的类的属性中。

struct SideTable 
{
    spinlock_t slock;// 保证原子操作的自旋锁
    RefcountMap refcnts;//引用计数器存储地,是一个哈希map表
    weak_table_t weak_table;//弱引用表,也是哈希map存储
}

在源码中,可以找到retainCount的源码:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

点击进去:
在这里插入图片描述
sidetable_getExtraRC_nolock();的实现源码
在这里插入图片描述
refcnts以对象的地址作为key,引用计数作为value

更多学习请看:
iOS管理对象内存的数据结构以及操做算法–SideTables、RefcountMap、weak_table_t


weak指针相关知识点

__strong会对对象产生强引用;
__weak会对对象产生弱引用,并且,在对象销毁的时候,将指针置为nil;
__unsafe_unretain会对对象产生弱引用,并且,在对象销毁的时候,什么也不做。如果再次使用该指针,则会报野指针错误。

weak指针的实现原理

也就是,weak指针指向对象销毁的时候,weak指针怎么做到变为nil的。
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个Hash(哈希)表,
Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

举个例子:
__weak NSObject *obj1 = [[NSObject alloc] init];
__weak NSObject *obj2 = obj1;

Key指的是[[NSObject alloc] init]
Value指的是[obj1, ibj2]

struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};
dealloc

当一个对象要释放时,会自动调用dealloc,接下来调用的是:
dealloc
_objc_rootDealloc
rootDealloc
object_dispose
objc_destructInstance、free

- (void)dealloc {
    _objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}


inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

	//weakly_referenced是弱指针引用
	//强引用走yes,弱引用走no
    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);
    }
}

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();//将指向当前对象的弱指针置为nil
    }

    return obj;
}

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer 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.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {//如果是弱指针
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

一句话,在64位下,weak指针存储在一个weak_table哈希表中,在dealloc中进行判断,如果是weak指针,则置为nil。

更多关于weak学习:
weak 弱引用的实现方式
iOS开发 - 底层解析weak的实现原理
笔记-更深层次的了解iOS内存管理


ARC都帮我们做了什么?

ARC主要是LLVM编译器+RunTime系统相互协作的结果;

通过LLVM编译器,帮我们自动做了retain、release、autorelease操作,也就是自动做release操作是编译器特性。
通过runtime,监控系统,在程序运行中,将weak指针销毁置为nil

在自动引用计数(ARC)的实现中,LLVM编译器和Objective-C运行时(RunTime)系统共同协作,实现了内存管理的自动化,大大减少了开发者需要手动编写的retain/release代码量。以下是LLVM编译器和Objective-C运行时在ARC下各自的角色和作用:

LLVM编译器的角色

  1. 自动插入内存管理代码: 在编译时,LLVM编译器负责自动插入retain、release和autorelease调用。这是通过静态分析代码的方式完成的,编译器会根据对象的生命周期以及所有权修饰符(如__strong__weak)来决定何时以及在何处添加这些内存管理调用。

  2. 优化内存管理操作: LLVM编译器还会尝试优化这些自动插入的内存管理调用,减少不必要的操作以提高性能。例如,通过删除冗余的retain/release调用,或者在编译期间合并多个可以安全合并的内存管理调用。

  3. 强弱引用管理: 编译器负责处理强引用和弱引用的创建和销毁。对于弱引用,编译器自动插入必要的代码来确保当所引用的对象被销毁时,弱引用会被置为nil。

Objective-C运行时(RunTime)的角色

  1. 维护引用计数: 运行时负责在对象的生命周期内维护其引用计数。每次对象被retain时,其计数增加;每次release时,计数减少。当计数达到零时,对象被销毁。

  2. 处理弱引用: 运行时维护一个弱引用表,记录所有的弱引用。当一个对象被销毁时,运行时会自动将所有指向该对象的弱引用置为nil,防止悬垂指针的出现。

  3. 自动释放池管理: 自动释放池(Autorelease Pool)的创建和销毁也是由运行时管理的。在ARC环境下,虽然开发者仍然可以显式使用自动释放池,但大多数autorelease调用是由编译器根据需要自动插入的。

  4. 对象生命周期和内存管理事件的响应: 运行时会响应与对象生命周期和内存管理相关的事件,如对象的构造、析构、引用计数变化等,并执行相应的操作,如调用析构函数来销毁对象。

通过这种协作,ARC能够实现对Objective-C对象内存管理的自动化,大幅简化了内存管理工作,同时减少了内存泄漏的风险。开发者不再需要手动调用retain/release,可以更专注于业务逻辑的实现,而内存管理相关的任务则交由编译器和运行时系统自动处理。


autoreleasepool

在之前的学习中,我们有这样的说法:
autorelease是在@autoreleasepool{}大括号结束的时候,调用一下release操作。

在这里插入图片描述从运行结果来看,确实是在17行结束的时候,调用了person的dealloc方法,也就是调用了release。本质是什么呢?


使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 将main.m文件转换为main.cpp文件,可以看到上图中的底层代码:

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_64ff8e_mi_0);
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            YZPerson *person1 = ((YZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((YZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((YZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YZPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
        }
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_64ff8e_mi_1);
    }
    
简化下:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog(@"begin");
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
        }
        NSLog(@"end");
    }   

也就是:

@autoreleasepool 
{
	YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
}

转化为:

{ 
__AtAutoreleasePool __autoreleasepool; 
	YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
}

在被转换的main.cpp文件中,可以看到__AtAutoreleasePool的定义:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {//构造函数,在创建结构体变量的时候调用
      atautoreleasepoolobj = objc_autoreleasePoolPush();
  }
  ~__AtAutoreleasePool() {//析构函数,在结构体变量销毁的时候调用
      objc_autoreleasePoolPop(atautoreleasepoolobj);
  }
  void * atautoreleasepoolobj;
};

也就是,在执行__AtAutoreleasePool __autoreleasepool; 代码的时候,会调用构造函数:
atautoreleasepoolobj = objc_autoreleasePoolPush();

{ 
__AtAutoreleasePool __autoreleasepool; 
YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
}

大括号结束的时候,会调用析构函数:objc_autoreleasePoolPop(atautoreleasepoolobj);

整合上面,也就是:

{ 
__AtAutoreleasePool __autoreleasepool; 
atautoreleasepoolobj = objc_autoreleasePoolPush();

YZPerson *person1 = [[[YZPerson alloc] init] autorelease];

objc_autoreleasePoolPop(atautoreleasepoolobj);
}

autoreleasepool自动释放池的研究,转化为对objc_autoreleasePoolPush()objc_autoreleasePoolPop()的研究。


objc_autoreleasePoolPush和objc_autoreleasePoolPop

在objc源码中,我们可以看到objc_autoreleasePoolPush和objc_autoreleasePoolPop的实现:

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

两个定义里面都用到了AutoreleasePoolPage

自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage。
调用了autorelease的对象,最终都是通过AutoreleasePoolPage对象来管理的。

接下来,我们来研究下AutoreleasePoolPage对象

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对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址(例子中的person地址值也被存放在了AutoreleasePoolPage内部,也就是调用autorelease就会把地址存放在AutoreleasePoolPage内部)。
  • 所有AutoreleasePoolPage对象通过双向链表的形式链接在一起。
  • 调用push方法,会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址,(POOL_BOUNDARY其实就是一个栈入口)
  • 调用pop方法时,传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。(找到栈的入口,查找到最后入栈的对象。)
  • id *next指向了下一个能存放autorelease对象地址的区域
  • 可以通过extern void _objc_autoreleasePoolPrint(void)私有函数来查看自动释放池的情况。

在这里插入图片描述
0x2000-0x1000 = 0x1000=D4096(十进制4096)
双向链表,通过parent、child指针来链接。

  • 一个对象调用了autorelease,就会将该对象的地址值,加入到page里面去

autorelease与runloop

使用autorelease修饰的对象,在什么时候调用release方法?

MRC环境下:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    @autoreleasepool {
        YZPerson *person = [[[YZPerson alloc] init] autorelease];
    }
    NSLog(@"3");
}

如果是将对象person加入到一个autoreleasepool中,那么person对象会在自动释放池}时调用release方法。原因上面已经讲过。

如果,没有明显的加入自动释放池中,又是什么时候调用release方法呢?

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    YZPerson *person = [[[YZPerson alloc] init] autorelease];
    NSLog(@"3");
}

结果:
1
3
-[YZPerson dealloc]

看着像是在viewDidLoad大括号结束的时候调用的release操作。
真的是这样吗?
首先,我们确定的是,肯定不是在main.m文件中的autoreleasepool中去管理person的release操作。因为,main.m文件中的autoreleasepool是整个程序生命周期延续的,只有在程序结束的时候,才会运行到},才会去销毁对象。如果person交给main.m文件中的autoreleasepool,也就意味着person在整个程序运行过程中,都不会被释放,这明显是不对的。因此:
main.m文件中的autoreleasepool不负责person对象的release操作。

在这里插入图片描述
从上面结果可以看出,[YZPerson dealloc]是在[ViewController viewWillAppear:]和[ViewController viewDidAppear:]中间被执行的。
也就是说,并不是在48行代码执行的dealloc。


直接打印[NSRunLoop mainRunLoop],我们在打印结果里面发现observers里面有这两个类型:_wrapRunLoopWithAutoreleasePoolHandler
在这里插入图片描述
拿取出来,可以看到:

observers = (
    "<CFRunLoopObserver 0x6000006e41e0 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x6000039bc690 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fc00e802038>\n)}}",
   
    "<CFRunLoopObserver 0x6000006e4280 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x6000039bc690 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fc00e802038>\n)}}"

其中,第一个activities = 0x1,第二个activities = 0xa0
结合

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),//1
    kCFRunLoopBeforeTimers = (1UL << 1),//2
    kCFRunLoopBeforeSources = (1UL << 2),//4
    kCFRunLoopBeforeWaiting = (1UL << 5),//32
    kCFRunLoopAfterWaiting = (1UL << 6),//64
    kCFRunLoopExit = (1UL << 7),//128
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

可以看出:
第一个activities = 0x1,监听的是kCFRunLoopEntry
第二个activities = 0xa0换算成十进制是160,也就是32+128=160,监听的是kCFRunLoopBeforeWaiting | kCFRunLoopExit

换句话说,就是在runloop里面有两个跟AutoreleasePool相关的observer,一个observer监听kCFRunLoopEntry,一个observer监听kCFRunLoopBeforeWaiting | kCFRunLoopExit

iOS在主线程的runloop中注册了2个observer
第一个observer监听了kCFRunLoopEntry,会调用objc_autoreleasePoolPush()
第二个observer监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
第二个observer也监听了kCFRunLoopExit事件,会调用objc_autoreleasePoolPop()

翻译成白话,也就是:
在线程中:
在runloop开始的时候,就开始调用入栈操作,将调用autorelease的对象存储在栈中。
在runloop休眠前,执行出栈(release)操作,再执行一次入栈操作。
在runloop销毁的时候,执行出栈(release)操作。


autorelease,即自动释放,即一段时间后释放
也就是:

@autoreleasepool 
{
	YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
	//上述代码后,person1的引用计数器为1,为1的原因是由于alloc的作用
	//而autorelease没有对引用计数器再次加1(变为2),其作用是:在适当时候,做release操作,将引用计数器变为0
}

也就是,autorelease并没有增加对象的引用计数器数值,仅仅是在消失的时候做release操作,对引用计数器做减一操作
而,MRC下的retain,或者ARC下的strong等操作,只是改变了引用计数器大小,而不会再次将对象加入自动释放池

其他学习资料:
iOS - 聊聊 autorelease 和 @autoreleasepool

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值