前文地址:《iOS基础深入补完计划》
在前文、我们提到了property中的关键字copy可以修饰block。
那么、block究竟有哪些特殊。
block该怎样声明
__block、__weak、__strong是如何工作的呢。
__block又是否可以解决循环引用呢?
首先、关于block的作用域
- 总的来说。block默认是存档在栈中(作用于被限制在当前上下文(函数, 方法...))、可能被随时回收(比如代码块执行完毕、会将块内变量内存回收)。通过copy操作可以使其在堆中保留一份,、相当于一直强引用着。以此保证在执行block对象时、程序不会因为执行一个空对象而崩溃。
举个例子
@interface ViewController () @property (nonatomic, copy) void(^myblock)(); @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self test];//当此代码结束时,test函数中的所有存储在栈区的变量都会被系统释放。 self.myblock(); } - (void)test { int n = 5; [self setMyblock:^{ NSLog(@"%d",n); }]; }
之后、把copy声明换成assign。
EXC_BAD_ACCESS、使用了一个野指针。
此外、我们还可以进一步确认崩溃的原因。
之后、到底是不是因为局部变量被释放、导致的崩溃?
用成员变量试试。
看来无论是否使用成员变量、内部的变量指针都会被释放
结论:
- copy声明会把变量在block保存一份指针变量、指向原对象地址。保证block内部所引用变量不会被自动释放而造成崩溃。
- assign声明的block、内部引用的变量(无论是否是局部变量)在出了赋值当前作用域之后、会被释放。导致崩溃。
除此之外:
- block声明为成员变量、用strong修饰。也是没有问题的、作用和copy一样。
- block声明为成员变量、不加任何修饰。效果也和copy/strong一样。
- block声明为局部变量、不加任何修饰。效果也和copy/strong一样。
关于copy之后block的一些特性
会对其内部的使用对象引用计数+1
这也是为什么需要用__weak去修饰self以防止循环引用的原因。
更多的解释可以参阅《__weak与__block区别,深层理解两者区别》、《深入分析 Objective-C block、weakself、strongself 实现原理》原理总结下来就是
__weak会新开辟一个变量指针、指向被修饰的变量地址、但不会对该变量增加引用。
当被修饰变量被释放、__weak变量指针依旧会指向原有变量地址、而该地址则会返回空。
所以需要用__strong在block内部修饰、让block内的变量不会在中途被释放掉、保证block内容一致性。
默认情况下、内外部的变量数据不会互通。
int a = 1; int (^backValue)(int) = ^(int num) { return num+a; }; a += 1;//这次+=1不会被传递到block中a上。 int b = backValue(1); NSLog(@"b-->%d", b); //输出结果b-->2
当然、我们可以__block来修饰这个a、使其在block内部的指针也可以被修改
为什么用__block修饰就可以修改外部的变量、或者将外部变量的修改传递进block内了?
先写结论、不愿意细看可以跳过。
无论是普通变量、还是OC变量。在不使用__block的情况下、无法对其本身进行修改。
因为二者、都只会在block执行代码中copy一份值/指向该对象地址的指针。即使修改了这个值/指针指向、也无法对外部造成影响。所以编译器干脆就禁止这个操作了(
虽然你可以通过直接操作指针的方式修改、但是被修改的依旧只是block内部的指针变量而已
)
OC对象在不使用__block修饰的情况下、可以修改其属性。
自身的指针会被捕获到block代码块中、借此操作该指针指向地址所属对象的属性。(这也是为什么在block里可以修改self.xxx属性的原因)。
OC对象以及普通变量在使用__block修饰的情况下、可以对自身进行修改。
会将原变量的指针从栈移动至堆区。由block对象进行捕获保存、并借此进行修改。
以下是相关分析
这里有一篇文章、借助编译后的block阐释了__block的工作原理
《你真的理解__block修饰符的原理么》
- 简而言之、block捕获普通变量其实将变量的值捕获到了block执行块中。这个值已经与原变量无关。(由于无法直接获得原变量、技术上无法实现修改、所以编译器直接禁止了)。
__block声明的变量、在被block捕获时。会被block以指针变量的形式保存在自身处于堆中结构体内。同时、原对象的指针地址也被修改到了堆中。既然变量指针内存地址相同、就代表是同一个指针、自然可以同步修改。
具体可见:《iOS中__block 关键字的底层实现原理》、《一篇文章看懂iOS代码块Block》
当然、还有个其他情况。就是OC对象的属性。
当block执行代码需要捕获的是OC对象时、block会自动在block代码块中copy一个对象指针(普通对象是copy值)、指向该对象当前地址。
就会出现如下情况
不过、并不允许你为这个存在栈中的变量指针直接重新指定一个新的指向。除非使用__block将这个对象的地址直接移动到堆中。
此外、还做了另外一个实验。
这也证明、即便是OC对象也仅仅是捕获了当前指针的指向。如果新生成了一个对象修改了外部的对象的指针指向地址、里面继续修改也没用了。
关于block的循环引用
-
__block是否可以防止循环引用。
可以、只需要在block内部根据需要将变量置空。缺点是需要手动调用一次block
__block id tmp = self; self.block = ^{ tmp = nil; }; self.block();
需要注意的是。在tmp被block的捕获的时候、和__weak一样是不会增加tmp的引用计数的。之所以要手动释放、是因为在__block id tmp = self;时、tmp对self进行了一次强引用。需要消除的是这次引用。而且、当tmp被置nil之后、下次block调用将会得到一个空地址。但原对象不受此影响。
最后
本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果不吝赐教小弟更加感谢。