MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的__weak与__strong变量修饰符语义

2021-05-293.4k 阅读

一、Objective-C 内存管理基础回顾

在深入探讨 __weak__strong 变量修饰符之前,先简单回顾一下 Objective-C 的内存管理机制。Objective-C 使用引用计数(Reference Counting)来管理对象的生命周期。当一个对象被创建时,它的引用计数初始化为 1。每当有一个新的变量指向这个对象时,引用计数就会增加 1;而当指向该对象的变量不再使用(例如超出作用域或被赋值为 nil)时,引用计数就会减 1。当对象的引用计数变为 0 时,系统会自动释放该对象所占用的内存。

例如,以下是一个简单的对象创建和释放的示例:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建一个 NSString 对象
        NSString *string = [[NSString alloc] initWithString:@"Hello, World!"];
        // 这里 string 指向的对象引用计数为 1
        NSLog(@"%@", string);
        // string 超出作用域,对象引用计数减 1,由于此时引用计数变为 0,对象被释放
    }
    return 0;
}

在 ARC(自动引用计数,Automatic Reference Counting)出现之前,开发者需要手动调用 retain 来增加引用计数,调用 release 来减少引用计数。ARC 则让编译器自动插入这些 retainrelease 调用,大大减轻了开发者的负担。

二、__strong 变量修饰符

1. __strong 的基本语义

__strong 是 ARC 下对象变量的默认修饰符。一个被 __strong 修饰的变量会持有指向对象的强引用。这意味着只要有一个 __strong 变量指向某个对象,该对象的引用计数就会至少为 1,不会被释放。

看下面的代码示例:

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        // person 是一个 __strong 变量,持有 Person 对象的强引用
        person.name = @"John";
        NSLog(@"Person's name: %@", person.name);
        // person 超出作用域,Person 对象引用计数减 1,由于 name 也是 __strong 类型,持有 NSString 对象的强引用,所以 NSString 对象不会被释放
    }
    return 0;
}

在上述代码中,person 变量是 __strong 类型,它持有 Person 对象的强引用。Person 类中的 name 属性默认也是 __strong 类型,它持有 NSString 对象的强引用。所以在 person 超出作用域之前,Person 对象和 NSString 对象都不会被释放。

2. __strong 变量之间的赋值

当一个 __strong 变量被赋值给另一个 __strong 变量时,会发生引用计数的变化。具体来说,目标变量会持有源变量所指向对象的强引用,源变量对该对象的引用计数会减 1。如果源变量之前是唯一指向该对象的 __strong 变量,那么对象的引用计数会变为 0 并被释放。

以下是示例代码:

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
@end

@implementation MyClass
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj1 = [[MyClass alloc] init];
        // obj1 持有 MyClass 对象的强引用,引用计数为 1
        MyClass *obj2 = obj1;
        // obj2 也持有 MyClass 对象的强引用,引用计数变为 2,obj1 引用计数不变
        obj1 = nil;
        // obj1 对 MyClass 对象的引用计数减 1,此时 MyClass 对象引用计数为 1,因为 obj2 还持有强引用,所以对象不会被释放
        NSLog(@"obj2 still exists");
    }
    return 0;
}

在这个例子中,obj1 首先创建并指向 MyClass 对象,引用计数为 1。当 obj2 = obj1 时,obj2 也指向该对象,引用计数变为 2。当 obj1 = nil 时,obj1 对对象的引用计数减 1,但由于 obj2 仍然持有强引用,所以 MyClass 对象不会被释放。

3. __strong 变量与方法参数

当一个 __strong 变量作为方法参数传递时,方法内部会对该对象持有一个强引用。这意味着在方法执行期间,对象不会被释放。

例如:

#import <Foundation/Foundation.h>

@interface Helper : NSObject
+ (void)printObject:(id)obj;
@end

@implementation Helper
+ (void)printObject:(id)obj {
    NSLog(@"Object: %@", obj);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *string = [[NSString alloc] initWithString:@"Test"];
        [Helper printObject:string];
        // 方法执行完毕后,虽然方法内部对 string 对象的强引用消失,但外部 string 变量仍然持有强引用,所以 string 对象不会被释放
    }
    return 0;
}

printObject: 方法中,obj 参数是一个 __strong 类型(默认),它持有传递进来的 string 对象的强引用。方法执行完毕后,objstring 对象的引用计数减 1,但由于外部的 string 变量仍然持有强引用,所以 string 对象不会被释放。

