iOS Block 原理
Block 是 iOS 开发中一种非常重要的特性,它本质上是 Objective-C 语言的扩展,类似于其他编程语言中的闭包(Closure)或 Lambda 表达式。Block 不仅可以封装代码,还可以携带执行环境(包括外部变量的引用),使得代码更加模块化和重用。
Block 的内部结构
- isa 指针:Block 本质上是一个 OC 对象,内部包含 isa 指针,指向其 Class 类。Block 有三种类型,都继承自 NSBlock,而 NSBlock 的基类是 NSObject。
- 调用信息:Block 内部包含了函数调用的地址和将来需要访问的变量等信息。
- 捕获机制:Block 能够捕获外部变量,根据变量的类型(局部变量、静态变量、全局变量)和存储位置(栈、堆)的不同,捕获的方式也有所不同。
Block 的类型
- NSGlobalBlock:没有访问外部变量的 Block,通常存储在全局数据区。
- NSStackBlock:访问了外部局部变量的 Block,默认存储在栈上。如果 Block 所在的函数返回,且没有对 Block 进行 copy 操作,该 Block 将被销毁。
- NSMallocBlock:通过 copy 操作从栈上转移到堆上的 Block,由开发者管理内存。
ARC 环境下的自动处理
在 ARC(Automatic Reference Counting,自动引用计数)环境下,编译器会根据情况自动将栈上的 Block 拷贝到堆上,例如:
- Block 作为函数返回值时。
- 将 Block 赋值给强指针时。
- Block 作为 Cocoa API 或 GCD API 的方法参数时。
注意事项
- 内存管理:
- 在 MRC(Manual Reference Counting,手动引用计数)环境下,需要手动管理 Block 的内存,避免内存泄漏或提前释放。
- 在 ARC 环境下,虽然编译器会自动处理大部分内存管理问题,但仍需注意避免循环引用。
- 访问外部变量:
- Block 可以访问函数以外、词法作用域以内的外部变量的值,但默认情况下,这些变量在 Block 内部是只读的。
- 如果需要在 Block 内部修改局部变量的值,应使用
__block
修饰符声明该变量。
- 循环引用:
- 当 Block 和一个对象相互强引用时,会造成循环引用,导致内存无法释放。此时,应使用
__weak
或_unsafe_unretained
修饰符来修饰对象,以避免循环引用。但需注意_unsafe_unretained
修饰符在对象被释放后不会将指针置为 nil,可能导致野指针问题。
- 当 Block 和一个对象相互强引用时,会造成循环引用,导致内存无法释放。此时,应使用
- 属性修饰符:
- 当 Block 作为属性时,通常应使用
copy
修饰符,以确保 Block 被正确复制到堆上,避免在函数返回后被销毁。
- 当 Block 作为属性时,通常应使用
- Block 的 copy、retain、release 操作:
- 不同于 NSObject 的内存管理操作,对 Block 的 copy、retain、release 操作通常不会改变其引用计数(retainCount 始终为 1)。
- NSGlobalBlock 的 copy、retain、release 操作均无效。
- NSStackBlock 的 retain、release 操作无效,必须 copy 到堆上才能有效使用。
- NSMallocBlock 支持 retain、release 操作,但引用计数始终为 1,实际是通过内存管理器中的计数来管理内存。
综上所述,Block 是 iOS 开发中非常强大的工具,但使用时也需要注意内存管理、变量访问权限、循环引用等问题,以确保代码的健壮性和稳定性。
iOS block修改NSMutableArray,需要添加__block吗
在iOS开发中,当你想要在block内部修改一个外部定义的NSMutableArray
(或任何可变集合,如NSMutableSet
、NSMutableDictionary
等)时,通常不需要在声明这个数组时加上__block
修饰符。这是因为NSMutableArray
(以及其他可变集合)是对象类型,它们是通过指针来引用的。当你在block内部修改这些对象时,你实际上是在修改它们指向的实例,而不是修改指针本身。
__block
修饰符主要用于在block内部修改在block外部定义的基本数据类型(如int
、float
等)或对象指针(但当你只是想要修改指针指向的对象时,通常不需要__block
)。使用__block
可以让这些变量在block内部被修改,并且这些修改能够反映到block外部。
然而,对于对象类型(如NSMutableArray
),即使你不使用__block
,你也可以在block内部安全地修改它们的内容(比如添加、删除元素等),因为这些操作不会改变对象指针本身,只是改变了对象内部的状态。
下面是一个简单的例子,展示了如何在block内部修改NSMutableArray
:
NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@"A", @"B", nil];
void (^myBlock)(void) = ^{
// 在block内部添加元素到mutableArray
[mutableArray addObject:@"C"];
// 打印修改后的数组,以验证修改是否生效
NSLog(@"%@", mutableArray);
};
// 执行block
myBlock();
// 再次打印数组,以验证block外部的数组是否也被修改了
NSLog(@"%@", mutableArray);
当然,让我们更详细地解释一下__block
修饰符与对象类型(如NSMutableArray
)之间的关系。
__block 修饰符的基本用途
__block
修饰符主要用于解决block捕获变量时的“值捕获”问题。在默认情况下,当block捕获一个外部变量时,它会捕获该变量的一个拷贝(对于基本数据类型)或一个指向该变量的常量指针(对于对象类型)。这意味着在block内部,你不能修改基本数据类型的值(除非使用__block
),也不能让对象指针指向一个新的对象(但你可以修改对象本身的状态)。
对象类型的特殊情况
然而,对于对象类型(如NSMutableArray
),情况有些不同。因为对象是通过指针来引用的,所以即使你不使用__block
,当你在block内部修改对象的内容时(比如调用addObject:
或removeObjectAtIndex:
等方法),你实际上是在修改对象本身,而不是修改指向该对象的指针。这种修改是“就地”发生的,不需要改变指针的指向,因此它会自动反映到block外部。
为什么通常不需要__block来修改对象
当你想要在block内部修改一个对象(比如向NSMutableArray
添加元素)时,你其实是在通过指针来操作该对象,而这个指针本身并没有被block捕获为常量。因此,你可以自由地修改对象的状态,而不需要担心这种修改不会被反映到block外部。
何时需要使用__block来修改对象
尽管在大多数情况下你不需要使用__block
来修改对象,但如果你需要在block内部修改一个指向对象的指针本身(即让指针指向一个新的对象),那么你就需要使用__block
了。然而,这种情况比较少见,并且通常与集合类型(如NSMutableArray
)无关,而是与对象指针的重新赋值有关。
示例
这里有一个示例,展示了何时可能需要使用__block
来修改对象指针:
__block SomeClass *someObject = [[SomeClass alloc] init];
void (^myBlock)(void) = ^{
// 如果想要让someObject指向一个新的对象,就需要使用__block
someObject = [[SomeClass alloc] initWithNewData];
// 注意:这里的修改只会影响block内部的someObject副本(如果它不是__block的),
// 但因为我们使用了__block,所以修改会反映到block外部。
};
// 执行block
myBlock();
// 现在,block外部的someObject也指向了新创建的对象
__weak
和 __strong
是Objective-C(OC)语言在自动引用计数(ARC)环境下用于管理对象生命周期的修饰符。它们的主要作用在于解决循环引用问题,特别是在使用block时。下面分别解释这两个修饰符的原理:
1. __weak
修饰符的原理
__weak
修饰符的主要作用是防止循环引用。当一个对象被 __weak
修饰后,它不会增加被引用对象的引用计数。这意味着,如果 __weak
修饰的变量所指向的对象被释放,该变量会自动被设置为 nil
,从而避免了野指针(dangling pointer)的问题。
在底层实现上,__weak
修饰符的变量会通过一个哈希表(weak table)来维护。当 __weak
变量被初始化时,会调用 objc_initWeak
函数,该函数内部会调用 objc_storeWeak
,将对象的地址和 __weak
指针的地址作为键值对存储在 weak table 中。当对象被释放时,会遍历 weak table,将指向该对象的所有 __weak
指针置为 nil
。
2. __strong
修饰符的原理
__strong
是Objective-C中默认的修饰符,用于表示对对象的强引用。当一个对象被 __strong
修饰的变量引用时,该对象的引用计数会增加。当 __strong
修饰的变量超出其作用域或被显式设置为 nil
时,对象的引用计数会减少。如果引用计数变为0,则对象会被释放。
在block中使用 __strong
修饰符的场景通常是为了避免在block执行期间,原本用 __weak
修饰的变量所指向的对象被提前释放。这是因为block可能会异步执行,而在block执行期间,原作用域中的对象可能已经被释放。此时,在block内部使用 __strong
修饰符来重新引用 __weak
变量所指向的对象,可以确保在block执行期间该对象不会被释放。然而,需要注意的是,这种强引用是临时的,当block执行完毕后,__strong
修饰的变量也会超出其作用域,对象的引用计数会相应减少。
总结
__weak
修饰符用于防止循环引用,不增加对象的引用计数,当对象被释放时自动置为nil
。__strong
修饰符表示对对象的强引用,增加对象的引用计数,当变量超出作用域或被显式设置为nil
时,对象的引用计数减少。- 在block中使用
__weak
和__strong
修饰符可以避免循环引用和野指针问题,确保block执行期间所需的对象不会被提前释放。