前言:Block不允许修改外部变量的值,Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。
block 有三种类型:
首先要知道block的三种类型:
-
_NSConcreteGlobalBlock:全局的静态 block,类似函数。如果block里不获取任何外部变量。或者的变量是全局作用域时,如成员变量、属性等; 这个时候就是Global类型。
-
_NSConcreteStackBlock:保存在栈中的 block,栈都是由系统管理内存,当函数返回时会被销毁。__block类型的变量也同样被销毁。为了不被销毁,block会将block和__block变量从栈拷贝到堆。
-
_NSConcreteMallocBlock:保存在堆中的 block,堆内存可以由开发人员来控制。当引用计数为 0 时会被销毁。
具体根据代码分析中他们属于那种类型。
Block与变量
1、全局变量,block可以进行读取和修改。
@interface ViewController () {
NSInteger num;
}
@implementation ViewController
- (void)viewDidLoad {
//1、block修改成员变量
void (^block1)() = ^(){
++num;
NSLog(@"调用成员变量: %ld", num);
};
block1();
}
2、局部变量,block只能读取,不能修改局部变量。这个时候是值传递,如果想修改局部变量,要用__block来修饰,这个时候是引用传递。下面会聊下block的实现原理:
看例子:
//1、调用局部变量,不用__block
NSInteger testNum = 10;
void ( ^ blockNum )() = ^() {
//testNum = 1000; 这样是编译不通过的
NSLog(@"修改局部变量: %ld", testNum); //打印:10
};
testNum = 20;
blockNum();
NSLog(@"testNum最后的值: %ld", 20); //打印:30
//2、修改局部变量,要用__block
__block NSInteger testNum2 = 10;
void (^blockNum2)() = ^() {
NSLog(@"读取局部变量: %ld", testNum3); //打印:20
testNum2 = 1000;
NSLog(@"修改局部变量: %ld", testNum3); //打印:1000
};
testNum2 = 20;
blockNum2();
testNum2 = 30;
NSLog(@"testNum最后的值: %ld", testNum2); //打印:30
block代码分析
网上很多通过Clang(LLVM编译器)将OC的代码转换成C++源码,来进行分析的。但是这些转换的代码并不是block的源代码,只是用来理解用的过程代码。
1、block不包含任何变量
新建一个testBlock.m文件。文件中代码为:

执行clang命令:
clang -rewrite-objc testBlock.m
生成.cpp的核心代码主要在.cpp文件的底部,大家可以看下图:

详细的注释,具体的看图片备注。代码执行的时候,block的 isa 有上面三个类型。
从图二中可以发现,block的类型问题:
impl.isa = &_NSConcreteStackBlock;
这里 impl.isa
的类型为_NSConcreteStackBlock
,由于clang
改写的具体实现方式和 LLVM 不太一样,所以这里 isa 指向的还是_NSConcreteStackBlock
。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock
类型,所以 block是什么类型 在 clang代码里是看不出来的。如果要查看block的类型还是要通过Xcode进行打印:

打印结果:
myBlock调用前======<__NSGlobalBlock__: 0x1000010b0>
myBlock 调用后=====<__NSGlobalBlock__: 0x1000010b0>
这个block没有获取任何外部变量,打印出来的是_NSConcreteGlobalBlock
,该类型的block和函数一样,存放在代码段内存段,内存管理简单。
注意:
在ARC环境下,平时见到最多的是_NSConcreteMallocBlock
,是因为我们会对Block有赋值操作,所以ARC下,block 类型通过 = 进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal
方法链。并导致 __NSStackBlock
类型的 block 转换为 __NSMallocBlock
类型。

