iOS内存管理iOS

iOS - 内存管理相关

2019-05-11  本文已影响5人  valentizx
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 的散列表中。

我们可在 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,接下来的调用轨迹是:

在源码中可看到 _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 编译器生成了相关内存管理的逻辑,然后配合运行时机制来管理若引用的结果。

上一篇下一篇

猜你喜欢

热点阅读