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

深入学习Objective-C中__block与__weak关键字

2022-10-261.9k 阅读

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

在深入探讨 __block__weak 关键字之前,我们先来回顾一下 Objective-C 的内存管理机制。Objective-C 采用引用计数(Reference Counting)的方式来管理对象的生命周期。当一个对象被创建时,它的引用计数初始化为 1。每当有一个新的指针指向该对象时,引用计数加 1;当一个指向对象的指针被释放或者重新赋值时,引用计数减 1。当对象的引用计数变为 0 时,系统会自动释放该对象所占用的内存。

例如,以下代码展示了基本的对象创建与引用计数操作:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj1 = [[NSObject alloc] init];
        NSLog(@"obj1 reference count: %lu", (unsigned long)[obj1 retainCount]);

        NSObject *obj2 = obj1;
        NSLog(@"obj1 reference count after obj2 = obj1: %lu", (unsigned long)[obj1 retainCount]);

        obj1 = nil;
        NSLog(@"obj2 reference count after obj1 = nil: %lu", (unsigned long)[obj2 retainCount]);

        obj2 = nil;
    }
    return 0;
}

在这段代码中,obj1 被创建后引用计数为 1。当 obj2 = obj1 时,obj1 的引用计数增加到 2。当 obj1 = nil 时,obj1 不再指向原对象,但原对象的引用计数因为 obj2 的存在仍为 1。最后 obj2 = nil,对象的引用计数变为 0,对象被释放。

二、__block 关键字的深入剖析

(一)__block 关键字的作用场景

__block 关键字主要用于解决在 block 内部对外部变量进行修改的问题。在 Objective-C 中,默认情况下,block 内部只能访问外部的局部变量,但不能修改它们。例如:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int number = 10;
        void (^block)(void) = ^{
            // 以下代码会报错,因为默认不能在 block 内修改外部局部变量
            // number = 20;
        };
        block();
    }
    return 0;
}

上述代码中,尝试在 block 内修改 number 变量会导致编译错误。为了解决这个问题,就需要使用 __block 关键字。

(二)__block 关键字的使用方法

使用 __block 关键字非常简单,只需在变量声明前加上 __block 修饰符即可。修改上述代码如下:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int number = 10;
        void (^block)(void) = ^{
            number = 20;
        };
        block();
        NSLog(@"number after block execution: %d", number);
    }
    return 0;
}

在这段代码中,通过 __block 修饰 number 变量后,block 内部就可以成功修改该变量的值。运行程序,输出结果为 number after block execution: 20

(三)__block 关键字的本质

从本质上讲,当使用 __block 修饰一个变量时,编译器会将该变量封装成一个结构体。这个结构体包含一个指向自身的指针以及变量的实际值。block 在捕获 __block 变量时,实际上捕获的是这个结构体的指针。这样,block 内部对变量的修改就可以通过结构体指针来操作外部的变量。

例如,假设有如下代码:

#import <Foundation/Foundation.h>

void testBlock() {
    __block int number = 10;
    void (^block)(void) = ^{
        number = 20;
    };
    block();
    NSLog(@"number: %d", number);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        testBlock();
    }
    return 0;
}

编译器在处理这段代码时,会将 __block int number 转换为类似如下的结构体定义(简化示意):

struct __Block_byref_number_0 {
    void *__isa;
    __Block_byref_number_0 *__forwarding;
    int __flags;
    int __size;
    int number;
};

在 block 内部,通过 __forwarding 指针来访问和修改 number 变量,从而实现对外部变量的修改。

(四)__block 与对象类型变量

__block 关键字不仅适用于基本数据类型,对于对象类型变量同样适用。例如:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSString *str = @"Initial String";
        void (^block)(void) = ^{
            str = @"New String";
        };
        block();
        NSLog(@"str: %@", str);
    }
    return 0;
}

在上述代码中,__block 修饰的 NSString 对象变量 str 可以在 block 内部被修改。运行程序,输出结果为 str: New String