打印的结果:
//打印block函数
myBlock : <__NSMallocBlock__: 0x100607780>
上面block代码,没有获取任何外部变量,但是通过 = 进行传递,所以是_NSConcreteMallocBlock
类型的。
测试了几次都不能打印出来__NSStackBlock
,看了个别人的写法,然后发现ARC下Block也是存在__NSStackBlock
的:
__block int temp = 10;
NSLog(@"%@",^{NSLog(@"*******%d %p",temp ++,&temp);});
打印结果:
<__NSStackBlock__: 0x7ffeefbff528>
这种情况就是ARC环境下Block是__NSStackBlock的类型。
2、block调用外部变量
-
如果block里面用到外部局部变量,会先把外部局部变量从栈区到堆区。因此可以访问外部局部变量的值,但是无法直接改变外部局部变量的值。
image.png
打印结果:
1==0x7ffee2642a9c
3==0x7ffee2642a9c
xBlock === <__NSMallocBlock__: 0x60c00004f330>
2==0x60c00004f350
bb === 0x7ffee2642a1c
从上面打印结果可以看出,在引用了外部局部变量后,block把变量aa
从栈区拷贝到堆区。
- 下面在新建一个例子,然后继续使用clang命令查看一下底层实现流程:
#import <Foundation/Foundation.h>
int global_variable = 1;
static int static_global_variable = 2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int static_local_variable = 3;
int val = 4;
void (^myBlock)(void) = ^{
global_variable ++;
static_global_variable ++;
static_local_variable ++;
printf("Block中 :global_variable = %d",global_variable);
printf("Block中 :static_global_variable = %d",static_global_variable);
printf("Block中 :static_local_variable = %d",static_local_variable);
printf("Block中 :val = %d",val);
};
global_variable ++;
static_global_variable ++;
static_local_variable ++;
val ++;
printf("Block中 :global_variable = %d",global_variable);
printf("Block中 :static_global_variable = %d",static_global_variable);
printf("Block中 :static_local_variable = %d",static_local_variable);
printf("Block中 :val = %d",val);
myBlock();
}
return 0;
}
clang编译后:

因为全局变量global_variable
和静态全局变量static_global_variable
,是全局的,作用域很广,它们可以被Block捕获进去,在Block捕获了它们进去之后,直接进行++操作,而且Block结束之后,它们的值依旧可以得以保存下来。
接下来仔细看看自动变量val
和静态变量static_local_variable
的问题:

