iOS——浅谈Blocks

Blocks?

Blocks是C语言的扩充功能,可以用一句话概括:带有自动变量的匿名函数。
匿名函数就是不带名称的函数,这在C语言函数中是不被允许的,但block就可以实现这个功能。

Blocks模式

Blocks语法

下面是一个block

^{printf(fmt, val);};

实际上上面的例子使用了省略方法,完整形式如下:

^void (void){printf(fmt, val);};

完整的block语法和c函数相比较有两个不同之处:
1.没有函数名。
2.返回值类型前带有"^"(插入记号,caret)记号。
下面是Block语法格式:^ 返回值类型 参数列表 表达式
其中返回值类型和参数列表都是可以省略的。

Block类型变量

在c中,我们可以将函数地址赋值给函数指针类型,同理,Block语法也可以赋值给Block类型的变量,声明Block类型变量示例如下:

int (^blk) (int);

Block类型变量和一般变量完全相同,可以作为以下用途:

自动变量
函数参数
静态变量
静态全局变量
全局变量

将Block赋值为Block类型变量:

void (^blk) (void) = ^{printf(fmt, val);};

函数中Block作为参数:

int func(int (^blk) (int));

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

//blk func() {
//    return ^(void){
//        printf("!");
//    };
//}

直接使用block类型变量作为返回值和参数记述比较复杂,还会有bug
在这里插入图片描述
这里盲猜是因为编译器无法识别前面整体的block类型。
如果我们使用typedef就可以解决这个问题。

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

定义完之后可以这样调用:int ans = blk(10);.

截获自动变量值

之前了解到Blocks是一个带有自动变量的匿名函数,那么我们解析一下自动变量,带有自动变量在Blocks中体现为“截获自动变量值”
示例如下:

    //Block截获自动变量值
    int dmy = 256;
    int val = 10;
    const char* fmt = "val = %d\n";
    val = 3;
    // 截获自动变量的瞬间值
    void (^blk) (void) = ^{printf(fmt, val);};
    val = 2;
    fmt = "val changed = %d\n";
    blk();

打印结果是val = 3;
而不是我们修改后的val changed = 2;

这就是Block截获自动变量,当Blocks执行时,使用的是自动变量的瞬间值,Block保存了这个值,在执行语句时会使用保存的值。

__block说明符

那么我们既然可以在block中调用自动变量,那么我们尝试一下去修改这些变量的值。

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

看上去很合理,但实际会报错,如果想在block中给语法外的自动变量赋值,那么在定义自动变量时,应该加上__block说明符,就可以实现赋值,如下:

// 倘如要在block中修改自动变量,应该加上__block修饰符,修饰的变量称为block变量
    
    __block int val = 0;
    void (^blk) (void) = ^{ val = 1;};
    blk();
    printf("%d", val);

这样就可以修改值了,加入该说明符的变量被称为__block变量。

截获的自动变量

上面我们尝试改变一个语法外的自动变量,不加说明符时会出现问题,那么我们尝试一下截取OC对象。

// 理解截获的自动变量
    // 调用捕获对象的方法可
    id array = [[NSMutableArray alloc] init];
    __block id array2 = [[NSMutableArray alloc] init];
    
    void (^blk) (void) = ^{
        id obj = [[NSObject alloc] init];
        
        [array addObject:obj];
    };

这里我们可以调用对象的方法,试一试赋值:

// 给对象赋值
    // 错误
    void (^blk1) (void) = ^{
//        array = [[NSMutableArray alloc] init];
    };

在这里插入图片描述
可以看到这里是错误的,正确写法和上面一样,加上说明符。

这里提一下截获c风格数组如下:
在这里插入图片描述
如果不使用指针,截获自动变量的方法并没有实现对C语言数组的截获,使用指针可以解决这一问题。

Blocks的实现

block实质

Block究竟是什么呢?他的本质是什么?
block其实本质上是被作为很简单的c语言来实现的,clang(LLVM编译器)具有转换为我们可读源代码的功能。我们可以通过“-rewrite-objc”选项就能将含有 Block 语法的源代码变换为C++的源代码。说是C++,其实也仅是使用了struct 结构,其本质是C语言源代码。
先简单些一个Block;

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



经过clang转化后的c++代码:

//经过clang转换后的C++代码
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 {
  	size_t reserved;
  	size_t Block_size;
} __main_block_desc_0_DATA = {
	0,
	sizeof(struct __main_block_impl_0)
};

