iOS开发——Blocks

什么是Blocks

Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。

按理说,C语言的标准不允许存在这样的函数。匿名函数就是不带函数名称的函数,那么带有自动变量是什么意思呢?Blocks提供了类似由C++和OC类生成实例或对象来保持变量值的方法。如“带有自动变量值”,Blocks保持自动变量的值。同时Blocks也被称作闭包、lambda计算。OC的Block在其他程序语言中的名称如图所示:
在这里插入图片描述

Blocks模式

Block语法

Block语法与一般的C语言函数定义不同的是:

  1. 没有函数名。
  2. 带有“^”。

第一点不同是没有函数名,因为它是匿名函数。第二点不同是返回值类型前带有“^”(插入记号,caret)记号。
BN范式,如图所示:
在这里插入图片描述

“返回值类型”同C语言函数的返回值类型,“参数列表”同C语言函数的参数列表,“表达式”同C语言函数中允许使用的表达式。当然与C语言函数一样,表达式中含有return语句时,其类型必须与返回值类型相同。
例如可以写出如下形式的Block语法:

^int (int count) {return count + 1;}

虽然前面出现过省略形式,但Block语法可省略好几个项目。首先是返回值类型。如图所示:
在这里插入图片描述

关于返回值类型,如果没有返回值则使用void类型,如果有返回值,无论return语句有多少个,所有return的返回值类型必须相同。

如果不使用参数,参数列表也可忽略:

^void (void){printf("Blocks\n");}

返回值类型以及参数列表均被忽略的Block语法如下图所示:
在这里插入图片描述

Block类型变量

在Block语法中,可将Block语法赋值给声明为Block类型的变量中。即源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”。Blocks中由Bloc语法生成的值也被称为“Block”。

Block类型变量仅仅是将声明函数指针类型变量的“*”变成了“^”。该Block类型变量与一般的C语言变量完全相同,可作为以下用途使用:

  1. 自动变量
  2. 函数参数
  3. 静态变量
  4. 静态全局变量
  5. 全局变量

下面我们就试着使用Block语法将Block赋值为Block类型变量:

int (^blk)(int) = ^(int count){return count + 1;};

用“^”开始的Block语法生成的Block被赋值给变量blk中。因为与通常的变量相同,所以当然也可由Block类型变量向Block类型变量赋值。

int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;

在函数返回值指定Block类型,可以将Block作为函数的返回值返回。

int (^func()(int))
{
	return ^(int count){return count + 1;};

}

由此可知,在函数参数和返回值中使用Block类型变量时,记述方式极为复杂。这时,我们可以像使用函数指针类型时那样,使用typedef来解决该问题。

typedef int (^blk_t)(int);

截获自动变量值

“带有自动变量值的匿名函数”中的“带有自动变量值 ”在Blocks中表现为“截获自动变量值”。截获自动变量值的实例如下:

int main(int argc, const char * argv[]) {
    int dmy = 256;
    int val = 10;
    void (^blk)(void) = ^{
        printf("val = %d\ndmy = %d\n",val,dmy);
    };
    val = 2;
    dmy = 6;
    blk();
    return 0;


}

该源代码中,Block语法的表达式使用的是它之前声明的自动变量fmt和val。所以它的输出是2和256,也就是说Blocks中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。

__block 说明符

实际上,自动变量值截获只能保存执行Block语法瞬间的值。保存后就不能改写该值。

另外关于C语言中的存储域类有以下几种:

  1. typedef
  2. extern
  3. static
  4. auto
  5. register

_block说明符类似于static、auto和register说明符,它们用于指定将变量值设置到哪个存储域中。例如,auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。

若想在Blcok语法的表达式中将值赋值给在Block语法外声明的自动变量,需要在该自动变量上附加__blcok说明符。 该源代码中,如果给自动变量声明int val附加__block说明符,就能实现在Block内赋值。例如:

__block int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n", val);

该源代码运行结果为:
val = 1
使用附有__block说明符的自动变量可在Block中赋值,改变量称为__block变量。

Blocks的实现

Block的实质

