Block简介

2018-03-14  本文已影响0人  kReader

      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 指针就可以了。这里不细说各种循环引用的问题。

上一篇下一篇

猜你喜欢

热点阅读