三、__weak 变量修饰符

1. __weak 的基本语义

__weak 变量修饰符用于创建一个指向对象的弱引用。与 __strong 不同,__weak 变量不会增加对象的引用计数。当对象的所有 __strong 引用都消失,对象被释放时,所有指向该对象的 __weak 变量会自动被设置为 nil

以下是一个简单的示例:

#import <Foundation/Foundation.h>

@interface MyObject : NSObject
@end

@implementation MyObject
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __strong MyObject *strongObj = [[MyObject alloc] init];
        __weak MyObject *weakObj = strongObj;
        // weakObj 指向 strongObj 所指向的对象,但不增加对象的引用计数
        NSLog(@"weakObj: %@", weakObj);
        strongObj = nil;
        // strongObj 对 MyObject 对象的强引用消失,对象被释放,此时 weakObj 自动被设置为 nil
        NSLog(@"weakObj after strongObj is nil: %@", weakObj);
    }
    return 0;
}

在上述代码中,weakObj 是一个 __weak 变量,它指向 strongObj 所指向的 MyObject 对象,但不会增加对象的引用计数。当 strongObj 被设置为 nil 时,MyObject 对象的引用计数变为 0 并被释放,同时 weakObj 自动被设置为 nil。这样可以避免野指针(dangling pointer)的问题,提高程序的安全性。

2. __weak 用于解决循环引用问题

循环引用是在内存管理中常见的问题,特别是在对象之间存在相互引用时容易发生。例如,假设有两个类 ClassAClassBClassA 持有一个 ClassB 的实例,ClassB 又持有一个 ClassA 的实例,如果这两个引用都是 __strong 类型,就会形成循环引用,导致对象无法被释放。

以下是循环引用的示例代码:

#import <Foundation/Foundation.h>

@interface ClassB;

@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end

@interface ClassB : NSObject
@property (nonatomic, strong) ClassA *classA;
@end

@implementation ClassA
@end

@implementation ClassB
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ClassA *a = [[ClassA alloc] init];
        ClassB *b = [[ClassB alloc] init];
        a.classB = b;
        b.classA = a;
        // a 和 b 之间形成循环引用,即使 a 和 b 超出作用域,它们所指向的对象也不会被释放
    }
    return 0;
}

为了解决这个问题,可以将其中一个引用改为 __weak 类型。例如:

#import <Foundation/Foundation.h>

@interface ClassB;

@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end

@interface ClassB : NSObject
@property (nonatomic, weak) ClassA *classA;
@end

@implementation ClassA
@end

@implementation ClassB
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ClassA *a = [[ClassA alloc] init];
        ClassB *b = [[ClassB alloc] init];
        a.classB = b;
        b.classA = a;
        // 这里 classB 是 __strong 引用,classA 是 __weak 引用,不会形成循环引用,当 a 和 b 超出作用域时,它们所指向的对象会被正确释放
    }
    return 0;
}

在这个修改后的代码中,ClassB 中的 classA 属性被声明为 __weak 类型,这样就打破了循环引用。当 ab 超出作用域时,ClassA 对象的引用计数会因为 a 的消失而变为 0 并被释放,此时 ClassB 对象中 classA 指向的 ClassA 对象已经不存在,classA 会自动被设置为 nil。接着,ClassB 对象的引用计数也会因为 b 的消失和 classA 不再持有强引用而变为 0 并被释放。

3. __weak 变量在闭包(Block)中的使用

在使用闭包(Block)时,如果闭包捕获了一个 __strong 变量,可能会导致循环引用。例如:

#import <Foundation/Foundation.h>

@interface MyViewController : UIViewController
@property (nonatomic, strong) NSString *titleText;
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf) {
            strongSelf.titleText = @"New Title";
        }
    });
}
@end

在上述代码中,MyViewController 类有一个 titleText 属性。在 viewDidLoad 方法中,创建了一个延迟执行的闭包。如果直接在闭包中使用 self,会捕获 self 的强引用,形成循环引用(因为闭包被持有在队列中,而闭包又持有 self 的强引用)。为了避免这种情况,先创建一个 __weak 类型的 weakSelf 来捕获 self。在闭包内部,再将 weakSelf 赋值给一个 __strong 类型的 strongSelf,这样在闭包执行期间,self 不会被意外释放,同时也避免了循环引用。