Block的实质就是通过编译器,将Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为普通的C语言源代码被编译。
下面,我们转换Block语法:

int main() {
	void (^blk)(void) = ^{printf("Block\n");};
	blk();
	return 0;
}

此源代码的Block语法最为简单,它忽略了返回值类型以及参数列表。该源代码通过clang可变换为以下形式:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself {
	printf("Block\n");
}

static struct __main_block_desc_0 {
  unsigned long reserved;
  unsigned long Block_size;
} __main_block_desc_0_DATA = { 
	0, 
	sizeof(struct __main_block_impl_0)
};

int main() {
	void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
	
	((void (*)(struct__block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct__block_impl *)blk);
    return 0;
}

我们将转化后的C++源码分成几个部分来看:
首先是__block_impl结构体

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

我们来看一下:

  1. void *isa:声明一个不确定类型的指针,用于保存Block结构体。
  2. int Flags:标识符。
  3. int Reserved:今后版本更新所需的区域大小。
  4. void *FuncPtr:函数指针,指向实际执行的函数,也就是block中花括号里面的代码内容。

然后是struct__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

这个结构体主要就是初始化变量impl以及desc指针

接下来看一下结构体static struct __main_block_desc_0

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

  1. 第一个成员变量主要是指以后版本升级所需区域的大小(一般情况下为0)
  2. 第二个成员变量就是Block的大小
  3. __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}:和我们写结构体一样,在结尾写一个实例结构体变量,我们可以看到reserved为0,Block的Size为:sizeof(struct __main_block_impl_0)。