int main(int argc, const char * argv[]) {
	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;
}



短短的几行代码,经过转化后变成了几十行,但其实无非是加入了一些结构体,我们逐步分析一下。
先看一下比较像的地方。

^{
	printf("Block\n");
};


转化后为:

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


这里我们可以观察到通过Blocks使用的匿名函数实际上被作为简单的C语言函数来处理了
该函数的参数__cself相当于C++实例方法中所指的自身变量this,或是OC实例方法中指向对象自身的变量self,即参数__cself为指向Block值的变量。
下面我们看看struct __main_block_impl_0* __cself这个参数,这个参数是一个结构体指针,该结构体声明如下:

struct __main_block_impl_0 {
  struct __block_impl impl;   
  struct __main_block_desc_0* Desc;
  
  //构造函数 初始化对象 
  //参数1 fp:函数指针
  //参数2 desc:作为静态全局变量初始化的 __main_block_desc_0 结构体实例 指针
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
  	//这里说明一下,Block就是OC对象
    impl.isa = &_NSConcreteStackBlock;   //先理解为block的类型  
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


这里加入了构造函数所以看着有些多
其实也就是两个结构体。
第一个结构体:__block_impl

struct __block_impl {
  void *isa; //Block类型
  int Flags;   //标识符
  int Reserved;    //今后版本升级所需的区域
  void *FuncPtr;   //函数指针
};


第二个结构体:__main_block_desc_0

static struct __main_block_desc_0 {
  	size_t reserved;
  	size_t Block_size;
}


这些也如同其成员名称所示,其结构为今后版本升级所需的区域和Block的大小。
那么,下面我们来看看初始化含有这些结构体的__main_block_impl_0结构体的构造函数。

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
	// 设置了Block类型
    impl.isa = &_NSConcreteStackBlock;
    
    impl.Flags = flags;
    // 将函数指针赋值给结构体实例
    impl.FuncPtr = fp;
    Desc = desc;
}


下面看一下main函数里面关于block的代码:

nt main(int argc, const char * argv[]) {
	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;
}
// 这里的强制转换有点多,去掉强制转换:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
// 这里的两个参数第一个为 转换后匿名函数的地址
// 第二个为作为静态全局变量初始化的_main_block_desc_0指针
struct __main_block_impl_0 *blk = &tmp;

// 这样就容易理解了。该源代码将__main_block_impl0结构体类型的自动变量,即栈上生成的__main_block_impl_0 结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量 blk。
// 对应下面的源代码
void(^blk)(void)=^{printf("Block\n");};

// 下面为__main_block_desc_0 结构体实例的初始化部分代码。

static struct __main_block_desc_0 __main_block_desc_0_DATA = {
	0sizeof(struct __main_block_impl_0)// block实例大小
};
// 下面我们看一下blk();部分
// 转化后代码
((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct_block_impl *)blk);

// 去掉转换:
(*blk->impl.FuncPtr)(blk);

// 这就是简单地使用函数指针调用函数。正如我们刚才所确认的,由Block 语法转换的__main_block_func_0函数的指针被赋值成员变量FuncPtr中。另外也说明了,__main_block_func_0函数的参数__cself指向Block值。在调用该函数的源代码中可以看出Block正是作为参数进行了传递。


截获自动变量实现

之前提到了Block可以截获自动变量,那么转化后到底是如何实现的
在这里插入图片描述
下面是转化后的代码:

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;
  	const char *fmt;
  	int val;
  	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
  	}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself {
  	const char *fmt = __cself->fmt;
  	int val = __cself->val; 
  
	printf(fmt, val);
}

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)
};

int main(int argc, const char * argv[]) {
	int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
    return 0;
}


这与前面转换的源代码稍有差异。下面来看看其中的不同之处。首先我们注意到,Block语法表达式中使用的自动变量被作为成员变量追加到了__main_block_impl_0结构体中

struct __main_block_impl_0 {
  	struct __block_impl impl;
  	struct __main_block_desc_0* Desc;
  	const char *fmt;
  	int val;
}


同样的在构造函数也多加入了参数去初始化结构体实例:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
fmt = "val = %d\n";
val = 10;


初始化时对fmt和val进行了赋值。由此可知,在__main_block_impl_0结构体实例中(即Block),自动变量被截获。
再看一下使用Block的匿名函数的实现:

// 源代码
^{printf(fmt, val)};
// 转化后
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  	const char *fmt = __cself->fmt;
  	int val = __cself->val;
  	
	printf(fmt, val);
}


