[toc] ## 1、block的基本概念及使用 `Block`是一种特殊的`数据结构`,**它可以保存一段代码**,等到需要的时候进行调用执行这段代码,常用于GCD、动画、排序及各类回调。 - Block变量的声明格式为: **返回值类型(^Block名字)(参数列表);** ``` //声明一个没有传参和返回值的block void(^myBlock1)(void) ; //声明一个有两个传参没有返回值的block 形参变量名称可以省略,只留有变量类型即可 void(^myBlock2)(NSString *name,int age); //声明一个没有传参但有返回值的block NSString *(^myBlock3)(); //声明一个既有返回值也有参数的block int(^myBlock4)(NSString *name); ``` - block的赋值: **Block变量 = ^(参数列表){函数体};** ``` //如果没有参数可以省略写(void) myBlock1 = ^{ NSLog(@"hello,word"); }; myBlock2 = ^(NSString *name,int age){ NSLog(@"%@的年龄是%d",name,age); }; //通常情况下都将返回值类型省略,因为编译器可以从存储代码块的变量中确定返回值的类型 myBlock3 = ^{ return @"小李"; }; myBlock4 = ^(NSString *name){ NSLog(@"根据查找%@的年龄是10岁",name); return 10; }; ``` - 当然也可以直接在声明的时候就赋值: **返回值类型(^Block名字)(参数列表) = ^(参数列表){函数体};** ``` int(^myBlock5)(NSString *address,NSString *name) = ^(NSString *address,NSString *name){ NSLog(@"根据查找家住%@的%@今年18岁了",address,name); return 18; }; ``` - blcok的调用:**Block名字();** ``` //没有返回值的话直接 Block名字();调用 myBlock1(); //有参数的话要传递相应的参数 myBlock2(@"校花",12); //有返回值的话要对返回值进行接收 NSString *name = myBlock3(); NSLog(@"%@",name); //既有参数又有返回值的话就需要即传参数又接收返回值 int age = myBlock5(@"河北村",@"大梨"); NSLog(@"%d",age); ``` 在实际使用Block的过程中,我们可能需要`重复`地声明多个相同返回值相同参数列表的Block变量(blcok内部执行的代码功能不一样),如果总是重复地编写一长串代码来声明变量会非常繁琐, 所以我们可以使用`typedef`来定义Block类型。 ``` #import "ViewController.h" //typedef 定义Block typedef void(^commentBlock)(NSString *name,int age); @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; commentBlock commentBlock1 = ^(NSString *name,int age){ //这里的操作是将age的name从数据库中筛选出来 }; commentBlock commentBlock2 = ^(NSString *name,int age){ //这里的操作是将age的name添加到数据库 }; commentBlock commentBlock3 = ^(NSString *name,int age){ //这里的操作是将age的name从数据库中删除 }; commentBlock1(@"ma",12); commentBlock2(@"dong",19); commentBlock3(@"mei",8); } @end ``` 这样可以减少重复代码,避免重复用**void(^commentBlock)(NSString *name,int age)**;声明block ## 2、block的底层结构 接下来我们来看一下block究竟是一个什么样的结构? 通过`clang`命令将oc代码转换成c++代码(如果遇到_weak的报错是因为_weak是个运行时函数,所以我们需要在clang命令中指定运行时系统版本才能编译): ``` xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp ``` ``` -(void)viewDidLoad{ [super viewDidLoad]; int i = 1; void(^block)(void) = ^{ NSLog(@"%d",i); }; block(); } ``` 转换成c++代码如下: ``` //block的真实结构体 struct __ViewController__viewDidLoad_block_impl_0 { struct __block_impl impl; struct __ViewController__viewDidLoad_block_desc_0* Desc; int i; //构造函数(相当于OC中的init方法 进行初始化操作) i(_i):将_i的值赋给i flags有默认值,可忽略 __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _i, int flags=0) : i(_i) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; //封存block代码的函数 static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) { int i = __cself->i; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_3g_7t9fzjm91xxgdq_ysxxghy_80000gn_T_ViewController_c252e7_mi_0,i); } //计算block需要多大的内存 static struct __ViewController__viewDidLoad_block_desc_0 { size_t reserved; size_t Block_size; } __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)}; //viewDidLoad方法 static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) { ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad")); //定义的局部变量i int i = 1; //定义的block底部实现 void(*block)(void) = &__ViewController__viewDidLoad_block_impl_0( __ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, i)); //block的调用 bloc->FuncPtr(block); } ``` 从中我们可以看出,定义的block实际上就是一直指向结构体`_ViewController_viewDidLoad_block_impl_0`的指针(将一个_ViewController_viewDidLoad_block_impl_0结构体的地址赋值给了block变量)。而这个结构体中,我们看到包含以下几个部分: **impl、Desc、引用的局部变量、构造方法。** 而从构造方法我们又可以看出impl中有以下几个成员:`isa、Flags、FuncPtr`,所以综合以上信息我们可以知道block内部有以下几个成员: ![](https://img.kancloud.cn/e5/ab/e5abb059845276dc74a135494db7a824_1946x882.png) 接下来,我们依依来看block底层结构中这些结构体或者参数的作用是什么? ![](https://img.kancloud.cn/33/e9/33e90bd6f07987d54e9cf1a5c1e09aca_1048x152.png) ### **首先Desc:** ``` static struct __ViewController__viewDidLoad_block_desc_0 { size_t reserved; size_t Block_size; } __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)}; ``` desc结构体中存储着两个参数,`reserved`和`Block_size`,并且reserved赋值为0而Block_size则存储着__ViewController__viewDidLoad_block_impl_0的占用空间大小。最终将desc结构体的地址传入__ViewController__viewDidLoad_block_impl_0中赋值给Desc。**所以Desc的作用是记录Block结构体的内存大小。** ### 接下来,我们来看,int i: i也就是我们定义的局部变量,因为在block块中使用到i局部变量,所以在block声明的时候这里才会将i作为参数传入,**也就说block会捕获i**。如果没有在block中使用age,这里将只会传入impl,Desc两个参数。这里需要注意的一点是,调用block结构体的构造函数时,是将我们定义的**局部变量i的值**传进去了的,也就是构造函数实现的时候i(_i) 这部分代码的作用就是将_i的值传给i。其实这也就解释清楚为什么我们在block中无法修改i的值了,**因为block用到的i根本和我们自己定义的i不是同一个**,block内部是自己单独创建了一个参数i,然后将我们定义的局部变量i的值赋给了自己创建的i。 ### 最后我们来看一下impl这个结构体: ``` struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; ``` 我们在block结构体的构造函数中也可以看出这几个成员分别有什么作用: - `isa`: 指针,存放结构体的内存地址 - `Flags`: 这个用不到 有默认值 - `FuncPtr`: block代码块地址 所以通过以上分析,我们可以得出以下几个结论: - 1、`block本质上也是一个OC对象`,它内部也有个isa指针,这一点我们也可以通过打印其父类是NSObject来证明; ![](https://img.kancloud.cn/41/22/41220ad687c27e56e523743a53e374b2_1332x336.png) - 2、block是封装了函数调用以及函数调用环境的OC对象.(所谓调用环境就是比如block用到了变量i就把它也封装进来了); - 3、FuncPtr则存储着viewDidLoad_block_func_0函数的地址,也就是block代码块的地址。所以当调用block的时候,bloc->FuncPtr(block);是直接调用的FuncPtr方法。 - 4、impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。 - 5、Desc存储__viewDidLoad_block_impl_0结构体所占用的内存大小,也就是`存储着Block的内存大小`。 我们 可以用一张图来表示各个结构体之间的关系: ![](https://img.kancloud.cn/94/03/94030dd60b41a1ef6a621ffd6b93dacd_2468x1382.png) 再简单化就是:(网络图片 比较清晰) ![](https://img.kancloud.cn/77/7b/777b8e75604fd3af318a1e993244fa08_700x412.png) block底层的数据结构也可以通过一张图来展示(variables就是结构体中所引用的变量,invoke就是上面的FuncPtr也就是block封装代码的函数地址,这个图是根据上面的分析可以总结出): >- isa指针,所有对象都有该指针,用于实现对象相关的功能。 >- flags,用于按bit位表示一些block的附加信息,block copy的实现代码可以看到对该变量的使用。 >- reserved,保留变量。 >- invoke,函数指针,指向具体的Block实现的函数调用地址。就是FuncPtr >- descriptor,表示该Block的附加描述信息,主要是size大小,以及copy和dispose函数的指针。 >- variables,截取过来的变量,Block能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。 > ![](https://img.kancloud.cn/30/53/30530c40b7916df92053d6e1047c4231_430x431.png) ## 3. block 变量捕获 我们定义了几个变量:全局变量name、局部变量-`auto变量`:i,obj、局部变量-`静态变量`:height,分别在blcok内部修改和访问↓↓ ![](https://img.kancloud.cn/22/0e/220e281fcd939a766a7a2ba9a0eb54b1_1732x734.png) 我们发现在block内部都可以访问这些变量,但是无法修改局部变量中的auto变量,无法修改的原因我们在上面的分析中也可以看出,是因为block内部自己创建了对应的变量,外部auto变量只是将值传递到block内赋给block创建的内部变量。block内部存在的只是自己创建的变量并不存在block外部的auto变量,所以没办法修改。 **但是为什么全局变量和静态变量就可以访问呢?**我们将oc代码转换成c++代码来看 ![](https://img.kancloud.cn/6a/3f/6a3f0391070e726f0bcf3d1d81b0fe2b_2252x866.png) 发现,block内部虽然访问了四个变量,但是其底层只捕获了三个变量,并没有捕获全局变量name。 而且比较局部变量中的auto变量和静态变量,发现block底层捕获auto变量时是捕获的其值,**而捕获静态变量时是捕获的变量地址**(i是值, *height是地址),这也就是为什么我们可以在block修改静态变量,因为block内修改的静态变量其实和block外的静态变量是同一个内存地址,同一个东西。 关于auto变量obj,block内部也是捕获它的值,不要因为它有*就觉得捕获的是地址,因为obj本身就是个对象,本身就是地址,如果block捕获的是obj的地址的话应该是NSObject **obj 即指向指针的地址。 所以我们通过上面这个例子可以总结出: ![](https://img.kancloud.cn/2d/57/2d57a9e708486df90a131cff06570c28_1546x282.png) **为什么会出现这种区别呢?** 首先,为什么捕获局部变量而不捕获全局变量这个问题很好理解: **全局变量整个项目都可以访问**,block调用的时候可以直接拿到访问,不用担心变量被释放的情况; 而局部变量则不同,局部变量是有**作用域**的,如果block调用的时候block已经被释放了,就会出现严重的问题,所以为了避免这个问题block需要捕获需要的局部变量。(比如我们局部变量和block都卸载了viewDidLoad方法,但是我在touchesBegan方法中调用block,这个时候局部变量早就释放了,所以block要捕获局部变量) **接下来,为什么auto变量是捕获的值,而静态变量是捕获的地址呢?** 这是因为自动变量和静态变量存储的区域不同,两者释放时间也不同。 >我们在关于局部变量、全局变量的分析中讲到了**自动变量是存放在栈中的**,创建与释放是由系统设置的,随时可能释放掉,而**静态变量是存储在全局存储区的**,生命周期和app是一样的,不会被销毁。所以对于随时销毁的自动变量肯定是把值拿进来保存了,如果保存自动变量的地址,那么等自动变量释放后我们根据地址去寻值肯定会发生怀内存访问的情况,而静态变量因为项目运行中永远不会被释放,所以保存它的地址值就完全可以了,等需要用的时候直接根据地址去寻值,肯定可以找到的。 那么,又有一个问题了,**为什么静态变量和全局变量同样不会被销毁,为什么一个被捕获地址一个则不会被捕获呢?** 我个人觉得是静态变量和全局变量因为两者访问方式不同造成的,我们都知道全局变量整个项目都可以拿来访问,所以某个全局变量在全局而言是唯一的(也就是全局变量不能出现同名的情况,即使类型不同也不行,否则系统不知道你具体访问的是哪一个)而静态变量则不是,全局存储区可能存储着若干个名为height的静态变量。 所以这就导致了访问方式的不同,比如说有个block,内部有一个静态变量和一个全局变量,那么在调用的时候系统可以直接根据全局变量名去**全局存储区**查找就可以找到,名称是惟一的,所以不用捕获任何信息即可访问。而静态变量而不行,全局存储区可能存储着若干个名为height的静态变量,所以blcok只能根据内存地址去区分调用自己需要的那个。 >我之前有个想法:是不是因为两者访问范围不同,全局变量可以全局访问,静态变量只能当前文件访问。但仔细想想block即使不是在当前文件调用的,但它的具体执行代码块内代码肯定是在当前文件执行的,也就是block内部访问变量不存在跨文件访问的情况,既然两者都可以访问到那么访问范围就不是原因了。 ``` -(void)viewDidLoad{ [super viewDidLoad]; void(^block)(void) = ^{ NSLog(@"%@",self); }; block(); void(^block2)(void) = ^{ NSLog(@"%@",self.address); }; block2(); } ``` 上面这段代码中的block是怎么捕获变量的呢? 我们转换成c++代码可以看出,我们可以看到viewdidload实际上是转换成了 void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) 也就是这个方法转换为底层实现时是有两个参数的:self和_cmd,既然self是方法的参数,那么self肯定是个局部变量,又因为这个局部变量并没有static修饰,所以self应该会被捕获并且是值传递 ![](https://img.kancloud.cn/58/d0/58d0d42a72ad19fdcbeb5c0851ff17a7_2224x1440.png) 在访问实例对象(self)的属性(address),我们发现block并没有捕获这个具体的属性而是捕获的实例对象(self),这是因为通过self就可以获取到这个实例对象的属性,捕获一个实例对象就够了,而在block内部使用这个属性的时候,也是通过实例对象来获取的↓↓ ![](https://img.kancloud.cn/22/e1/22e1f2b33a4a3d133947eb69de9bfa10_2208x252.png) ## 4. block的类型 我们在分析block底层结构的时候,看到了isa存储的是&_NSConcreteStackBlock地址,也就是这个block是个block类型的,那么block只有这一种类型吗? 答案是否定的,blcok有三种类型,我们可以通过代码来验证: ![](https://img.kancloud.cn/45/46/454672690a465f9edb411ba4cc18ffad_1498x562.png) 有的同学可能将oc转为c++↓↓发现oc代码编译后三个block都是StackBlock类型的,和我们刚才打印的不一样。这是因为runtime运行时过程中进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。 ![](https://img.kancloud.cn/12/c3/12c39fcd4095cec9cabf29930683c6fb_1620x1214.png) **既然存在三种不同的类型,那系统是根据什么来划分block的类型的呢?不同类型的block分别存储在哪呢?** ![](https://img.kancloud.cn/7d/3c/7d3c1a583d5cea0dd4d8e34b1d1bdb7d_691x137.png) 就是根据两点:有没有访问auto变量、有没有调用copy方法: ![](https://img.kancloud.cn/c5/d7/c5d74613677e2a13fcf0b264dc2a7519_1652x652.png) 而这三种变量存放在内存中的位置也不同:__NSMallocBlock__是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。 ![](https://img.kancloud.cn/ab/f7/abf7bb974355f8498377a9f872b650a3_700x533.png) 关于判断类型的两个条件,我们第一个条件也就是判断有误访问auto变量这个是明白的,但是第二个就不太清楚了,**调用copy有什么用?做了哪些操作了?** >__NSGlobalBlock __ 调用copy操作后,`什么也不做` >__NSStackBlock __ 调用copy操作后,复制效果是:`从栈复制到堆`;副本存储位置是堆 > __NSMallocBlock __ 调用copy操作后,复制效果是:`引用计数增加`;副本存储位置是堆 也就是: ![](https://img.kancloud.cn/83/a5/83a5f92c92de02a1e04f8fcefa6a1dd7_1168x216.png) 由于ARC环境下,系统会对一下情况下的block自动做copy处理: ``` //1.block作为函数返回值时 typedef void (^Block)(void); Block myblock() { int a = 10; //此时block类型应为__NSStackBlock__ Block block = ^{ NSLog(@"---------%d", a); }; return block; } int main(int argc, const char * argv[]) { @autoreleasepool { Block block = myblock(); block(); // 打印block类型为 __NSMallocBlock__ NSLog(@"%@",[block class]); } return 0; } //2.将block赋值给__strong指针时,比如(arc中默认所有对象都是强指针指引) void (^block1)(void) = ^{ NSLog(@"Hello"); }; //3.block作为Cocoa API中方法名含有usingBlock的方法参数时。例如:遍历数组的block方法,将block作为参数的时候。 NSArray *array = @[]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }]; //4.block作为GCD API的方法参数时 例如:GDC的一次性函数或延迟执行的函数,执行完block操作之后系统才会对block进行release操作。 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ }); ``` 所以我们关闭ARC才能更好的看清楚copy的作用: >project -> Build settings -> Apple LLVM complier 3.0 - Language -> objective-C Automatic Reference Counting设置为NO 然后我们定义几个不同类型的block,并分别调用copy方法查看结果: ![](https://img.kancloud.cn/34/04/3404779b242a3a313c132d835ecd1310_1900x1078.png) 发现block的copy方法确实是有这样的作用,需要说明的一点,block3虽然多次copy后打印出来的retainCount始终是1,但其内存管理器中仍然会增加。 **既然copy方法最大的一个作用是把block从栈拷贝到堆,它这样做的原因是什么?block在栈中和堆中有什么区别吗?** 我们都知道栈中对象的内存空间是随时可能被系统释放掉的,而堆中的内存空间是由开发者维护的,如果block存在于栈中就可以出现一个问题,当我们调用block的时候,他被释放掉了,从而出现错误: ![](https://img.kancloud.cn/39/31/39311ca09e12c21efcc143ab0b20b764_1146x734.png) 我们发现,当我们调用block的时候打印出来莫名其妙的东西,这是因为test方法执行完后,栈内存中block所占用的内存已经被系统回收,因此就有可能出现乱得数据。 可能有的同学会根据上面block访问auto变量的想法来思考,blcok不是已经捕获了这个变量了么,其实这完全是两码事,block确实是把变量a捕获到了自己内部,但是现在它自己的空间都被释放掉了,更不用说它捕获的变量了,肯定被释放掉了。 所以,这种情况下,是需要将block移到堆上面的,让开发者控制它的生命周期,这就用到了copy(arc环境下不用,因为test方法中将block赋值给了一个__strong指针,会生成copy) ![](https://img.kancloud.cn/5a/75/5a7552a3c41d4a4de443f4f549dd5800_1242x830.png) 既然blcok存放在堆中了,block内部有捕获了a的值,所以就可以正常输出了。 我们从这种情况也可以知道为什么不同环境下block的声明属性写法的不同: MRC下block属性的建议写法: ``` @property (copy, nonatomic) void (^block)(void); ``` ARC下block属性的建议写法: ``` @property (strong, nonatomic) void (^block)(void); @property (copy, nonatomic) void (^block)(void); ``` copy属性意味着,系统会自动对修饰的block进行一次copy操作, 所以在mrc环境下,copy属性修饰block就不会出现上面block存在栈里,在访问时被释放的情况; 而在arc环境下,系统会在block被__strong指针引用时自动执行copy方法,所以就可以写strong和copy两种 ## 5. __block 既然block内部可以修改静态变量和全局变量的值,而无法修改自动变量的值,那么有没有什么方式可以解决这个问题呢? 答案是肯定的,我们可以通过_ _block修饰这个自动变量,从而可以在block内部访问并修改这个自动变量了: __block不能修饰全局变量、静态变量(static) ![](https://img.kancloud.cn/26/07/26073528fdbe2c9f36996aa69b0972c3_942x692.png) **那么,_ _block 这个修饰符做了什么操作呢?**就让可以让block内部可以访问自动变量 我们通过底层代码可以看出,__weak将int类型的数据转换成了一个`__Block_byref_i_0`的结构体类型 ![](https://img.kancloud.cn/d5/59/d559df656ce54254148228937366835f_2248x478.png) 而这个结构体的结构是: ``` struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int i; }; ``` 而从赋值上看,isa为0,既然有isa指针,那么说明这个结构体也是一个对象,__forwarding存储的是__Block_byref_i_0的地址值,flags为0,size为Block_byref_i_0的内存大小,i是真正存储变量值的地方,其内部结构就是这样的↓↓(关于des结构体为什么会多了一个copy函数和一个dispose函数在下面相互引用中会讲到) ![](https://img.kancloud.cn/ce/76/ce76602d3c7e6a60439c1159134dbe99_1798x926.png) 而当我们在block内部修改和访问这个变量是,底层实现是这样的: ![](https://img.kancloud.cn/38/53/3853225bf0224ef9df7678cec498c2a8_1866x484.png) 是通过`__Block_byref_i_0`结构体的指针__forwarding读取和修改的变量i. 为什么要通过__forwarding转一下呢,而不是直接读取i 这是因为当我们调用block的时候,block可能存在于栈中可能存在于堆中, ``` void (^block)(void); void test() { // __NSStackBlock__ int a = 10; block = ^{ NSLog(@"block---------%d", a); } ; //情况一:此时调用block还存在与栈中 block(); //此时block有两份 一个在栈中 一个在堆中   [blcok copy]; //一次copy对应一次release [block release]; //这个方法执行完后虽然栈中的block释放了 但是已经拷贝到堆里一份,所以还是可以继续调用的 } int main(int argc, const char * argv[]) { @autoreleasepool { test(); //情况二:test方法执行完后 栈中的block被释放了 堆中还有一个copy的block block(); } return 0; } ``` __forwarding指向示意图: ![](https://img.kancloud.cn/5d/70/5d703d2c329410c7441fe5ccd5c19100_659x505.png) 如果是直接通过结构体的内存地址访问变量,因为结构体在堆中的地址和在栈中的地址肯定不一样,情况一和情况二很明显又是执行的同一个方法,所以就没有办法实现这个功能,也就是如果方法里是根据栈中的地址访问属性的,那么情况二就会出错,因为这个时候这个地址已经被释放了,如果是根据堆中的值去访问变量的话,那么情况一又有问题了,因为这个时候堆里还没有这个block呢。所以需要根据__forwarding指针去访问变量,这样的话才能确保情况一和情况二都会访问到这个结构体。 所以我们总结一下上面的分析: - 1.__block将int i进行包装,包装成一个__Block_byref_i_0结构体对象,结构体中的i是存储i的int值的; - 2.当我们在block内修改或访问该对象时,是通过该对象的__forwarding去找对应的结构体再找对应的属性值,这是因为__forwarding在不同情况下指向不同的地址,防止只根据单一的一个内存地址出现变量提前释放无法访问的情况。 那么我们就明白为什么可以修改__block修饰的自动变量了,因为__block修饰变量变成了一个对象,我们修改只是修改的这个对象中的一个属性,并不是修改的这个对象:就像这样↓↓ ![](https://img.kancloud.cn/94/45/944568abc0cfb51067598733a6fe7457_1270x894.png) __block修饰下的i不再是int类型而变成一个对象(对象p),我们block内部访问修改和访问的是这个对象内部的int i(字符串p),所以是可以修改访问的。只不过这个转化为对象的内部过程封装起来不让开发者看到,所以就给人的感觉是可以修改auto变量也就是修改时是int i。 block外部访问__block修饰的变量也是通过__forwarding指针找到结构体对象内部的int i,既然是访问你的block内部的属性i,那么就是修改后的21了↓↓ ![](https://img.kancloud.cn/bd/d9/bdd95e832008a0b84d31a2f8f71093af_2224x482.png)