另外还有一个static void __main_block_func_0(struct __main_block_impl_0 *__cself

static void __main_block_func_0(struct __main_block_impl_0 *__cself {
	printf("Block\n");
}

经过变换后的源代码可以看到,通过Blocks使用的匿名函数实际上就是被作为简单的C语言函数来处理。另外,根据Block语法所属的函数名(此处为main)和该Block语法在该函数出现的 顺序值(此处为0)来给clang变换的函数命名。
该函数的参数_cself相当于C++实例方法中指向实例自身的变量this,或是OC实例方法中指向对象自身的变量self,即参数_cself为指向Block值的变量。

最后看main函数:

int main(int argc, const char * argv[]) {
	void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
	
	((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我们来看一下代码,去掉转化部分

struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;

该源代码将__main_block_impl_0结构体类型的自动变量,也就是栈上生成的__main_block_impl_0结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量blk。
第二行代码就是相当于源代码中的blk(),即使用该Block部分。去掉转换部分就是:

(*blk->impl.FuncPtr)(blk);

这是使用函数指针调用函数。由Block语法转换的__main_block_impl_0函数的指针被赋值成员变量FunPtr中。
以上就是Block的实质,Block即为Objective-C对象。"id"这一变量类型用于存储Objective——C对象。在OC源码中,虽然可想像使用void *类型那样随意使用id,但此id类型也能够在C语言中声明。
比如:

typedef struct objc_object {
	Class isa;
}*id;

id为objc_object结构体的指针类型。我们再看看Class。

typedef struct objc_clsaa *Class;

Class为objc_class 结构体的指针类型。objc_class结构体在/usr/include/objc/runtime.h声明如下:

struct objc_class {
	Class isa;
};

这与objc_object结构体相同。然而,objc_object结构体和objc_class结构体归根结底是在各个对象和类的实现中使用的最基本的结构体。下面我们通过编写简单的OC类声明来确认一下:

@interface MyObject : NSObject
{
	int val0;
	int val1;
}
@end

基于objc_object结构体,该类的对象的结构体如下:

struct MyObject {
	Class isa;
	int val0;
	int val1;
};

MyObject类的实例变量val0和val1被直接声明为对象的结构体成员。“Objective—C中由类生成对象”意味着,像该结构体这样“生成由该类生成的对象的结构体实例”。生成的各个对象,即由该类生成的对象的各个结构体实例,通过成员变量isa保持该类的结构体实例指针。如图所示:

在这里插入图片描述

各类的结构体就是基于objc_class结构体的class_t结构体。class_t结构体在objc4运行时库的runtime/objc- runtime-new.h中声明如下:

struct class_t {
	struct class_t *isa;
	struct class_t *superclass;
	Cache cache;
	IMP *vtable;
	uintptr_t data_NEVER_USE;
};

在OC中,比如NSObject的class_t结构体实例以及NSMutableArray的class_t结构体实例等,均生成并保存各个类的class_t结构体实例。该实例持有声明的成员变量、方法的名称、方法的实现(即函数指针)、属性以及父类指针,并被OC运行时库所使用。
了解了这些,我们就可以理解OC的类与对象的实质了。
那么回到我们的Block结构体:

struct __main_block_impl_0 {
	void *isa;
	int Flags;
	int Reserved;
	void *FuncPtr;
	struct __main_block_desc_0* Desc;
}

此__main_block_impl_0结构体相当于基于objc_object结构体的OC类对象的结构体。另外,对其中的成员变量isa进行初始化,具体如下:

isa = &_NSConcreteStackBlock;

即_NSConcreteStackBlock相当于class_t结构体实例。在将Block作为OC的对象处理时,关于该类的信息放置于 _NSConcreteStackBlock中。

Block存储域

通过前面可以知道,Block转换为Block的结构体类型的自动变量,__block变量转换为__block变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。
在这里插入图片描述

Block也可以当作OC对象。将Block当作OC对象来看时,该Block的类为_NSConcreteStackBlcok。虽然该类并没有出现在已变换源代码中,但有三个与之类似的类,如:

  1. _NSConcreteStackBlock
  2. _NSConcreteGlobalBlock
  3. _NSConcreteMallocBlock

这三个有啥区别呢?我们通过一个表来看一下:
在这里插入图片描述

我们可以看到不同的类有不同的设置对象的存储域。
接下来我们看一下应用程序中的内存分配情况:
在这里插入图片描述

如果Block用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此Blcok用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例。因此将Blcok用结构体实例设置在与全局变量相同的数据区域中即可。

只有截获自动变量时,Block用结构体实例截获的值才会根据执行时的状态变化。即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。

虽然通过clang转换的源代码通常是_NSConcreteStackBlcok类对象,但实际上却有不同,总结如下:

  1. 记述全局变量的地方有Block语法时
  2. Block语法的表达式中不使用应截获的自动变量时

在以上情况下,Block为_NSConcreteGlobalBlock类对象。即Block配置在程序的数据区域中。除此之外的Block语法生成的Block为 _NSConcreteStackBlock类对象,且设置在栈上。

那么问题来了,如果将Block配置在堆上的_NSConcreteMallocBlock类在何时使用呢?

Block超出变量作用域可存在的原因?配置在全局变量上的Block,从变量作用域外也可以通过指针安全的使用。但设置在栈上的Block,如果其所属的变量作用域结束,该Block就被废弃。由于__block变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该__block变量也会被废弃。如图所示:
在这里插入图片描述

所有Blocks提供了将Blcok和__block变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block语法记述的变量作用域结束,堆上的Blcok还可以继续存在。如图所示:
在这里插入图片描述

实际上复制到堆上的Block将_NSConcreteMallocBlock类对象写入结构体实例的成员变量isa。

impl.isa = &_NSConcreteMallocBlock;

而__block变量用结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。

这句话具体怎么理解呢,有时在__block变量配置在堆上的状态下,也可以访问栈上的block变量。在此情形下,只要在栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__blcok变量还是从堆上的__block变量都能够正确访问。

按配置Block的存储域,将copy方法进行复制的动作总结如下:
在这里插入图片描述

_ _block变量存储域

使用__block变量的Block从栈复制到堆上时,__block变量也会受到影响。具体情况如图所示:
在这里插入图片描述

若在1个Block中使用__block变量,则当该Block从栈复制到堆时。使用的所有__block变量也必定配置在栈上。这些__block变量也全部被从栈复制到堆。此时,Blcok持有__block变量。即使在该Blcok已复制到堆的情况下,复制Block也对所使用的__block变量没有任何影响。如图所示:
在这里插入图片描述

在多个Block中使用__block变量时,因为最先会将所有的Block配置在栈上,所以__block变量也会配置在栈上。在任何一个Blcok从栈复制到堆时,__block变量也会一并从浅复制到堆并被该Block所持有。当剩下的Block从栈复制到堆时,被复制的Block持有__block变量,并增加__blcok变量的引用计数。
如图所示:
在这里插入图片描述

如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。如图所示:
在这里插入图片描述

成员变量__forwarding的用处是什么?在栈上的__block变量用结构体实例在__block变量从栈复制到堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量用结构体实例的地址,如图所示:

在这里插入图片描述
通过该功能,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利地访问同一个__block变量。

截获对象

OC的运行时库能够准确把握Block从浅复制到堆以及堆上的Block被废弃的时机,因此Block结构体中即使含有附有__strong修饰符或__weak修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copy和dispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和__main_block_dispose_0函数。

copy函数和dispose函数调用的时机:
在这里插入图片描述

那么什么时候栈上的Block会复制到堆呢?

  1. 调用Block的copy实例方法时
  2. Block作为函数返回值返回时
  3. 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  4. 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中传递Block时。

截获对象时和使用__block变量时的不同

在这里插入图片描述

通过BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF参数,区分copy函数和dispose函数的对象类型是对象还是__block变量。
但是与copy函数持有截获的对象dispose函数释放截获的对象相同,copy函数持有所使用的__block变量,dispose函数释放所持有的__block变量。
由此可见,Block中使用的赋值给所富有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可超出其变量作用域而存在。

Block中使用对象类型自动变量时,除以下情形外,推荐调用Block的copy实例方法:

  1. Block作为函数返回值返回时
  2. 将Block复制给类的附有__strong修饰符的id类型或Block类型成员变量时
  3. 向方法名中含有usingBlock的Cocoa框架方法或Grang Central Dispatch的API中传递的Block时

Block变量和对象

__block说明符可指定任何类型的自动变量。

通过clang转换后的代码如下:

在这里插入图片描述

使用_Block_object_assign函数,持有Block截获的对象。当堆上的Block被废弃时,使用_Block_object_dispose函数,释放Block截获的对象。当__block变量附有__strong修饰符的id类型或对象类型自动变量的情形下会发生同样的过程。当__block变量从浅复制到堆时,使用_Block_object_assign函数,持有赋值给__block变量的对象。当堆上的__block变量被废弃时,使用_Block_object_dispose函数,释放赋值给__block变量的对象。
由此可知,即使对象赋值复制到堆上的附有__strong修饰符的对象类型__block变量中,只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。这与Block中使用赋值给附有__strong修饰符的对象类型自动变量的对象相同。

Block循环引用

什么情况下Block会造成循环引用?
一个对象中强引用了block,在block中又使用了该对象,就会发生循环引用。
解决是的办法是:将该对象使用__weak或者__block修饰符修饰之后再在block中使用。
变量前加block和weak和strong的区别?

  1. __strong是为了防止block持有的对象提前释放,__weak和__block是解决循环引用
  2. __block不管是ARC还是MRC都可以使用,可以修饰对象和基本数据类型
  3. __weak只能在ARC模式下使用,也只能修饰对象,不能修饰基本数据类型
  4. __block对象可以在Block中被重新赋值,__weak不可以

对于__block变量MRC如何解决循环引用?ARC如何解决?

  1. MRC解决循环引用用__block,禁止Block对所引用的对象进行retain操作.
  2. ARC时期__block并不能禁止Block对所引用的对象进行强引用。解决办法可以是在Block中将变量置空,因为需要在Block中对Block进行操作,所以还是需要__block修饰符
  3. __block在MRC下有两个作用:允许在BLock中访问和修改局部变量,禁止Block对所引用的对象进行retain操作
  4. ARC下仅仅只有一个作用
    允许在BLock中访问和修改局部变量
    iOS开发“强弱共舞——weak和strong配套使用解决block循环引用问题

总结:

  1. __weak是为了解决循环引用
  2. __strong是为了防止block持有的对象提前释放
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值