转换后的源代码中,截获到__main_block_impl_0 结构体实例的成员变量上的自动变量,这些变量在Block语法表达式之前被声明定义。因此,原来的源代码表达式无需改动便可使用截获的自动变量值执行。
总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(即Block 自身)中。

__block说明符

之前提到想要修改自动变量的值就需要加上__block说明符,如前所述,因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出给被截获自动变量赋值的操作时,便产生编译错误。
解决这个问题有两种方法。第一种:C语言中有一个变量,允许Block改写值。具体如下:

  • 静态变量
  • 静态全局变量
  • 全局变量
    虽然Block语法的匿名函数部分简单地变换为了C语言函数,但从这个变换的函数中访问静态全局变量/全局变量并没有任何改变,可直接使用。
    但是静态变量的情况下,转换后的函数原本就设置在含有Block语法的函数外,所以无法从变量作用域访问。
    我们来看看下面这段源代码。
int global_val = 1;
static int static_global_val = 2;
int main()
{
	static int static_val = 3;
	void (^blk)(void) = ^{
		global_val *= 1;
		static_global_val *= 2;
		static_val *= 3;
	};
	return 0;
}


源代码使用了Block改写静态变量static_val、静态全局变量static_global_val 和全局变量 global_val。该源代码转换后如下:

int global_val = 1;
static int static_global_val = 2;
struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0* Desc;
	int *static val;
	
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,
int *_static_val, int flags=0) : static_val(_static_val) {
		impl.isa = &_NSConcreteStackBlock;
		impl.Flags = flags;
		impl.FuncPtr = fp;
		Desc = desc;
	}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	int *static_val = __cself->static_val;
	global_val *= 1;
	static_global_val *= 2;
	(*static_val) *= 3;
}
static struct __main_block_desc_0 {
	unsigned long reserved;
	unsigned long Block_size;
} __main_block_desc_0_DATA = {
	0sizeof(struct__main_block_impl_0)
};
int main() {
	static int static val = 3;
	blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val); 
	return 0;
}


其实这里的全局变量和静态全局变量都很好理解,那么静态变量是如何修改的?

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	int *static_val = __cself->static_val;
	
	(*static_val) *= 3;
}


使用静态变量static_val的指针对其进行访问。将静态变量static_val的指针传递给main_block_impl_0结构体的构造函数并保存。这是超出作用域使用变量的最简单方法。
静态变量的这种方法似乎也适用于自动变量的访问。但是我们为什么没有这么做呢?
实际上,在由Block语法生成的值Block上,可以存有超过其变量作用域的被截获对象的自动变量。变量作用域结束的同时,原来的自动变量被废弃,因此 Block 中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。这些在下节详细说明。
解决Block中不能保存值这一问题的第二种方法是使用“__block说明符”。更准确的表述方式为“__block存储域类说明符”(__block storage-class-specifier)。

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


该源代码转化后如下:

// 被修饰变量的结构体声明:
struct __Block_byref_val_0 {
	void *__isa;
	// 指向自身的一个指针
	__Block_byref_val_0 *__forwarding;
	int __flags;
	int __size;
	// 持有成员变量为原变量

	int val;
};
struct __main_block impl_0 {
	struct __block_impl 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_byrefval_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 中向静态变量赋值时,使用了指向该静态变量的指针。而向__block变量赋值要比这个更为复杂。Block 的__main_block_impl_0 结构体实例持有指向__block 变量的__Block_ byref_val_0 结构体实例的指针。
__Block_byref_val_0 结构体实例的成员变量__forwarding 持有指向该实例自身的指针。通过成员变量__forwarding 访问成员变量val。(成员变量 val 是该实例自身持有的变量,它相当于原自动变量。)
*/
	__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);
}

static void __main_block_dispose_0(struct __main_block_imp1_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(structmain_block_impl_0),
	__main_block_copy_O,
	__main_block_dispose_0
};
int main() {

/* _block变量也同 Block一样变成__Block_byref_val_0结构体类型的自动变量,同时该结构体持有相当于原自动变量的成员变量。*/
	__Block_byref_val_0 val = {
		0,
		&val,
		0,
		sizeof(__Block_byref_val_0),
		10
	};
	blk = &__mainblock_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);

	return 0;
}


另外,__block变量的__Block_byref_val_0 结构体并不在 Block 用__main_block_impl_0 结构体中,这样做是为了在多个Block 中使用__block变量。我们看一下下面的源代码。