四、__weak 与 __strong 的性能考量

1. __strong 的性能

__strong 变量的操作相对简单直接,因为它只是增加和减少对象的引用计数。编译器在处理 __strong 变量时,会在合适的位置插入 retainrelease 调用(在 ARC 下自动完成)。由于 __strong 变量持有对象的强引用,只要有 __strong 变量指向对象,对象就不会被释放,这在某些情况下可能会导致不必要的内存占用。

例如,在一个频繁创建和销毁对象的场景中,如果对象被 __strong 变量长时间持有,可能会影响内存的回收效率,导致内存峰值升高。

2. __weak 的性能

__weak 变量的实现相对复杂一些。由于 __weak 变量需要在对象被释放时自动设置为 nil,系统需要额外的机制来跟踪对象的生命周期。这通常涉及到一个弱引用表(weak reference table),当对象的引用计数变为 0 时,系统会遍历这个表,将所有指向该对象的 __weak 变量设置为 nil

虽然 __weak 变量不会增加对象的引用计数,有助于避免循环引用和及时释放不再使用的对象,但由于其实现机制,在性能上会有一定的开销。特别是在大量使用 __weak 变量的情况下,对弱引用表的操作可能会成为性能瓶颈。

因此,在选择使用 __weak 还是 __strong 变量时,不仅要考虑内存管理的正确性,还要根据具体的应用场景和性能需求来权衡。如果对象之间的关系比较简单,不存在循环引用的风险,使用 __strong 变量通常可以获得更好的性能。而在可能出现循环引用的情况下,或者需要对象被释放后自动清理引用的场景,__weak 变量则是更好的选择。

五、__weak 和 __strong 在多线程环境下的行为

1. __strong 在多线程环境下

在多线程环境中,__strong 变量的引用计数操作并非原子性的。这意味着如果多个线程同时对同一个对象的 __strong 变量进行赋值或释放操作,可能会导致数据竞争和未定义行为。

例如,假设线程 A 正在释放一个对象(减少其引用计数),而线程 B 同时尝试访问该对象,由于引用计数操作不是原子的,可能在线程 A 减少引用计数到 0 并释放对象后,线程 B 仍然认为对象存在并尝试访问,从而导致程序崩溃。

为了避免这种情况,在多线程环境下对 __strong 变量的操作通常需要使用锁(如 @synchronizedNSLock)来保证线程安全。

以下是一个简单的示例,展示如何使用 @synchronized 来保护 __strong 变量的操作:

#import <Foundation/Foundation.h>

@interface SharedObject : NSObject
@property (nonatomic, strong) NSString *data;
@end

@implementation SharedObject
@end

void threadFunction(SharedObject *sharedObj) {
    @synchronized(sharedObj) {
        NSString *localData = sharedObj.data;
        // 对 localData 进行操作,这里假设是一些复杂的计算
        NSLog(@"Thread processed data: %@", localData);
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SharedObject *sharedObject = [[SharedObject alloc] init];
        sharedObject.data = @"Initial Data";

        NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunction:) object:sharedObject];
        NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunction:) object:sharedObject];

        [thread1 start];
        [thread2 start];

        [thread1 join];
        [thread2 join];
    }
    return 0;
}

在这个示例中,@synchronized(sharedObj) 确保了在同一时间只有一个线程可以访问和操作 sharedObjdata 属性,从而避免了数据竞争。

2. __weak 在多线程环境下

__weak 变量在多线程环境下也存在一些潜在的问题。由于 __weak 变量的更新(当对象被释放时设置为 nil)涉及到系统对弱引用表的操作,多个线程同时访问和修改弱引用表可能会导致竞争条件。

例如,假设线程 A 正在释放一个对象,系统开始更新弱引用表,将所有指向该对象的 __weak 变量设置为 nil。同时,线程 B 可能正在读取其中一个 __weak 变量的值,由于弱引用表的更新不是原子的,线程 B 可能会读取到不一致的数据,即 __weak 变量还没有被完全更新为 nil 时的值。

为了在多线程环境中安全地使用 __weak 变量,同样需要采取同步措施。可以使用与 __strong 变量类似的锁机制,确保在对 __weak 变量进行操作时,不会发生数据竞争。

另外,在多线程环境下,使用 __weak 变量时还需要注意对象的生命周期管理。由于多个线程可能同时影响对象的引用计数,需要更加谨慎地确保对象在合适的时机被释放,并且 __weak 变量能够正确地更新为 nil