然而,需要注意的是,当 __block 修饰对象类型变量时,block 对该对象的引用计数处理与普通情况有所不同。block 会对 __block 修饰的对象进行强引用(即使 block 本身是弱引用类型)。例如:

#import <Foundation/Foundation.h>

@interface MyObject : NSObject
@end

@implementation MyObject
- (void)dealloc {
    NSLog(@"MyObject deallocated");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block MyObject *obj = [[MyObject alloc] init];
        void (^block)(void) = ^{
            NSLog(@"obj in block: %@", obj);
        };
        block();
        obj = nil;
    }
    return 0;
}

在这段代码中,即使 obj 在外部被赋值为 nil,但由于 block 对 __block 修饰的 obj 进行了强引用,MyObject 对象并不会立即被释放。运行程序,输出结果为:

obj in block: <MyObject: 0x7f8f80d0c4c0>

并且不会输出 MyObject deallocated,直到 block 被释放,MyObject 对象的引用计数才会变为 0 并被释放。

三、__weak 关键字的深入剖析

(一)__weak 关键字的作用

__weak 关键字主要用于解决循环引用(Retain Cycle)的问题。在 Objective-C 中,当两个或多个对象相互强引用时,就会形成循环引用,导致对象无法被释放,从而造成内存泄漏。

例如,假设有两个类 ClassAClassB,它们相互持有对方的强引用:

#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
- (void)dealloc {
    NSLog(@"ClassA deallocated");
}
@end

@implementation ClassB
- (void)dealloc {
    NSLog(@"ClassB deallocated");
}
@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 = nil;
        b = nil;
    }
    return 0;
}

在上述代码中,ClassAClassB 相互持有对方的强引用,当 ab 被赋值为 nil 后,ClassAClassB 对象由于循环引用,引用计数不会变为 0,因此不会被释放,导致内存泄漏。运行程序,不会输出 ClassA deallocatedClassB deallocated

为了解决这个问题,我们可以使用 __weak 关键字。将 ClassB 中的 classA 属性改为弱引用:

#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
- (void)dealloc {
    NSLog(@"ClassA deallocated");
}
@end

@implementation ClassB
- (void)dealloc {
    NSLog(@"ClassB deallocated");
}
@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 = nil;
        b = nil;
    }
    return 0;
}

在修改后的代码中,ClassBClassA 的引用为弱引用,不会增加 ClassA 的引用计数。当 a 被赋值为 nil 时,ClassA 对象的引用计数变为 0 并被释放,此时 b.classA 会自动被设置为 nil(即弱引用具有自动归零特性)。接着 b 被赋值为 nilClassB 对象也会被释放。运行程序,会输出:

ClassA deallocated
ClassB deallocated

(二)__weak 关键字在 block 中的应用

在 block 中使用 __weak 关键字也是为了避免循环引用。当一个对象持有一个 block,而 block 又捕获了该对象时,如果不使用 __weak,就会形成循环引用。

例如,假设有如下代码:

#import <Foundation/Foundation.h>

@interface MyViewController : UIViewController
@property (nonatomic, copy) void (^block)(void);
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.block = ^{
        NSLog(@"self in block: %@", self);
    };
    self.block();
}
- (void)dealloc {
    NSLog(@"MyViewController deallocated");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyViewController *vc = [[MyViewController alloc] init];
        vc = nil;
    }
    return 0;
}

在这段代码中,MyViewController 对象持有 block,而 block 又捕获了 self,形成了循环引用。当 vc 被赋值为 nil 时,MyViewController 对象不会被释放,导致内存泄漏。运行程序,不会输出 MyViewController deallocated

为了避免这种情况,我们可以在 block 中使用 __weak 关键字:

#import <Foundation/Foundation.h>

@interface MyViewController : UIViewController
@property (nonatomic, copy) void (^block)(void);
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            NSLog(@"self in block: %@", strongSelf);
        }
    };
    self.block();
}
- (void)dealloc {
    NSLog(@"MyViewController deallocated");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyViewController *vc = [[MyViewController alloc] init];
        vc = nil;
    }
    return 0;
}