__block int val = 10;
void (^blk0)(void) = ^{val = 0;};
void (^blk1)(void) = ^{val = 1;};
// 我们把这两部分源代码的转换结果摘录出来。
__Block_byref_val_0 val = {0, &val, 0, sizeof(_Block_byref_val_0), 10};

blk0 = &__main_block_impl_0(__main_block_func_0, &_main_block_desc_0_DATA, &val, 0x22000000);

blkl = &__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, &val, 0x22000000);



两个Block 都使用了__Block_byref_val_0结构体实例val的指针。这样一来就可以从多个 Block 中使用同一个__block 变量。当然,反过来从一个 Block中使用多个__block 变量也是可以的。只要增加 Block 的结构体成员变量与构造函数的参数,便可对应使用多个__block 变量。

Block存储域

这里解释一下之前的ias指针。
通过之前的说明可知 Block也是Objective-C 对象。将Block当作Objective-C对象来看时,该Block的类为_NSConcreteStackBlock。虽然该类并没有出现在已变换源代码中,但有很多与之类似的类。

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

首先,我们能够注意到_NSConcreteStackBlock 类的名称中含有“栈”(stack)一词,即该类的对象Block设置在栈上。
同样地,_NSConcreteGlobalBlock类对象如其名“全局”(global)所示,与全局变量一样,设置在程序的数据区域(.data区)中。
NSConcreteMallocBlock类对象则设置在由malloc 函数分配的内存块(即堆)中。
这里内存分区可以参考一下我之前写的博客:iOS内存分区
到现在为止出现的Block例子使用的都是_NSConcreteStackBlock 类,且都设置在栈上。但实际上并非全是这样,在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象,如:

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

// 这里的isa指针就指向_NSConcreteGlobalBlock;

该 Block 的类为_NSConcreteGlobalBlock 类。此 Block 即该 Block 用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此Block用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例。因此将Block用结构体实例设置在与全局变量相同的数据区域中即可。
只在截获自动变量时,Block 用结构体实例截获的值才会根据执行时的状态变化。例如以下源代码中,虽然多次使用同一个Block语法,但每个for循环中截获的自动变量的值都不同。

typedef int(^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
	blk_t blk = ^(int count) {return rate * count;};
}


也就是说,即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。

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

在以上这些情况下,Block 为_NSConcreteGlobalBlock 类对象。即Block配置在程序的数据域中。除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。
那么将 Block配置在堆上的_NSConcreteMallocBlock类在何时使用呢?
下面先解释一下:

  • Block 超出变量作用域可存在的原因。
  • __block变量用结构体成员变量__forwarding 存在的原因

配置在全局变量上的Block,从变量作用域外也可以通过指针安全地使用。但设置在栈上的 Block,如果其所属的变量作用域结束,该 Block 就被废弃。由于__block 变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该__block 变量也会被废弃。
在这里插入图片描述
变量作用域结束后,栈上的__block变量和Block也被废弃。
Blocks 提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在。
在这里插入图片描述
复制到堆上的Block将_NSConcreteMallocBlock 类对象写入 Block 用结构体实例的成员变量isa。

impl.isa = &_NSConcreteMallocBlock;

__block 变量用结构体成员变量__forwarding 可以实现无论__block变量配置在栈上还是堆上时都能够正确地访问__block变量
只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block 变量还是从堆上的__block 变量都能够正确访问。
Block copy:
在ARC环境下编译器会进行判断,以下情况会自动复制

  • Block作为函数返回值返回
  • Cocoa框架的方法且方法名中含有usingBlock等时,GCD的API
  • 将Block赋值给类的附有__strong修饰符的id类型 或 Block类型的成员变量时
    其余的情况我们要自行判断是否添加copy,例如下面的例子:
- (id)getBlockArray {
	int val = 10;
	
	return [[NSArray alloc] initWithObjects:
		[^{NSLog(@"blk:%d", val);} copy],
		[^{NSLog(@"blk1:%d",val);} copy], nil];
}

id obj = getBlockArray();

typedef void (^blk_t)(void);

blk_t blk =(blk_t)[obj objectAtIndex:0];

blk();


虽然看起来有点奇怪,但像这样,对于Block 语法可直接调用copy方法。当然对于Block类型变量也可以调用copy方法。

typedef int (^blk_t)(int);

blk_t blk = ^(int count){return rate * count;};

blk = [blk copy];


