博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
iOS高级编程Blocks笔记
阅读量:755 次
发布时间:2019-03-23

本文共 12788 字,大约阅读时间需要 42 分钟。

文章目录

一句话概括Blocks

带有局部变量的匿名函数

什么是Blocks

在整个程序中,一个变量总保持在一个内存区域。

虽然多次调用函数,但变量的值总保持不变(局部变量)。
下图出现了问题,全局变量 buttonId在for循环里的值被改变
在这里插入图片描述

特别注意,c语言中不写参数意味着,参数的数量是不定的。

因为这个函数是递归函数,buttonId又是全局变量,每次传入函数的buttonId都是同一个,导致buttonId的值在“递”的过程中一直在变化,而在“归”的过程中buttonId的值始终是 BUTTON_IDOFFSET - 1,也就是最后的buttonid。

为啥用局部变量不行?

原文中,回调那个函数传递函数指针,如果用局部变量,那么还需要传递局部变量,一个还好,如果是许多的变量需要传递呢?

没有blocks时,最好是传递一个类的实例,因为实例会持有id等其他属性,所以我们只需要传递函数指针和实例就行了。
使用block就方便许多,可以直接访问 for循环里的i作为buttonId;

截获变量

block会保存I的瞬间值,自动截获临时变量。

如果想要改写局部变量的值,需要在定义变量是加__block
如果截获的是对象,对象可以调用变更方法,而不能直接赋值,同样的,想要赋值必须加__block.
对于c语言数组,blocks无法截获数组,但可以使用数组指针,用指针来访问数组。

block的实现

需要学习一下c++的strcut

首先是三个block

1struct __block_imp {void *isa;				//void *isa 无类型指针,可以存放地址,可以强制类型转换int flags;int Reserved;void *FuncPtr;}2.struct __main_block_imp_0 {	struct __block_imp impl;  //第一种类型的结构体	struct __main_block_desc_0 *Desc;  //第三种类型的结构体	//c++中允许struct中有函数	//构造函数	__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	}};3. struct __main_block_desc_0 {	unsigned long reserved;	unsigned long Block_size;} __main_block_desc_0_DATA = {	0, 	sizeof(struct __main_block_impl_0)};

一个block代码转为c++

^ {printf("Blcok/n");}c++:statci void __main_block_func_0(struct __main_block_imp_0 * __cself) {	printf("Block\n");}

上面是blcok的内容

我们看看如何把这部分内容传给上面三个结构体

void (^blk)(void) = ^{	printf("Block\n");};转为c++void (*blk)(void) = (void(*)(void))&__main_block_impl_0 ((void *)__mainblock_func_0, &__main_block_desc_0_DATA);搜了一下,(void(*)(void))是强制类型转换,转换为一个函数指针转换规则就是 去掉所有的冗杂符号,然后将每个符号分割开来例如上面的这个函数。简化一下:void (*fun)(void *参数)去掉之后就成了void (*) (void *)忘记了是哪个大神写的转换规则把上面俩行代码转为我们能看懂的形式struct __main_block_impl_0 tmp =__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);strcut __main_block_impl_0 *blk  = &tmp;

上面众多结构体中都有_0,这是根据block的数量创建的,书中只有一个block实体,所以俩个个结构体的后缀都是_0

先定义一个结构体 tmp,然后调用它的构造函数,将上面(刚刚写过)block的内容的函数作为参数,
__main_block_impl_0(void *fp, struct __main_block _desc_0 *desc, int flags = 0)
将block的大小结构体 DATA也作为参数
刚刚写过构造函数的参数,那么block就是fp,DATA对应第二个参数
如果把第一个结构体合并给第二个结构体

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

根据构造函数,fp传给了 FuncPtr,也就是block的实体。

Decs接收了block的大小。
然后把这个结构体赋值给tmp
再把tmp给blk。
那么使用block呢