在__main_block_impl_0
中,可以看到静态变量static_local_variable
和自动变量val
,被Block从外面捕获进来,成为__main_block_impl_0
这个结构体的成员变量了。这个构造函数中,自动变量和静态变量被捕获为成员变量追加到了构造函数中。在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。
这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量等等,这些变量在Block里面并不会被使用到,那么这些变量就不会被Block捕获进来,用不到的值,它不会去捕获。
再研究一下源码,我们注意到__main_block_func_0这个函数的实现
int *static_local_variable = __cself->static_local_variable; // bound by copy
int val = __cself->val; // bound by copy
系统自动加上的注释,bound by copy
自动变量val
虽然被捕获进来了,但是是用 __cself -> val
来访问的。Block仅仅捕获了val的值,并没有捕获val的内存地址。所以在__main_block_func_0
这个函数中即使我们重写这个自动变量val
的值,依旧没法去改变Block外面自动变量val的值。
OC基于这一点,在编译的层面就防止开发者可能犯的错误,因为自动变量没法在Block中改变外部变量的值,所以编译过程中就报编译错误。
小结一下:
自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量时,可以改变值的是静态全局变量、全局变量。
3、在Block中改变变量值有2种方式,一是传递内存地址指针到Block中,二是改变存储区方式(__block)。
- 先来实验一下第一种方式,传递内存地址到Block中,改变变量的值。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray * muArray = [NSMutableArray array];
[muArray addObject:@1];
NSLog(@"block前 muArray 指针地址 %x--- 指针指向的内存地址: %p",&muArray,muArray);
void(^myBlock)(void) = ^{
[muArray addObject:@23232];
NSLog(@"block内 muArray 指针地址 %x--- 指针指向的内存地址: %p",&muArray, muArray);
};
myBlock();
NSLog(@"block外 muArray 指针地址 %x--- 指针指向的内存地址: %p",&muArray, muArray);
}
return 0;
}
运行后打印结果:
block 前 muArray 指针地址 efbff568--- 指针指向的内存地址: 0x10050c280
block 内 muArray 指针地址 506480 --- 指针指向的内存地址: 0x10050c280
block 外 muArray 指针地址 efbff568--- 指针指向的内存地址: 0x10050c280
从上面结果可以看出:muArray
在block内的指针发生了变化,但是指向的内存地址都是一样的。
在使用clang命令后:(我直接把编译后的全部源码贴出来了,有点儿长,你们可以写个工程编译后,对着看,这样更好理解)
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *muArray;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_muArray, int flags=0) : muArray(_muArray) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSMutableArray *muArray = __cself->muArray; // bound by copy
((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)muArray, sel_registerName("addObject:"), (id _Nonnull)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 23232));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q8_8t5zqb1x3yg3dzb7h7trddj80000gn_T_main_dbee92_mi_1,&muArray, muArray);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->muArray, (void*)src->muArray, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->muArray, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSMutableArray * muArray = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)muArray, sel_registerName("addObject:"), (id _Nonnull)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q8_8t5zqb1x3yg3dzb7h7trddj80000gn_T_main_dbee92_mi_0,&muArray,muArray);
void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, muArray, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q8_8t5zqb1x3yg3dzb7h7trddj80000gn_T_main_dbee92_mi_2,&muArray, muArray);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
在__main_block_func_0
里面可以看到NSMutableArray *muArray = __cself->muArray
传递的是指针,所以成功改变了变量的值。
因为在C语言的结构体中,编译器没法很好的进行初始化和销毁操作。这样对内存管理来说是很不方便的。所以就在__main_block_desc_0
结构体中间增加成员变量:
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*)
和
void (*dispose)(struct __main_block_impl_0*)
利用OC的Runtime进行内存管理,相应的也就增加了2个方法,这里的_Block_object_assign和_Block_object_dispose就对应着retain和release方法。。
#对__Block_byref_num_0结构体进行内存管理。新加了copy和dispose函数。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->muArray, (void*)src->muArray, 3/*BLOCK_FIELD_IS_OBJECT*/);}
# 当 block 从堆内存释放时,调用此函数:__main_block_dispose_0
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->muArray, 3/*BLOCK_FIELD_IS_OBJECT*/);}
#BLOCK_FIELD_IS_OBJECT 是Block截获对象时候的特殊标示,
#如果是截获的__block,那么是BLOCK_FIELD_IS_BYREF。
Block_copy实现了把Block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。《Block_copy底层实现》
- 改变外部变量值的第二种方式是加 __block
用__block
修饰外部变量num
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int num = 20;
NSLog(@"block 前 num 指针地址 %x--- 指针指向的内存地址: %p",&num,num);
void(^myBlock)(void) = ^{
num = 100;
NSLog(@"block 中 num 指针地址 %x--- 指针指向的内存地址: %p",&num,num);
};
myBlock();
NSLog(@"block 后 num 指针地址 %x--- 指针指向的内存地址: %p",&num,num);
}
return 0;
}
从上面clang编译后我们能发现,带有 __block的变量num
也被转化成了一个结构体__Block_byref_num_0
这个结构体有5个成员变量。
struct __Block_byref_num_0 {
void *__isa; //第一个是isa指针
__Block_byref_num_0 *__forwarding; //第二个是指向自身类型的__forwarding指针
int __flags; //第三个是一个标记flag
int __size; //第四个是它的大小
int num; //第五个是变量值,名字和变量名同名
};
在__Block_byref_num_0
结构体中有一个指向自身类型的__forwarding
指针,这个__forwarding
到底是怎么指向自身的呢?运行上面的代码看看num
的地址:
打印结果:
block 前 num 指针地址 efbff568 --- 指针指向的内存地址: 0x14
block 中 num 指针地址 31013b8 --- 指针指向的内存地址: 0x64
block 后 num 指针地址 31013b8 --- 指针指向的内存地址: 0x64
明显看出num
地址不同了,这个__forwarding
指针并没有指向之前的自己啊,那__forwarding
指针现在指向到哪里了呢?
在前文的分析中,已经说过如果block里面用到外部局部变量,Block通过copy到了堆上,堆上也会重新复制一份Block,并且该Block也会继续持有该 __block 。当Block释放的时候,__block没有被任何对象引用,也会被释放销毁。
__forwarding
指针这里的作用就是针对堆上的Block,这句话怎么来解释呢? 原来是把__forwarding
指针指向自己的,换成了指向拷贝成_NSConcreteMallocBlock
的__block自己。然后堆上的变量的__forwarding
再指向原来的自己。这样不管__block怎么复制到堆上,还是在栈上,这样就可以通过 (num -> __forwarding -> num) = 100
来访问到变量值。如下图:

ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也就变成__NSMallocBlock。ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上,如
NSLog(@"%@",^{NSLog(@"*******%d %p",temp ++,&temp);})
MRC环境下,只有copy,__block才会被复制到堆上,否则,__block一直都在栈上,block也只是__NSStackBlock,这个时候__forwarding指针就只指向自己了。__block根本不会对指针所指向的对象执行copy操作,而只是把指针进行的复制。而在ARC环境下,对于声明为__block的外部对象,在block内部会进行retain,以至于在block环境内能安全的引用外部对象。
对象的变量
对象的变量在OC中,默认声明自带__strong所有权修饰符的,当block被赋值给__strong
类型的对象,或者block的成员变量时,编译器会自动调用block的copy方法。执行copy方法,block拷贝到堆上。
网友评论