另外,对于已配置在堆上的Block以及配置在程序的数据区域上的Block,调用copy方法又会如何呢?下面按配置Block的存储域,将copy方法进行复制的动作总结了出来。
在这里插入图片描述

不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。

__block变量存储域

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

在这里插入图片描述
在多个 Block 中使用__block 变量时,因为最先会将所有的 Block 配置在栈上,所以__block变量也会配置在栈上。在任何一个 Block 从栈复制到堆时,__block 变量也会一并从栈复制到堆并被该 Block所持有。当剩下的 Block 从栈复制到堆时,被复制的 Block 持有__block 变量,并增加__block变量的引用计数。
在这里插入图片描述
如果配置在堆上的Block被废弃,那么它所使用的 __block变量也就被释放。
在这里插入图片描述
到这里我们可以看出,此思考方式与OC 的引用计数式内存管理完全相同。使用 block变量的 Block 持有__block 变量。如果 Block 被废弃,它所持有的__block变量也就被释放。

__forwarding指针

了解了__block的存储域之后,对于__block变量所转换的结构体中的__forwarding指针,之前说他是指向自身的指针。
栈上的__block变量在__block变量从栈上复制到堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量的结构体实例的地址。
在这里插入图片描述
通过该功能,无论是在 Block 语法中、Block 语法外使用__block变量,还是__block 变量配置在栈上或堆上,都可以顺利地访问同一个__block变量。

截获对象

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

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]); 
blk([[NSObject alloc] init]);


变量作用域结束的同时,变量array 被废弃,其强引用失效,因此赋值给变量array的 NSMutableArray 类的对象必定被释放并废弃。但是该源代码运行正常,其执行结果如下:

array count = 1
array count = 2
array count = 3


这一结果意味着赋值给变量array的NSMutableArray类的对象在该源代码最后Block的执行部分超出其变量作用域而存在。通过编译器转换后的源代码如下:

/*Block用结构体/函数部分*/
struct __main_block_impl_0 {
	struct __block_impl_impl;
	struct __main_block_desc_0* Desc;
	id __strong array;
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) {
		impl.isa = &_NSConcreteStackBlock;
		impl.Flags = flags;
		impl.FuncPtr = fp;
		Desc = desc;
	}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
	id strong array = __cself->array;
	[array addObject:obj];
	NSLog(@"array count = %ld", [array count]);
}

static void __main_block_copy_0(struct __main_block_impl_o *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);
}

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(struct __main_block_impl_0),
	__main_block_copy_0,
	__main_block_dispose_0
};


/*Block语法,使用Block部分 */
blk_t blk;
{
	id __strong array = [[NSMutableArray alloc] init];
	blk = &_main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
	blk = [blk copy];
}

(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);


在Objective-C中,C语言结构体不能含有附有strong修饰符的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。
但是Obiective-C的运行时库能够准确把握Block从栈复制到堆以及堆上的Block 被废弃的时机,因此Block 用结构体中即使含有附有__strong修饰符或__weak 修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copy 和 dispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和_main_block_dispose_0函数。
由于在该源代码的Block用结构体中,含有附有__strong修饰符的对象类型变量array,所以需要恰当管理赋值给变量array的对象。因此__main_block_copy_0 函数使用_Block_object_assign 函数将对象类型对象赋值给Block用结构体的成员变量array中并持有该对象。

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);
}


_Block_object_assign 函数调用相当于retain 实例方法的函数,将对象赋值在对象类型的结构体成员变量中。
另外,__main_block_dispose_0 函数使用_Block_object_dispose 函数,释放赋值在 Block 用结构体成员变量array中的对象。

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


_Block_object_dispose 函数调用相当于 release 实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。
虽然此__main_block_copy_0 函数(以下简称 copy 函数)和__main_block_dispose_0 函数(以下简称 dispose 函数)指针被赋值在__main_block_desc_0 结构体成员变量copy 和 dispose 中,但在转换后的源代码中,这些函数包括使用指针全都没有被调用。那么这些函数是从哪调用呢?
在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。
调用copy函数和dispose 函数的时机:
在这里插入图片描述
那么什么时候栈上的 Block 会复制到堆呢?

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