blk();转为c++	((void(*)(struct __block_imp *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl*)blk);	再转换一下:	(*blk->impl.FuncPtr)(blk);	正如刚刚block的实体cself。	FuncPtr保存了 printf("Blocks/n");	cself相当于self。因此传入了blk本身

在上面的isa中

isa = &_NSConcreteStackBlock;

isa指针在OC中指向一个类。例如对象的iSA指针指向它的类,类的iSA指针指向元类

,类又叫类对象。这里isa指针被赋值block指针,所以block就是OC对象。

如何截获自动变量

转换成源码时

//block结构体struct __main_block_impl_0 {	struct __block_imp impl;	struct __block_imp_desc_0*desc;	const char *fmt;	int val;}//block的内容static void __main_block_func_0(struct __main_block_imp_0 *_cself) {	const char *fmt = __cself->fmt;	int val = __cself->val;	printf(fmt, val);}//主函数int main() {	int dmy = 256;	int val = 10;	const char *fmt = "val = %d\n";	void ( *blk)(void) = &__main_block_imp_0 (	__main_block_func_0, &__main_block_desc_0_DATA, fmt, val);	return 0;}//struct的构造方法__main_block_imp_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0):fmt(_fmt), val(_val) {	//其他没变}

临时变量追加到了block的结构体中,变量类型完全一致,在block中没有使用的临时变量不会追加到结构体。

但注意,无法直接截获数组
因为数组不支持整体赋值。
上述内容只是将变量拷贝了一份,在block中对变量做的改变无法影响原变量。
改变也会报错

__block说明符号

对于普通的静态全局变量和全局变量,在block中没有改变,相当于在函数中使用。

如果是普通的静态局部变量呢?

int *static_val = __cself->static_val;(*static_val) *= 3;

静态局部变量会和局部变量一样被追加到block的结构体中,但是在操作这些静态局部变量时方式是不同的。

这里使用指针指向这个变量,然后通过改变指针指向的地址的值来改变变量。

//局部变量int val = __cself->val;printf("%d", val);//静态局部变量int *val = __cself->val;printf("%d", *(val));

这样一来,似乎局部变量也可以通过上面的方式修改,为什么只有静态局部变量可以呢?

因为静态局部变量的生命周期问题,如果在一个函数内定义一个静态局部变量,函数retrun了之后,这个变量依然存在,值不会改变,所以在block中用指针访问始终是安全的,如果是局部变量,那么这个局部变量在超出作用域后会被释放,指针将为野指针。

使用__block存储域类说明符

如果在定义局部变量之前加上 block说明符。

__block int val = 10;void (^blk)(void) = ^ {val = 1 ;}

转为c++的代码后:

struct __Block_byref_val_0 {	void *__isa;	__Block_byref_val_0  *__forwarding;	int __flags;	int __sizel	int val;};struct __main_block_impl_0 {	struct __block_imp impl;	struct __main_block_desc_0 *desc;	__Block_byref_val_0 *val;	__main_block_impl_0(void* fp, struct __main_block_desc_0 *desc, __Block_byref_val_0* val, int flags=0): val(_val->forwarding) {	impl.isa = &_NSConcreteStackBlock;	impl.Flags =flags;	impl.FuncPtr = fp;	Desc = desc;	}};static void  __main_block_func_0(struct __main_block_impl_0 * __cself) {	__Block_byref_val_0 *val = _cself_val;	(val->_forwarding->val) = 1;}static void __main_block_copy_0 {	struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {	_Block_object_assign(&dst_>val, src->val, BLOCK_FIELD_IS_BYREF);}statict void  __main_block_dispose_0(struct __main_block_impl_0*src) {	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);}static struct __main_block_desc_0 {	unsigned long reserved;	unsigned long 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(__main_block_impl_0),	__main_block_copy_0,	__main_block_dispose_0};int main() {	_Block_byref_val_0 val = {		0, 		&val,		0,		sizeof(__Block_byref_val_0),		10	};	blk = &__main_block_impl_0 (__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);	return 0;}

__block说明符,多了一个结构体__Block_byref_val_0.

意味着他变成了一个结构体类型的局部变量。
而局部变量被赋值给了他的成员变量val,名称相同。
原本__main_block_impl_0里追加的是局部变量,现在变成了结构体变量__Block_byref_val_0;
来看看__main_block_impl_0的构造函数

val(_val->forwarding)_val是刚初始化的,val是__main_block_impl_0成员。传入的是__val的forwarding,在初始化的时候,forwarding里存的是它自己的地址。完成了结构体的初始化。在block里修改val的值时,和修改静态变量类似,创建一个__Block_byref_val_0*来持有结构体里的val结构体变量。而这个val结构体变量的初始化是:
__block int val = 10;__block_byref_val_0 = {	0,	&val,	0,	sizeof(__Block_byref_val_0),	10}

这个结构体里存放着自己的地址。这个指针是__forwarding

所以在修改的时候

val->__forwarding->val = 1;

此时的val是个结构体指针,因为原本的变量是个结构体局部变量,在匿名函数block中修改它的值已经超出了作用域,所以用指针的方式来修改。

我们在这里修改的是 main()函数里初始化的__block 变量里的成员val。
val指向的也是__block变量,__block变量的forwarding指向自己。
所以我们修改的就是外面的int a。因为这个int a被封装成了 __block结构体。

__Block_byref_val_0结构体的好处

多个block都可以使用这个结构体,不用重复创造

截获变量总结

在这里插入图片描述

截获对象继续往下看

  • block不截获全局变量、静态全局变量
  • blcok截获自动变量
    截获静态局部变量是在结构体中加入指针
    截获auto局部变量是在结构体中加入这个变量的副本,无法修改。

block存储域

  • _NSConcreteStackBlock 栈区
  • _NSConcreteGlobalBlock 程序的数据区data区
  • _NSConcreteMallocBlock 堆区

如果在声明全局变量的地方使用block,那么创建的block为_NSConcreteGlobalBlock类对象

虽然声明在函数内,但是如果没有截取自动变量,那么创建的block还是_NSConcreteGlobalBlock类对象。
我们之前创建的block一直是局部变量,没有出现过 _NSConcreteMallocBlock类型。
看看下面的例子,这是在MRC下运行

#import "ViewController.h"void(^block)(void);@implementation ViewController- (void)viewDidLoad {    [super viewDidLoad];    NSInteger i = 10;    block = ^{        NSLog(@"%ld", i);    };}- (void)touchesBegan:(NSSet
*)touches withEvent:(UIEvent *)event { block();}@end