在修改后的代码中,首先使用 __weak 声明一个弱引用 weakSelf,然后在 block 内部将 weakSelf 提升为强引用 strongSelf。这样做的好处是,在 block 执行期间,strongSelf 会保持对 self 的强引用,防止 self 在 block 执行过程中被释放。同时,由于 weakSelf 是弱引用,不会导致循环引用。当 vc 被赋值为 nil 时,MyViewController 对象可以正常被释放,运行程序,会输出 MyViewController deallocated

(三)__weak 关键字的本质

__weak 关键字本质上是一种弱引用,它不会增加对象的引用计数。当对象的引用计数变为 0 并被释放时,所有指向该对象的 __weak 引用会自动被设置为 nil

在底层实现上,__weak 引用是通过一个哈希表(Hash Table)来管理的。当一个对象被创建时,系统会为其分配一个唯一的标识符。__weak 引用会将这个标识符作为键,将自身作为值存储在哈希表中。当对象被释放时,系统会遍历哈希表,将所有指向该对象的 __weak 引用设置为 nil

四、__block 与 __weak 关键字的对比

(一)作用不同

__block 关键字主要用于解决 block 内部对外部变量的修改问题,它使得 block 可以修改外部的局部变量。而 __weak 关键字主要用于解决循环引用问题,避免对象之间的相互强引用导致内存泄漏。

(二)对对象引用的影响不同

__block 修饰对象类型变量时,block 会对该对象进行强引用。这意味着即使在外部变量被释放后,只要 block 存在,对象就不会被释放。而 __weak 关键字修饰的对象引用是弱引用,不会增加对象的引用计数,当对象的引用计数变为 0 并被释放时,__weak 引用会自动被设置为 nil

(三)应用场景不同

__block 适用于需要在 block 内部修改外部局部变量的场景,无论是基本数据类型还是对象类型变量。而 __weak 主要应用于存在循环引用风险的场景,如两个对象相互引用或者对象与 block 之间的相互引用。

例如,在一个简单的计数器应用中,如果需要在 block 内部修改计数器的值,就可以使用 __block 关键字:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int counter = 0;
        void (^incrementBlock)(void) = ^{
            counter++;
        };
        incrementBlock();
        NSLog(@"counter: %d", counter);
    }
    return 0;
}

而在一个视图控制器持有 block,且 block 需要访问视图控制器属性的场景中,为了避免循环引用,就需要使用 __weak 关键字:

#import <Foundation/Foundation.h>

@interface MyViewController : UIViewController
@property (nonatomic, strong) NSString *titleString;
@property (nonatomic, copy) void (^block)(void);
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.titleString = @"My Title";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            NSLog(@"title in block: %@", strongSelf.titleString);
        }
    };
    self.block();
}
- (void)dealloc {
    NSLog(@"MyViewController deallocated");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyViewController *vc = [[MyViewController alloc] init];
        vc = nil;
    }
    return 0;
}

五、总结与注意事项

通过深入学习 __block__weak 关键字,我们了解到它们在 Objective-C 编程中有着重要的作用。__block 为我们提供了在 block 内部修改外部变量的能力,而 __weak 则有效地解决了循环引用导致的内存泄漏问题。

在实际应用中,需要注意以下几点:

  1. __block 与内存管理:当 __block 修饰对象类型变量时,要注意 block 对对象的强引用可能导致对象无法及时释放。在使用完毕后,应确保 block 被释放,以避免内存泄漏。
  2. __weak 与自动归零__weak 引用具有自动归零特性,这在处理对象生命周期时非常有用。但在使用 __weak 引用时,需要注意在访问对象前检查其是否为 nil,以防止野指针错误。
  3. 合理使用关键字:根据具体的需求选择合适的关键字。如果只是需要在 block 内部修改外部变量,使用 __block;如果存在循环引用风险,使用 __weak。同时,要注意在复杂的对象关系中,正确判断是否存在循环引用,并合理运用 __weak 来解决问题。

总之,熟练掌握 __block__weak 关键字的使用方法和原理,对于编写高效、稳定的 Objective-C 代码至关重要。