Dispatch 的 API 中传递 Block 时在调用Block的 copy实例方法时,如果Block配置在栈上,那么该Block 会从栈复制到堆。Block作为函数返回值返回时、将Block赋值给附有__strong 修饰符id类型的类或Block类型员变量时,编译器自动地将对象的Block作为参数并调用_Block_copy函数,这与调用Block的copy实例方法的效果相同。在方法名中含有usingBlock 的 Cocoa 框架方法或 Grand Central Dispatch 的API 中传递 Block 时,在该方法或函数内部对传递过来的Block 调用 Block 的 copy 实例方法或者_Block_copy函数。
也就是说,虽然从源代码来看,在上面这些情况下栈上的Block 被复制到堆上,但其实可归结为_Block_copy函数被调用时 Block 从栈复制到堆。
相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数。这相当于对象的 dealloc 实例方法。
有了这种构造,通过使用附有__strong修饰符的自动变量,Block 中截获的对象就能够超出其变量作用域而存在。
虽然这种使用copy函数和dispose函数的方法在前面没做任何说明,但实际上在使用 block变量时已经用到了。

static void __main_block_copy0(
	struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}


转换后的源代码在Block用结构体的部分基本相同,其不同之处:
截获对象时和使用__block变量时的不同
在这里插入图片描述

Block内存管理

如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block 从栈复制到堆时,该对象为Block 所持有。这样容易引起循环引用。我们来看看下面的源代码:

typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
	blk_t blk_;
}
@end

@implementation MyObject

- (id)init {
	self = [super init];
	blk_ = ^{
		NSLog(@"self = %@", self);
	};
	return self;
}
- (void)dealloc {
	NSLog(@"dealloc");
}
@end

int main(){
	id o = [[MyObject alloc] init];
	NSLog(@"%@", o);
	return 0;
}


该源代码中MyObject 类的 dealloc 实例方法一定没有被调用。
MyObject 类对象的 Block 类型成员变量blk_持有赋值为Block 的强引用。即 MyObject 类对象持有Block。init 实例方法中执行的 Block 语法使用附有__strong 修饰符的 id 类型变量 self。并且由于Block 语法赋值在了成员变量blk_中,因此通过Block 语法生成在栈上的 Block 此时由栈复制到堆,并持有所使用的self。self持有Block,Block 持有 self。这正是循环引用。
在这里插入图片描述
为避免此循环引用,可声明附有__weak 修饰符的变量,并将 self 赋值使用。

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


在这里插入图片描述
在该源代码中,由于Block存在时,持有该Block 的 MyObject 类对象即赋值在变量tmp中的 self 必定存在,因此不需要判断变量 tmp 的值是否为nil。
另外,还可以使用__block变量来避免循环引用。

typedef void (^blk_t)(void);
@interface MyObject : NSObject {
	blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
	self = [super init];
	
	__block id tmp = self;
	blk_ = ^{
		NSLog(@"self = %@", tmp);
		tmp = nil;
	};
	return self;
}
- (void)execBlock {
	blk_();
}
-(void)dealloc {
	NSLog(@"dealloc");
}
@end

int main() {
	id o = [[MyObject alloc] init];
	[o execBlock];
	return 0;
}


该源代码没有引起循环引用。但是如果不调用execBlock实例方法,即不执行赋值给成员变量blk_的 Block,便会循环引用并引起内存泄漏。在生成并持有MyObject 类对象的状态下会引起以下循环引用。

  • MyObject 类对象持有Block
  • Block 持有__block变量
  • __block变量持有MyObject类对象
  • 在这里插入图片描述

如果不执行 execBlock 实例方法,就会持续该循环引用从而造成内存泄漏。
通过执行 execBlock实例方法,Block 被实行,nil 被赋值在__block变量tmp中。

 blk_ = ^{
	NSLog(@"self = %@", tmp);
	tmp = nil;
};


因此,__block 变量 tmp 对 MyObject 类对象的强引用失效。避免循环引用的过程如下所示:

  • MyObject类对象持有 Block
  • Block 持有__block变量
    在这里插入图片描述
    下面我们对使用block变量避免循环引用的方法和使用__weak修饰符及__unsafe_unretained 修饰符避免循环引用的方法做个比较。
    使用__block变量的优点如下:
  • 通过__block 变量可控制对象的持有期间
  • 在不能使用__weak修饰符的环境中使用__unsafe_unretained 修饰符即可(不必担心悬垂指针)
    在执行 Block 时可动态地决定是否将 nil 或其他对象赋值在__block 变量中。使用__block变量的缺点如下:
  • 为避免循环引用必须执行Block
    存在执行了 Block 语法,却不执行 Block 的路径时,无法避免循环引用。若由于 Block 引发了循环引用时,根据 Block 的用途选择使用__block变量、__weak 修饰符或__unsafe_unretained修饰符来避免循环引用。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值