声明了一个全局的块变量,然后在viewDidLoad里创建了一个块,把他赋值给了全局块变量block。

在touchesBegan中调用块会出现错误。
在arc下可以运行,在arc下相当于给block加了__strong关键字
因为arc为我们完成了将块从栈复制到堆的步骤。
在这里插入图片描述
复制到堆上的Block将_NSConcreteMallocBlock类对象写入isa指针中。
而__block变量用结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确访问__block变量。
这个复制过程是怎样的呢?

typedef int (^blk_t)(int);blk_t func(int rate) {	return ^(int count){		return rate * count;	};}

转为c++

blk_t func(int rate) {	blk_t tmp = &__func_block_impl_0 {		__func_block_func_0, &__func_block_desc_0_DATA, rate);		tmp = objc_retainBlock(tmp);		return objc_autoreleaseReturnValue(tmp);	}}

在arc下,tmp默认是__strong

objc_retainBlock在runtime中是Block_copy函数,即:

tmp =__Block_copy(tmp);return objc_autoreleaseReturnValue(tmp);

通过上面的函数,将block复制到堆上,然后讲堆上的地址作为指针赋值给了tmp。

return时,将block作为OC对象加入到自动释放池中。
大多数情况下不用我们手动复制,arc帮我完成了,当以下情况时,我们需要显式的调用copy方法。

  • 向方法或函数的参数中传递block时
    但是如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了。
    以下方法或函数不用手动复制。
  • Cocoa框架的方法且方法名含有usingBlock等时。
  • GCD的API。
    不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。

__block变量存储域

当一个Block被复制到堆上时,所有的__block变量也会被复制到堆,多个Blcok被复制到堆上,使用的__block变量的引用计数将增加。这和OC的引用计数内存管理方式相同。

那么forwarding的指向呢?

__block int val = 0;void (^blk)(void) =[ ^{++val;} copy];++val;blk();NSLog(@"%d", val);

在这里的blk从栈复制到了堆上,在block外调用++val时,原来在栈上的block并未被释放,val变量在也会被复制到堆上,那此时++val,到底加的是谁的呢

在之前的赋值操作中,__block变量使用的是它结构体里的forwarding指针。
block在栈时,__forwarding指向自己本身。

struct __Block_byref_val_0 {	void *__isa;	__Block_byref_val_0 * __forwarding;	int __flags;	int __size;	int val;}//赋值self->__forwarding->val = 1;

而复制到堆上的__block变量的__forwarding也是指向本身的指针,此时在栈上的__block他的__forwarding指针指向的是堆上的地址,所以复制到堆上后,改变的都是堆上block中截取的变量的值。

block截获对象

截获对象时,block结构体追加了id类型的变量

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id __strong array;
};
虽然OC结构体中不能含有对象型变量,因为编译器不知道在什么时候进行c语言结构体的初始化和废弃操作,不能很好的管理内存,可是OC的runtime可以准确把握Block从栈复制到堆以及堆上的Block被废弃的时机,因此Block用结构体中即使含有对象型变量,也可以初始化和废弃。所以还增加了copy和dispose成员变量以及对应的初始化函数。
因为有array变量,所以要恰当的管理赋值给array的对象,所以__main_block_copy_0函数使用_Block_object_assign函数将对象类型对象赋值给Block用结构体的成员变量array中并持有该对象。
这个_Block_object_assign相当于retain实例方法函数。__main_block_dispose_0函数使用_Block_object_dispose函数。

static void __main_block_copy_0(struct __main_block_impl_0* dst, struct __main_block_impl_0 *src) {	_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);}static void __main_block_dispose_0(struct __main_block_impl_0 *src) {	_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);}

虽然这些函数OC代码转换后都存在,但是从什么时候被调用呢?

Block从栈复制到堆上和堆上的block被废弃的时候会调用这俩个函数。
__block变量的结构体也有这俩个方法,区别在于后边的参数
BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF

blk_t blk{	id array = [[NSMutableArray alloc] init];	blk = ^(id obj) {		[array addObject:obj];		NSLog(@"array count = %d", [array count]);	}}blk([[NSObject alloc] init])

书上说:

上面的伪代码中,运行花阔号外的代码会崩溃,因为array是局部变量,在花括号结束后会释放。
所以给block加上copy关键字,使得block持有对象,代码正常运行。
可是我试验的时候发现:
没有加copy,block也被默认复制了。
而array的引用计数是3,因为栈上的block和堆上的block都引用了array。
查看引用计数方法:

创建一个NSObject的扩展文件,对这个文件设置-fno-objc-arc,函数:- (NSUInteger)arcDebugRetainCount {return [self retainCount];}外部ARC环境就可以调用了

看一个例子

在这里插入图片描述
&obj是指针的地址,直接打印是[NSObject alloc] init]的地址。
发现指针地址不同,而所指的内存地址是一样的。
c++代码:
在这里插入图片描述
void (blk)(void) = ((void ()())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, obj, 570425344));
可以看到结构体里是追加了一个同样类型的新指针,赋值也是使新指针的指向一直所以出现上面的打印结果。

__block修饰对象

在这里插入图片描述

struct __Block_byref_obj_0 {  void *__isa;__Block_byref_obj_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); NSObject *obj;};

前面讲了会产生这个结构体,而初始化这个结构体

__Block_byref_obj_0 obj = {(void*)0,(__Block_byref_obj_0 *)&obj, 33554432, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};

所以指针是新指针,内存还是一样。

__block可指定任何类型的自动变量。下面指定用于赋值Objective-C对象的id类型自动变量。

__block id obj = [[NSObject alloc] init];

当Block从栈复制到堆时,使用__Block_object_assign函数,持有Block截获的对象,当堆上的Block被废弃的时候,才会执行_Block_object_dispose函数,释放Block截获的对象。

我们前面截获对象默认是__strong类型,如果是__weak呢

blk_t blk; { 	id array = [[NSMutableArray alloc] init]; 	id __weak array2 = array; 	blk = [^(id obj) { 	 	[array2 addobject:obj]; 	NSLog(@"array2 count = %d", [array count]);	 	} copy]; } blk([[NSobject alloc] init]); blk([[NSobject alloc] init]); blk([[NSobject alloc] init]);

打印结果:

如果是__weak:array2 count = 0array2 count = 0array2 count = 0如果是 __strong:array2 count = 1array2 count = 2array2 count = 3

如果__block和weak一起加呢

array2 count = 0array2 count = 0array2 count = 0

和之前一样。

而打印引用计数是2,只有array和array2持有,block并不持有weak对象。
__weak和__block俩个同时加上,引用计数还是2,此时的block也是在堆上。

block循环引用

typedef void (^blk_t)(void);@interface Person : NSObject {    blk_t blk_;}@end@implementation Person- (instancetype)init {    if (self = [super init]) {        blk_ = ^{            NSLog(@"self = %@", self);        };    }    return self;}- (void)dealloc {    NSLog(@"delloc");}int main(void) {    id o = [[Person alloc] init];    NSLog(@"%@", o);    return 0;}@end

在init方法中会出现警告:

Capturing ‘self’ strongly in this block is likely to lead to a retain cycle
而且delloc方法没有被调用。
因为在init方法里,block调用了self,所以复制到堆上的block持有了self,而self的成员变量blk_持有了block,导致循环引用。
可以在调用self时使用weak。
而且使用weak不必担心野指针的问题,因为block存在时(Person持有block),self必存在。

id  __weak tmp = self;
@interface Person : NSObject {    blk_t blk_;    id obj_;}@implementation Person- (instancetype)init {    if (self = [super init]) {        blk_ = ^{            NSLog(@"self = %@", obj_);        };    }    return self;}- (void)dealloc {    NSLog(@"delloc");}@end

在这里插入图片描述

这里的成员变量obj_相当于self->obj。所以也截获了self,导致循环引用
也可以用__block来避免循环引用,但必须执行block

- (id)init {	self = [super init];	__block id tmp = self;	blk_ = ^{		NSLog(@"self = %@", tmp);		tmp = nil;	}	return self;}

因为必须执行tmp = nil;否则就会出现循环引用。

Person实例对象持有了Block,Block截获持有__block类型变量tmp,tmp持有Person实例对象。
如果在block里把tmp设置为nil,
tmp不再持有Person实例对象,引用环打破。

参考文章

转载地址:http://lavzk.baihongyu.com/

你可能感兴趣的文章