最近在看Swift闭包截获变量时遇到了各种问题,总结之后发现主要是还用停留在OC时代的思维来思考Swift问题导致的。借此机会首先复习一下OC中关于block的细节,同时整理Swift中闭包的相关的问题。不管是目前使用OC还是Swift,又或者是从OC转向Swift,都可以阅读这篇文章并与我交流。
OC的block
OC的block已经有很多相关的文章介绍了,主要难点在于__block
修饰符的作用和原理,以及循环引用问题。我们首先由浅入深举几个例子看一看__block
修饰符,最后分析循环引用问题。这里的讨论都是基于ARC的。
截获基本类型
1 2 3 4 5 6 7 8 |
int value = 10; void(^block)() = ^{ NSLog(@"value = %d", value); }; value = 20; block(); // 打印结果是:"value = 10" |
OC的block会截获外部变量,对于int
等基本数据类型,block的内部会拷贝一份,简单来说,它的实现大概是这样的:
1 2 3 4 |
struct block_impl { //其它内容 int value; }; |
因为block内部拷贝了截获的变量的副本,所以生成block后再修改变量,不会影响被block截获的变量。同时block内部也不能修改这个变量。
修改基本类型
如果要想在block中修改被截获的基本类型变量,我们需要把它标记为__block
:
1 2 3 4 5 6 7 8 |
__block int value = 10; void(^block)() = ^{ NSLog(@"value = %d", value); }; value = 20; block(); // 打印结果是:"value = 20" |
这是因为,对于被标记了__block
的变量,block在截获它时,会保存一个指针。简单来说,它的实现大概是这样的:
1 2 3 4 5 6 7 8 |
struct block_impl { //其它内容 block_ref_value *value; }; struct block_ref_value { int value; // 这里保存的才是被截获的value的值。 }; |
由于block中一直有一个指针指向value,所以block内部对它的修改,可以影响到block外部的变量。因为block修改的就是那个外部变量而不是外部变量的副本。
上面关于block具体实现的例子只是一个简化模型,事实上并非如此,但本质类似。总的来说,只有由__block
修饰符修饰的变量,在被block截获时才是可变的。关于这方面的详细解释,可以参考这三篇文章:
- iOS OC语言: Block底层实现原理:这个很详细地讲了
__block
的实现原理 - Block的引用循环问题 (ARC & non-ARC):这个讲了一些block底层的实现原理以及循环引用问题。
- 你真的理解__block修饰符的原理么?:这是我之前写过的一篇介绍
__block
原理的文章,内容会详细一些。
截获指针
block截获指针和截获基本类型是相似的,不过稍稍复杂一些。先看一个最简单的例子。
1 2 3 4 5 6 7 8 9 |
Person *p = [[Person alloc] initWithName:@"zxy"]; void(^block)() = ^{ NSLog(@"person name = %@", p.name); }; p.name = @"new name"; block(); // 打印结果是:"person name = new name" |
在截获基本类型时,block内部可能会有int capturedValue = value;
这样的代码,类比到指针也是一样的,block内部也会有这样的代码:Person *capturedP = p;
。在ARC下,这其实是强引用(retain)了block外部的p
。
由于block内部的p
和外部的p
指向的是同一块内存地址。所以在block外部修改p
的属性,依然会影响到block内部截获的p
。
需要强调一点,这里的p
依然不是可变的。修改p
的name
不是改变p
,只是改变p
内部的属性:
1 2 3 4 5 6 7 8 |
Person *p = [[Person alloc] initWithName:@"zxy"]; void(^block)() = ^{ p.name = @"new name"; //OK,没有改变p p = [[Person alloc] initWithName:@"new name"]; //编译错误 NSLog(@"person name = %@", p.name); }; block(); |
改变指针
类比__block
修饰符对基本类型的作用原理,由它修饰的指针,在被block截获时,截获的其实是这个指针的指针。比如我们把刚刚的例子修改一下:
OC的block已经有很多相关的文章介绍了,主要难点在于__block
修饰符的作用和原理,以及循环引用问题。我们首先由浅入深举几个例子看一看__block
修饰符,最后分析循环引用问题。这里的讨论都是基于ARC的。
截获基本类型
1 2 3 4 5 6 7 8 |
int value = 10; void(^block)() = ^{ NSLog(@"value = %d", value); }; value = 20; block(); // 打印结果是:"value = 10" |
OC的block会截获外部变量,对于int
等基本数据类型,block的内部会拷贝一份,简单来说,它的实现大概是这样的:
1 2 3 4 |
struct block_impl { //其它内容 int value; }; |
因为block内部拷贝了截获的变量的副本,所以生成block后再修改变量,不会影响被block截获的变量。同时block内部也不能修改这个变量。
修改基本类型
如果要想在block中修改被截获的基本类型变量,我们需要把它标记为__block
:
1 2 3 4 5 6 7 8 |
__block int value = 10; void(^block)() = ^{ NSLog(@"value = %d", value); }; value = 20; block(); // 打印结果是:"value = 20" |
这是因为,对于被标记了__block
的变量,block在截获它时,会保存一个指针。简单来说,它的实现大概是这样的:
1 2 3 4 5 6 7 8 |
struct block_impl { //其它内容 block_ref_value *value; }; struct block_ref_value { int value; // 这里保存的才是被截获的value的值。 }; |
由于block中一直有一个指针指向value,所以block内部对它的修改,可以影响到block外部的变量。因为block修改的就是那个外部变量而不是外部变量的副本。
上面关于block具体实现的例子只是一个简化模型,事实上并非如此,但本质类似。总的来说,只有由__block
修饰符修饰的变量,在被block截获时才是可变的。关于这方面的详细解释,可以参考这三篇文章:
- iOS OC语言: Block底层实现原理:这个很详细地讲了
__block
的实现原理 - Block的引用循环问题 (ARC & non-ARC):这个讲了一些block底层的实现原理以及循环引用问题。
- 你真的理解__block修饰符的原理么?:这是我之前写过的一篇介绍
__block
原理的文章,内容会详细一些。
截获指针
block截获指针和截获基本类型是相似的,不过稍稍复杂一些。先看一个最简单的例子。
1 2 3 4 5 6 7 8 9 |
Person *p = [[Person alloc] initWithName:@"zxy"]; void(^block)() = ^{ NSLog(@"person name = %@", p.name); }; p.name = @"new name"; block(); // 打印结果是:"person name = new name" |
在截获基本类型时,block内部可能会有int capturedValue = value;
这样的代码,类比到指针也是一样的,block内部也会有这样的代码:Person *capturedP = p;
。在ARC下,这其实是强引用(retain)了block外部的p
。
由于block内部的p
和外部的p
指向的是同一块内存地址。所以在block外部修改p
的属性,依然会影响到block内部截获的p
。
需要强调一点,这里的p
依然不是可变的。修改p
的name
不是改变p
,只是改变p
内部的属性:
1 2 3 4 5 6 7 8 |
Person *p = [[Person alloc] initWithName:@"zxy"]; void(^block)() = ^{ p.name = @"new name"; //OK,没有改变p p = [[Person alloc] initWithName:@"new name"]; //编译错误 NSLog(@"person name = %@", p.name); }; block(); |
改变指针
类比__block
修饰符对基本类型的作用原理,由它修饰的指针,在被block截获时,截获的其实是这个指针的指针。比如我们把刚刚的例子修改一下: