美文网首页iOS内存管理iOS
iOS - 内存管理相关

iOS - 内存管理相关

作者: valentizx | 来源:发表于2019-05-11 13:39 被阅读5次
    image

    CADisplayLink、NSTimer 使用注意

    CADisplayLinkNSTimer 会对 target 产生强引用,如果 target 又对他们产生强引用,那么会引发循环引用。

    CADisplayLink 也是一个定时器,它是必须显示的添加到 RunLoop 当中才能进行,它的调用频率是和屏幕的刷帧频率(60fps)理论上是一致的,也就是 1 秒会调用 60 次。但在复杂的 UI 层级中会有误差。CADisPlayLink 的基本使用:

    @interface ViewController ()
    @property(strong, nonatomic) CADisplayLink* link;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(test)];
        [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode: NSDefaultRunLoopMode];
       
    }
    
    - (void)test {
        NSLog(@"%s", __func__);
    }
    @end
    

    运行打印结果:

    image
    打印的频率及其快。
    在上述代码中,当前控制器对 CADisplayLink 对象有强引用,而 CADisplayLink 对象在初始化的时候设置的 target 也是对控制器的强引用,所以会造成循环引用。
    并且在控制器销毁的时候,很多人会如下方式停止定时器:
    - (void)dealloc {
        [self.link invalidate];
    }
    

    如果当前控制器压在导航控制器(UINavigationController)的栈中的话,在从当前控制器返回的时候会发现,控制器不会销毁,定时器也不会调用 invalidate 方法。

    NSTimer 也会存在这样的问题:

    @interface ViewController ()
    @property(strong, nonatomic) NSTimer* timer;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(test) userInfo:nil repeats:YES];
       
    }
    
    - (void)test {
        NSLog(@"%s", __func__);
    }
    
    - (void)dealloc {
        [self.timer invalidate];
    }
    @end
    

    处理定时器的循环引用

    对于 NSTimer 产生的循环引用的问题,第一种解决办法就是,使用 scheduledTimerWithTimeInterval: repeats: block: 方法

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf test];
    }];
    

    这种方法,躲避了传入的 target 造成的强引用问题。

    另一种方法是,可以将 target 设置成另外一个对象,而该对象对控制器为弱引用,这样也就解决了循环引用的问题


    image

    图中就是打破了互相强持有的问题。

    我们可以新建一个代理 Proxy 类:

    .h
    @interface Proxy : NSObject
    
    @property(nonatomic, weak) id target;
    
    +(instancetype)proxyWithTarget:(id)target;
    
    @end
    
    .m
    @implementation Proxy
    
    + (instancetype)proxyWithTarget:(id)target {
        Proxy* proxy = [[Proxy alloc] init];
        proxy.target = target;
        return proxy;
    }
    
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return self.target;
    } 
    @end
    

    则外部:

    self.timer = [NSTimer scheduledTimerWithTimeInterval: .1 target:[Proxy proxyWithTarget:self] selector:@selector(test) userInfo:nil repeats:YES];
    

    就会解决问题。

    Q. Proxy 为什么要实现 forwardingTargetForSelector: 方法?
    A. 我们看到 Timer 的 target 是 Proxy 对象,也就是最终 test 方法是由 Proxy 对象来执行,但是显然 Proxy 中是没有 test 方法的,那么既然没有方法,我们自然而然的就想到了三个拯救程序崩溃的函数:消息发送、动态解析、消息转发,在这里消息转发即可,在该函数中返回有 test 方法的对象即可。

    CADisplayLink 解决办法同理。

    Foundation 的框架中,早就有了 NSProxy,它的存在,就是为了解决上述的代理问题的,详见官方文档有关 NSProxy 的介绍而且,NSProxy 这个类很特殊,它自身和 NSObject 一样都是基类,并非继承自 NSObject 也没有 init 方法。

    但是与自身实现的 Proxy 不同的是,NSProxy 解决消息处理的方法为通过
    forwardInvocation: 和 methodSignatureForSelector: 来处理消息。

    若 Proxy 继承自 NSProxy 来实现则:

    @implementation Proxy
    
    + (instancetype)proxyWithTarget:(id)target {
        Proxy* proxy = [Proxy alloc]; // 没有 init 方法
        proxy.target = target;
        return proxy;
    }
    // 返回方法签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        return [self.target methodSignatureForSelector:sel];
    }
    // 直接调用
    - (void)forwardInvocation:(NSInvocation *)invocation {
        [invocation invokeWithTarget:self.target];
    }
    
    @end
    

    定时器的研究

    CADisplayLink 和 NSTimer 其实都是基于 RunLoop 实现的,所以都会产生这样一个问题:定时器可能并不准时,如果 RunLoop 的任务太过繁重,或者模式的切换都能导致定时器的不准时。解决这样的问题除了将模式设置为 NSRunLoopCommonModes 之外,还有一个就是使用 GCD 定时器来解决这个问题:

    // 创建队列
    dispatch_queue_t queue = dispatch_get_main_queue(); 
    // 创建计时器
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 设置时间
    // 第二个参数为:多久以后开始,DISPATCH_TIME_NOW 表示立即开始
    // 第三个参数为:间隔,第四个参数为误差:一般传入 0
        
    NSTimeInterval start = 2.0;
    NSTimeInterval interval = 1.0; // 间隔
    
    dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
    // 设置回调
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"========");
    });
    dispatch_resume(self.timer);
    
    // 停止定时器
    dispatch_source_cancel(self.timer);
    

    目标秒数 * NSEC_PER_SEC 是因为,dispatch_source_set_timer 中接受的时间单位是纳秒,所以要完成秒->的转换,故要乘以 NSEC_PER_SEC。
    timer 一定要被强引用才能生效

    GCD 的定时器不依赖于 RunLoop,是和内核直接挂钩的,所以准确性很高,并且若传入的队列不是主队列,回调中的内容会在子线程中执行。

    iOS 程序的内存布局

    iOS 中的内存布局是这样的:


    image

    代码段:是编译之后的代码;

    数据段:字符串常量如 NSString* str = @"0305",已初始化的全局变量、静态数据等:int a = 5,未初始化全局变量、静态数据:int b。

    栈:函数调用开销,如局部变量。分配内存是从高到低。

    堆:通过 allocmalloccalloc 等动态分配的空间。分配内存是从低到高。

    Tagged Pointer

    从 64bit 开始,iOS 引入了 Tagged Pointer 技术,用于优化 NSNumber、NSDate、NSString 等小对象的存储。

    如 NSNumber* intNumber = @10; 明明存储的是一个整形 4 个字节的数字 10,却需要至少 16 个字节(一个 Objective-C 对象至少是 16 个字节:isa 8 个字节+其他)的空间来存储,很浪费性能。

    在没有使用 Tagged Pointer 之前,NSNumber 等对象需要动态分配内存、维护引用计数等。NSNumber 指针存储的是堆中 NSNumber 对象的地址值。

    使用了 Tagged Pointer 之后,NSNumber 指针里面存储的数据变成了:Tag + Data,换而言之,就是将数据直接存在了指针当中。

    当指针的最低有效位是 1 的时候,则该指针位 Tagged Pointer,判断是否为 Tagged Pointer 的源码逻辑:

    #   define _OBJC_TAG_MASK 1UL
    static inline bool 
    _objc_isTaggedPointer(const void * _Nullable ptr)
    {
        return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
    }
    

    当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。

    objc_msgSend 能识别 Tagged Pointer,如 NSNumber 的 intValue 方法,可以直接从指针中提取数据,节省开销。

    延伸

    运行以下代码,可能会出现什么结果?

    @interface ViewController ()
    @property (copy, nonatomic) NSString* name;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        for (int i = 0; i < 1000; i ++) {
            dispatch_async(queue, ^{
                self.name = [NSString stringWithFormat: @"christinaaguilera"];
            });
        }
    }
    @end
    

    答案是会引发程序的 Crash。


    image.png

    因为 self.name = xxx 本质是调用 name 的 setter 方法,而 setter 方法的大致实现是:

    - (void)setName:(NSString *)name {
        if (_name != name) {
            [_name release]; // 释放旧值
            _name = [name copy]; // 若 name 是由 strong 修饰,这里为 [name retain];
        }
    }
    

    由于是并发队列在异步函数中执行任务,所以会导致某个时间点有两个线程同时执行 [_name release] 操作,会导致程序的崩溃。

    解决办法一:
    name 使用 atomic 修饰,相当于对 setter/getter 进行线程同步保护,可以避免上述坏情况发生。
    解决办法二:
    self.name = [NSString stringWithFormat: @"abc"] 进行线程同步保护,也就是在这句前面加锁,然后执行完该句解锁。

    我们将上述例子的字符串由 @"christinaaguilera" 改为 @"boa" 再运行,发现什么?
    程序没有崩溃!!
    这是因为什么?
    我们打印:

    NSLog(@"%p %p", [NSString stringWithFormat:@"christinaaguilera"], [NSString stringWithFormat:@"boa"]);
    

    得结果:

    0x600003202d60 0xea348006ede94334
    

    0x600003202d60 以 6xxxx 开头的一般都是堆内存地址,而那个 boa 是直接存储到指针当中,也就是 0xea348006ede94334 是一个 Tagged Pointer,所以不存在安全隐患。

    Objective-C 对象的内存管理

    在 iOS 中,使用引用计数来管理 Objective-C 对象的内存。

    一个新创建的 Objective-C 对象引用计数默认是 1,当引用计数为 0 的时候,对象就会销毁,释放其所占用的内存空间。

    MRC

    在 MRC 时代,调用 retaincopynew 方法会让 Objective-C 对象的引用计数加 1,release 操作会让对象的引用计数减 1。

    Xcode 关闭 ARC:Build Setting -> CLANG_ENABLE_OBJC_ARC -> NO。

    retain 和 release

    新建 Person 类继承自 NSObject,重写其 dealloc 方法:

    - (void)dealloc {
        [super dealloc];
        NSLog(@"%s", __func__);
    }
    

    在外部运行:

    Person* p = [[Person alloc] init];
    NSLog(@"%lu", (unsigned long)[p retainCount]);
    

    得打印结果为 1,dealloc 方法亦没有执行。这说明对象是没有销毁的,这种情况就是内存泄漏,我们需要执行 release 操作:

    [p release];
    

    释放该对象,也可以:

    Person* p = [[[Person alloc] init] autorelease];
    NSLog(@"%lu", (unsigned long)[p retainCount]);
    

    这意味着在 @autoreleasepool { ... } 执行完毕,会给对象发送一条 release 消息释放该对象。

    在属性是对象的时候,也要重写其方法对新值进行一次 retain 或者 copy 操作以免外部释放了对象导致该对象销毁,如新建 Player 类,该类有一个 play 方法,Person 有一个 Player 型的属性:

    .h
    @class Player;
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Person : NSObject
    {
        Player* _player;
    }
    
    - (void)setPlayer:(Player*)player;
    
    - (Player*)player;
    
    @end
    NS_ASSUME_NONNULL_END
    
    .m
    @implementation Person
    
    - (void)setPlayer:(Player *)player {
        
        if (_player != player) {
            [_player release]; // 释放旧值
             _player = [player retain]; // 引用新值
        }
        // 若两次传进来的对象一样,则不进行任何操作
    }
    
    - (Player *)player {
        return _player;
    }
    
    - (void)dealloc {
    
        [_player release];
        _player = nil;
        [super dealloc];
        NSLog(@"%s", __func__);
    }
    
    @end
    

    在 MRC 情况下,基本数据类型是不需要进行内存管理的。

    若用 @property 声明属性,如:

    @property(nonatomic, retain) Player* player;
    

    则需要借助 @synthesize 来声明 _player 成员变量达到和上面一样的效果:

    @synthesize player = _player; // 不写这句是调用不到 _player 的,当然名字也可以是 _player123 等等等
    
    - (void)setPlayer:(Player *)player {
        _player = player; // player 用 retain 修饰,则不需用判断和释放旧值的操作
    }
    

    并且 @synthesize 可以自动生成成员变量和属性的 setter/getter 实现。
    在这里同样需要在 dealloc 中将对象置为 nil。

    在 MRC 情况下:一般情况下类方法是不需要 release 的,通过 allocnewcopy 以及 mutableCopy 方法初始化的对象都需要 release 的。

    copy

    出现拷贝技术的目的就是产生一个副本,并且副本和原对象互相独立,修改两者之一另一个对象不会受任何影响。通常来修饰字符串 (NSString)、字典 (NSDictionary) 和数组 (NSArray)。

    iOS 中提供了两种拷贝方法:不可变拷贝 copy 和不可变拷贝 mutableCopy。
    copy 的结果是不可变副本,mutableCopy 的结果是可变副本:

    NSString* str = [NSString stringWithFormat:@"valenti"];
    NSString* str1 = [str copy]; // 不可变
    NSMutableString* str2 = [str mutableCopy]; // 可变
    [str2 appendString:@"love boa"];
    
    NSMutableString* str = [NSMutableString stringWithFormat:@"valenti"];
    NSString* str1 = [str copy]; // 不可变
    NSMutableString* str2 = [str mutableCopy]; // 可变
    [str2 appendString:@"love boa"];
    

    我们在第一个例子中打印:

    NSLog(@"%p %p %p", str, str1, str2);
    

    发现:

    0x1aed77c89a8821d1 0x1aed77c89a8821d1 0x100707470
    

    说明 str 和 str1 指向的是同一个对象。而 str2 的地址为全新的,这说明 copy 操作拷贝的仅仅是指针,可以明白,因为 copy 得到的结果是不可变的,而原字符串本身也是不可变的,所以没必要产生一个全新的内容副本,仅仅复制指针即可达到效果。而 mutableCopy 不可以这样,可变拷贝的结果是可以执行可变的操作,这个操作不能影响原对象所以需要指针和内容都要重新拷贝一份。

    仅仅拷贝指针称为浅拷贝,内容指针一起拷贝称为深拷贝。

    同理数组:

    NSArray* arr = [[NSArray alloc] initWithObjects:@"1", @"2", nil];
    NSArray* arr1 = [arr copy];
    NSMutableArray* arr2 = [arr mutableCopy];
    NSLog(@"%p %p %p", arr, arr1, arr2);
    

    结果为:0x10052ee80 0x10052ee80 0x10052f6e0
    同理字典。

    在 MRC 情况下,retain 修饰的属性生成的 setter 方法会自动释放旧值赋值新值,assign 修饰的属性生成的 setter 方法仅仅是赋值操作,那么 copy 修饰的属性会做什么?
    答案是:

    - (void)setXxx:(Xxxx *)xxx {
        if (_xxx != xxx) {
            [_xxx release];
            _xxx= [xxx copy];
        }
    }
    

    和 retain 不同的是最后是 copy 操作,产生一个不可变副本存储,所以即使是:

    @property(nonatomic, copy) NSMutableString* name;
    

    name 也是不能调用 appendString: 函数的。

    autorelease

    autorelease 相比 release 的好处是可以在适当的时机释放当前对象。并且在使用 release 操作的时候,对象所有的操作都需要放在 release 之前,否则会出现野指针错误,这样很容易出错。

    那么 autorelease 释放时机是什么?
    首先我们要知道自动释放池(autoreleasepool)这个东西,@autoreleasepool {...} 的底层结构是是一个结构体:

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

    那么下段代码:

    @autoreleasepool {
        Person* p = [[Person alloc] init];        
    }
    

    底层的形式就是:

    atautoreleasepoolobj = objc_autoreleasePoolPush();
    Person *p = [[[Person alloc] init] autorelease];
    objc_autoreleasePoolPop(atautoreleasepoolobj); // @autoreleasepool 作用域结束时候会调用这个析构函数
    

    我们在源码中可以找到这两个函数,其中 objc_autoreleasePoolPush() 逻辑为:

    static inline void *push() {
        id *dest;
        if (DebugPoolAllocation) {
            dest = autoreleaseNewPage(POOL_BOUNDARY); // 创建一个新的 AutoreleasePoolPage,POOL_BOUNDARY 为 nil
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
    

    objc_autoreleasePoolPop() 的逻辑为:

    static inline void pop(void *token) // token 为 POOL_BOUNDARY
        {
            AutoreleasePoolPage *page;
            id *stop;
    
            if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
         
                if (hotPage()) {
                    pop(coldPage()->begin());
                } else {
                    setHotPage(nil);
                }
                return;
            }
            page = pageForPointer(token);
            stop = (id *)token;
            if (*stop != POOL_BOUNDARY) {
                // 跨页处理
                if (stop == page->begin()  &&  !page->parent) {
                } else {
                    return badPop(token);
                }
            }
    
            if (PrintPoolHiwat) printHiwat();
    
            page->releaseUntil(stop); // 里面是循环,对每个对象,执行 release 操作
            if (DebugPoolAllocation  &&  page->empty()) {
                AutoreleasePoolPage *parent = page->parent;
                page->kill();
                setHotPage(parent);
            } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
                page->kill();
                setHotPage(nil);
            } 
            else if (page->child) {
               
                if (page->lessThanHalfFull()) {
                    page->child->kill();
                }
                else if (page->child->child) {
                    page->child->child->kill();
                }
            }
        }
    

    由源码得知自动释放池的主要底层数据结构有:__AtAutoreleasePoolAutoreleasePoolPage,并且自动释放的对象都是由 AutoreleasePoolPage 来管理的。
    AutoreleasePoolPage 的主要成员有:

    class AutoreleasePoolPage 
    {
        magic_t const magic;
        id *next; // 指向下一个能存放 autorelease 对象地址的区域
        pthread_t const thread;
        AutoreleasePoolPage * const parent;
        AutoreleasePoolPage *child;
        uint32_t const depth;
        uint32_t hiwat;
    }
    

    每个 AutoreleasePoolPage 对象占用 4096 个字节的内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址,也就是例子中 Person 对象的地址。

    image

    0x10000x2000 相差 0x1000 刚好是 4069 个字节。0x10380x2000 之间存储的都是 autorelease 对象的地址。
    所有的 AutoreleasePoolPage 对象通过双向链表的形式连接在一起。
    AutoreleasePoolPage 中有一个 begin() 方法,调用之后返回的就是能够存储对象区域的起始地址。对应的,end() 方法会返回能够存储对象区域的结束地址,当不够存储的时候会创建新的一页。
    childparent 的指向关系为:

    image

    当执行 push 操作的时候,会传入 POOL_BOUNDARY 返回一个地址,如 0x1038。

    image.png

    那么,atautoreleasepoolobj 存储的地址就是 0x1308,接下来不管的有对象调用 autorelease,atautoreleasepoolobj 会将这些新对象的地址存在 0x1309、0x1310... 的位置。
    当执行 pop 操作的时候,会传入 push 时传入 POOL_BOUNDARY 返回的那个地址,如 0x1308,拿到这个地址,会从最后一个压入 AutoreleasePoolPage 的对象开始逐个调用对象的 release 方法,直到遇到 POOL_BOUNDARY 所在的那个地址

    autorelease 与 RunLoop

    那么 autorelease 的对象什么时机会被调用 release?
    @autoreleasepool{...} 代码域结束后,若对象的引用计数为 1,则会立即被调用 release 方法。那么在 viewDidLoad 等其他方法中呢?
    这个答案和 RunLoop 有关,在主线程的 RunLoop 中注册了 2 个 Observer:kCFRunLoopEntryKCFRunLoopBeforeWaiting|KCFRunLoopBeforeExit
    监听 kCFRunLoopEntry 会调用 objc_autoreleasePoolPush() 方法。
    监听 KCFRunLoopBeforeWaiting 会调用 objc_autoreleasePoolPop()objc_autoreleasePoolPush() 方法。也就是在休眠之前会释放一次对象。
    监听 KCFRunLoopBeforeExit 会调用 objc_autoreleasePoolPop() 方法。
    所以一个对象的释放时机是由 RunLoop 管理的,当线程处于休眠的状态或者程序退出的时候会进行对象的 release 操作。

    引用计数的存储

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

    struct SideTable {
        spinlock_t slock;
        RefcountMap refcnts;
        weak_table_t weak_table;
    }
    

    足够存储的话会直接存储在 isa 中,否则存储在 SideTable 的散列表中。

    • refcnts 是一个存放着对象引用计数的散列表;
    • weak_table_t 是一个存放弱引用的散列表;

    我们可在 objc 中看到获得引用计数的源码:

    inline uintptr_t 
    objc_object::rootRetainCount()
    {
        // 如果是 Tagged Pointer 直接返回本身
        if (isTaggedPointer()) return (uintptr_t)this;
    
        sidetable_lock();
        isa_t bits = LoadExclusive(&isa.bits); // isa.bits 就是 isa 本身
        ClearExclusive(&isa.bits);
        if (bits.nonpointer) { // 判断是否是优化过的指针
            uintptr_t rc = 1 + bits.extra_rc; 
            if (bits.has_sidetable_rc) { // 说明引用计数不是存储在 isa 中,而是存在 SideTable 中
                rc += sidetable_getExtraRC_nolock();
            }
            sidetable_unlock();
            return rc;
        }
    
        sidetable_unlock();
        return sidetable_retainCount();
    }
    

    sidetable_getExtraRC_nolock 函数为:

    size_t 
    objc_object::sidetable_getExtraRC_nolock()
    {
        assert(isa.nonpointer);
        SideTable& table = SideTables()[this];
        RefcountMap::iterator it = table.refcnts.find(this); // 根据 key 查找,this 应该指该对象的地址
        if (it == table.refcnts.end()) return 0;
        else return it->second >> SIDE_TABLE_RC_SHIFT; // 进行一次位运算返回
    }
    

    release 和 retain 的操作在源码中 rootReleaserootRetain 中,原理相同。

    weak 指针

    weak 指针表示一个弱引用,可以有效的解决循环引用的问题。在一个对象被销毁的时候,weak 指针指向的弱引用对象会自动置为 nil。

    当一个对象要释放的时候,会自动调用 dealloc,接下来的调用轨迹是:

    • dealloc
    • _objc_rootDealloc
    • object_dispose
    • objc_destructInstancefree

    在源码中可看到 _obj_rootDealloc 的源码:

    inline void
    objc_object::rootDealloc()
    {
        if (isTaggedPointer()) return;
        
        // 依次表示:是否优化、是否弱引用、是否关联对象、是否 C++ 析构函数、是否用 SideTable 存储引用计数
        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 中调用了 objc_destructInstance 函数,objc_destructInstance 的逻辑为:

    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            bool cxx = obj->hasCxxDtor();
            bool assoc = obj->hasAssociatedObjects();
            if (cxx) object_cxxDestruct(obj); // 清除成员变量
            if (assoc) _object_remove_assocations(obj); // 清除关联对象
            obj->clearDeallocating(); // 将当前的弱指针对象置为 nil
        }
    
        return obj;
    }
    

    clearDeallocating 会调用 clearDeallocating_slow 函数,其逻辑为:

    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); // 取出弱引用散列表,以对象地址为 key,取出弱引用对象进行处理
        }
        if (isa.has_sidetable_rc) {
            table.refcnts.erase(this);
        }
        table.unlock();
    }
    

    weak_clear_no_lock() 函数中找到对应对象后,进行了一次 weak_entry_remove() 操作,也就是移除弱引用。

    weak 操作是运行时的过程,它在运行时检测到对象被销毁,然后对弱指针引用的对象进行处理。这是 LLVM 编译器生成了相关内存管理的逻辑,然后配合运行时机制来管理若引用的结果。

    相关文章

      网友评论

        本文标题:iOS - 内存管理相关

        本文链接:https://www.haomeiwen.com/subject/kejxaqtx.html