六、__weak 和 __strong 与其他内存管理概念的结合

1. __weak 和 __strong 与 autorelease

autorelease 是 Objective-C 内存管理中的一个重要概念,它允许对象在自动释放池(@autoreleasepool)结束时自动释放。在 ARC 环境下,编译器会自动插入 autorelease 调用。

__strong__weak 变量与 autorelease 相互配合。当一个对象被发送 autorelease 消息时,它的引用计数会增加(但增加的方式与 retain 略有不同),并且会被放入最近的自动释放池中。当自动释放池被销毁时,池中的所有对象会收到 release 消息。

例如:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *string = [[[NSString alloc] initWithString:@"Autorelease Example"] autorelease];
        // string 是 __strong 类型,持有 NSString 对象的强引用,同时对象被放入自动释放池
        NSLog(@"%@", string);
    }
    // 自动释放池结束,string 所指向的对象收到 release 消息,如果此时对象的引用计数变为 0,则会被释放
    return 0;
}

在这个例子中,string__strong 类型,持有 NSString 对象的强引用。同时,对象通过 autorelease 被放入自动释放池。当自动释放池结束时,对象会收到 release 消息。如果此时对象的引用计数(除了 string 的强引用外,可能还有其他引用)变为 0,对象就会被释放。

对于 __weak 变量,如果一个对象被 autorelease 后,在自动释放池销毁前,对象的所有 __strong 引用都消失,对象被释放,那么指向该对象的 __weak 变量会自动被设置为 nil

2. __weak 和 __strong 与内存缓存

在内存缓存(如 NSCache)的场景中,__strong__weak 变量也有着不同的应用。NSCache 是一个类似于字典的容器,用于缓存对象。它的特点是当系统内存不足时,会自动释放缓存中的对象。

通常,缓存中的对象会被 __strong 引用,这样可以确保对象在缓存中不会被意外释放。例如:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSCache *cache = [[NSCache alloc] init];
        NSString *key = @"SomeKey";
        NSString *value = [[NSString alloc] initWithString:@"Cached Value"];
        [cache setObject:value forKey:key];
        // cache 使用 __strong 引用持有 value 对象,确保对象在缓存中不会被释放
        NSString *retrievedValue = [cache objectForKey:key];
        NSLog(@"Retrieved value: %@", retrievedValue);
    }
    return 0;
}

然而,在某些情况下,可能需要使用 __weak 引用。例如,如果缓存中的对象本身又持有其他对象,并且希望在缓存对象被释放时,这些被持有的对象也能正确释放,避免循环引用,可以考虑使用 __weak

另外,在处理缓存的淘汰策略时,如果希望在缓存对象被淘汰后,与之相关的其他对象(通过 __weak 引用)能自动清理,__weak 变量就非常有用。

七、总结 __weak 与 __strong 的使用场景

  1. 使用 __strong 的场景
    • 简单对象持有:当一个对象需要被另一个对象直接持有,并且不存在循环引用风险时,使用 __strong 是最简单和高效的选择。例如,一个视图控制器持有它所管理的视图对象,这些视图对象通常不会反过来持有视图控制器,所以可以使用 __strong 引用。
    • 方法参数和局部变量:在方法内部作为参数传递或定义的局部变量,如果需要在方法执行期间确保对象不会被释放,使用 __strong。因为 __strong 变量的操作简单直接,性能开销小。
  2. 使用 __weak 的场景
    • 解决循环引用:在对象之间可能存在相互引用,容易形成循环引用的情况下,使用 __weak 来打破循环。如前面提到的 ClassAClassB 相互引用的例子,将其中一个引用改为 __weak 可以确保对象能被正确释放。
    • 避免野指针:当希望在对象被释放后,指向该对象的变量自动变为 nil,避免野指针错误时,使用 __weak。这在处理视图控制器之间的父子关系,或者对象之间的临时关联时非常有用。
    • 闭包中的使用:在闭包捕获对象时,为了避免循环引用,通常先使用 __weak 捕获对象,然后在闭包内部再根据需要转换为 __strong 引用,以确保在闭包执行期间对象不会被释放。

总之,正确理解和使用 __weak__strong 变量修饰符对于编写高效、健壮的 Objective-C 代码至关重要。在实际编程中,需要根据具体的对象关系、内存管理需求以及性能要求来合理选择使用哪种修饰符。