Block是C语言的扩充功能。一言以蔽之,带有局部变量的匿名函数,简单说它就是一个函数指针。
一、语法:
格式:Block变量 = Block值
具体形式如下:
声明并赋值:返回值类型(^变量名)(参数列表) = ^返回值类型 参数列表 表达式;
调用:变量名(参数列表);
使用实例1:
// 声明Block变量,并赋值
void(^name)(int num) = ^void(int num){ NSLog(@"%d", ++num); };
// 使用Block变量
name(6);
使用实例2:
// 定义Block类型
typedef void(^BlockType)(int num);
// 用定义的类型声明变量,并赋值
BlockType name = ^void(int num){ NSLog(@"%d", ++num); };
// 调用
name(8);
二、Block的种类
2.1、NSGlobalBlock
a.全局区域创建的Block,永远是 NSGlobalBlock
b.局部区域创建的Block,有指针变量引用,不访问局部变量/成员变量,是 NSGlobalBlock
d.局部区域创建的Block,没有指针变量引用,不访问局部变量/成员变量,是 NSGlobalBlock
2.2、NSMallocBlock
c.局部区域创建的Block,有指针变量引用,访问局部变量/成员变量,是 NSMallocBlock
2.3、NSStackBlock
e.局部区域创建的Block,没有指针变量引用,访问局部变量/成员变量,是 NSStackBlock
对于d\e两种情况的一般用法,如下:
// 1> 作为方法参数,如:[self methodBlock:^{ NSLog(@">>>"); }];
// 2> 创建后直接调用(一般不这么用),如:^{ NSLog(@">>>"); }();
三、实质:
获取源码的方法:
1> 创建Command Line Tool工程。
2> 添加Block相关代码。
3> 打开终端并CD到main.m文件所在的文件夹。
4> 执行命令:clang -rewrite-objc main.m,该文件夹下会多出main.cpp文件,该文件的末尾就是相关源码。
以下是5种比较典型的使用情况的源码:
情况1:局部Block不访问Block外部的变量
// OC代码
// main函数中,声明局部Block变量,并赋值,Block的实现只是打印一个字符串,然后调用该Block。
// clang编译后的相关代码
// 该结构体和Block相关,我们只关注两个属性:isa && FuncPtr
// isa:表示Block的类型
// FuncPtr:表示Block的实现(其实是个函数指针)
// 该结构体有两个属性和一个构造函数
// impl:和Block相关的结构体,其结构体指针和__main_block_impl_0结构体指针相同(因为它是第一个成员变量,固它们的首地址相同)
// Desc:是个结构体,对结构体__main_block_impl_0的描述,即开辟内存空间时的依据
// __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0):构造函数,该函数需要两个参数,分别是函数指针和描述性的结构体指针
// 静态函数,其实就是Block的对应实现
// 结构体实例,是__main_block_impl_0结构体的描述
// 主函数,函数内部
// 1、声明了结构体指针变量并赋值
// 2、根据该指针找到对应的函数,调用了该函数
// 大概流程就是:
1、从 main 函数开始
1.1、声明结构体指针block,然后调用__main_block_impl_0结构体的构造函数,传入参数__main_block_func_0函数指针(函数首地址,函数名即函数首地址)和__main_block_desc_0_DATA指针,得到__main_block_impl_0结构体,并将得到的结构体的地址赋值给block。
1.2、block就是__main_block_impl_0结构体的首地址,也是__block_impl结构体的首地址,所以根据block->FuncPtr,可以拿到对应函数指针,然后调用该函数,该函数打印一个字符串。
情况2:局部Block读取其外部的局部变量
// OC代码
// main函数中,声明局部变量 num 并赋值,声明局部Block变量,并赋值,Block的实现打印一个字符串和 num 值,然后调用该Block。
// clang编译后的相关代码
// 和情况1一样
// 比情况1多了一个 num 变量,构造函数也多了一个对应该变量的参数
// 该函数内部声明一个局部变量,并根据__cself获取对应的值赋给该变量,然后打印相应的东西
// 注意:根据OC对象的定义,对象其实就是结构体指针,所以这里__cself是一个Block对象
// 和情况1一样
// 主函数,函数内部
// 1、声明变量 num 并赋值 666
// 2、声明了结构体指针变量并赋值
// 3、根据该指针找到对应的函数,调用了该函数
// 大概流程就是:
1、从 main 函数开始
1.1、声明变量 num 并赋值 666
1.2、声明结构体指针block,然后调用__main_block_impl_0结构体的构造函数,传入参数__main_block_func_0函数指针(函数首地址,函数名即函数首地址)和__main_block_desc_0_DATA指针和num,得到__main_block_impl_0结构体,并将得到的结构体的地址赋值给block。
1.3、block就是__main_block_impl_0结构体的首地址,也是__block_impl结构体的首地址,所以根据block->FuncPtr,可以拿到对应函数指针,然后调用该函数,该函数打印一个字符串。
情况3:局部Block修改其外部的局部变量
// OC代码
//main函数中,声明__block修饰的局部变量 num 并赋值,声明局部Block变量,并赋值,Block的实现打印一个字符串和 ++num 值,然后调用该Block。
// clang编译后的相关代码
// 和情况1一样
// num 变量转变的结构体
// 对比情况2,这里把int类型的num变量,换成了__Block_byref_num_0类型的结构体指针(即对象),这里__block修饰后的num变量被编译成了对象
// __cself对象中获取num对象,又从num对象中获取__forwarding对象,然后从__forwarding对象中获取num变量,执行++num之后打印输出最后的值
// __forwarding就是一个指向自身结构体的首地址的结构体指针,这样做的目的就是在把变量从栈区复制到堆区的时候,不论变量在哪一个存储区域,都可以正确访问其对应的变量值
// 复制对象(引用计数+1)
// 处理对象(引用计数-1)
// 结构体实例,是__main_block_impl_0结构体的描述,对比情况2,成员变量中多了两个函数指针,这两个函数指针就是结构体指针(即对象)的内存管理函数,系统会在适当的时候自动调用它们
// 主函数,函数内部
// 1、声明结构体变量 num 并赋了相关的值
// 2、声明了结构体指针变量block并赋值,这里调用构造函数创建该结构体的时候,第三个参数是num的结构体指针,即num对象
// 3、根据该指针找到对应的函数,调用了该函数
// 大概流程就是:
1、从 main 函数开始
1.1、声明结构体变量 num 并赋了相关的值
1.2、声明结构体指针block,然后调用__main_block_impl_0结构体的构造函数,传入参数__main_block_func_0函数指针(函数首地址,函数名即函数首地址)和__main_block_desc_0_DATA指针和num对象,得到__main_block_impl_0结构体,并将得到的结构体的地址赋值给block。
1.3、block就是__main_block_impl_0结构体的首地址,也是__block_impl结构体的首地址,所以根据block->FuncPtr,可以拿到对应函数指针,然后调用该函数,该函数打印相关值。
情况4:局部Block访问(读取&修改)全局变量
// OC代码
// 声明全局int类型的变量num,并赋值6
// main函数中,声明局部Block变量,并赋值,Block的实现打印一个字符串和 num 值,然后调用该Block
// clang编译后的相关代码
// 和情况1一样
// 声明全局变量num并赋初始值6。
// 和情况1一样
// 和情况1一样,只是多了num变量的打印,num值来自全局变量
// 和情况1一样
// 和情况1一样
// 大概流程就是:
1、从 main 函数开始
1.1、声明结构体指针block,然后调用__main_block_impl_0结构体的构造函数,传入参数__main_block_func_0函数指针(函数首地址,函数名即函数首地址)和__main_block_desc_0_DATA指针,得到__main_block_impl_0结构体,并将得到的结构体的地址赋值给block。
1.2、block就是__main_block_impl_0结构体的首地址,也是__block_impl结构体的首地址,所以根据block->FuncPtr,可以拿到对应函数指针,然后调用该函数,该函数打印一个字符串。
情况5:全局Block访问(读取&修改)全局变量
// OC代码
//全局Block读取全局变量
//全局Block修改全局变量
// main函数调用Block
// clang编译后的相关代码
// 和情况4一样
// 和情况4一样
// 和情况4一样
// 和情况4一样
// 和情况4一样
// 调用对应的函数构造__blk1_block_impl_0静态结构体
// 把上面的结构体指针(对象)赋值给blk1变量
// main函数,调用全局blk1
// 大概流程就是:
1、从 main 函数开始
1.1、直接调用代表Block实例的全局的结构体指针
综上可知:
1、Block其实就是OC所谓的实例,每一个Block,都会被编译为对应的结构体指针,通过这个结构体指针,可以找到对应的函数实现以及Block内部访问的外部的局部变量或成员变量。
2、Block有3种类型:_NSConcreteGlobalBlock(全局)、_NSConcreteStackBlock(栈)、__NSConcreteMallocBlock(堆)
3、访问全局变量或者不访问任何外部变量的Block变量(注意是变量,不是实现),永远都是全局Block
4、参数形式出现的栈Block,如果在方法内部被一个新Block指针变量引用,那么该栈Block被会复制一份到堆区,新Block指针指向这个堆Block
5、__block修饰后的变量,该变量会被编译为一个结构体,且该结构体包含这个变量,并以结构体指针(对象)的形式被访问,因此当一个变量没有被__block修饰时,Block内部捕获的是该变量的值,被__block修饰时,Block内部捕获的是包含该变量的对象
四、总结
归根结底,Block就是一个包含函数实现和所使用的变量(如果需要的话)的结构体指针,即OC对象,所以Block可以被当做全局变量、局部变量、成员变量、参数、返回值,来回传递使用,更重要的是,它能携带函数的实现(函数指针),这意味着它能封装一段代码块,被来回传递,在适当的时候调用执行,执行的同时也可以修改它携带的变量值,起到异地异步传值的的目的。
因为Block是个OC对象,当它本身作为一个对象的成员变量时,而其内部又引用这个对象或者这个对象的成员变量和方法的时候(或者多个对象依次引用构成了循环链),就会导致循环引用,从而导致内存问题。这种情况,一般用 __weak 弱化 self 指针就可以了。这里不细说各种循环引用的问题。