深入学习Objective-C中__block与__weak关键字
一、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 中,当两个或多个对象相互强引用时,就会形成循环引用,导致对象无法被释放,从而造成内存泄漏。
例如,假设有两个类 ClassA
和 ClassB
,它们相互持有对方的强引用:
#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;
}
在上述代码中,ClassA
和 ClassB
相互持有对方的强引用,当 a
和 b
被赋值为 nil
后,ClassA
和 ClassB
对象由于循环引用,引用计数不会变为 0,因此不会被释放,导致内存泄漏。运行程序,不会输出 ClassA deallocated
和 ClassB 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;
}
在修改后的代码中,ClassB
对 ClassA
的引用为弱引用,不会增加 ClassA
的引用计数。当 a
被赋值为 nil
时,ClassA
对象的引用计数变为 0 并被释放,此时 b.classA
会自动被设置为 nil
(即弱引用具有自动归零特性)。接着 b
被赋值为 nil
,ClassB
对象也会被释放。运行程序,会输出:
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
则有效地解决了循环引用导致的内存泄漏问题。
在实际应用中,需要注意以下几点:
- __block 与内存管理:当
__block
修饰对象类型变量时,要注意 block 对对象的强引用可能导致对象无法及时释放。在使用完毕后,应确保 block 被释放,以避免内存泄漏。 - __weak 与自动归零:
__weak
引用具有自动归零特性,这在处理对象生命周期时非常有用。但在使用__weak
引用时,需要注意在访问对象前检查其是否为nil
,以防止野指针错误。 - 合理使用关键字:根据具体的需求选择合适的关键字。如果只是需要在 block 内部修改外部变量,使用
__block
;如果存在循环引用风险,使用__weak
。同时,要注意在复杂的对象关系中,正确判断是否存在循环引用,并合理运用__weak
来解决问题。
总之,熟练掌握 __block
与 __weak
关键字的使用方法和原理,对于编写高效、稳定的 Objective-C 代码至关重要。