iOS之Block深度学习

Block基础回顾

1.什么是Block?

带有局部变量的匿名函数(名字不重要,知道怎么用就行),差不多就与C语言中的函数指针类似,可以当做参数传来传去,而且可以没有名字。

2.Block语法完整的形式的Block语法如下,格式一

Block语法.png|center

并且与一般的C语言函数定义相比,仅有两点不同:

  • 没有函数名
  • 带有"^"符号所以根据前面的语法格式可以写出如下例子:
    ^int(int count) {return count+1};

当然,也可以有很多的省略格式,省略返回值如下图:

省略返回值.png|center

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

省略返回值类型时,如果表达式中有return语句时,block语句的的返回值类型就使用return返回的类型;如果return中没有返回类型,就使用void类型。再省略参数列表,参数列表和返回值都省略是最简洁的方式,同时将参数和返回值省略如下图:

都省略.png|center

^{printf("good!");}

3.Block类型变量在Block语法下,一旦使用了Block语法就相当于生成了可赋值给Block类型变量的值,"Block"既指源代码中的Block语法,也指由Block语法所生成的值即:

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

从上面看出,Block确实代表了一种语法,但在这里,对于blk,他也是一个Block类型变量的值。但是,当Block作为函数的参数或者返回 值的时候若传来传去,写法上难免有点复杂,毕竟都是那么长一串儿,此时,就可以像C语言那样使用typedef了:

typdef int(^blk_t)(int);

这时,blk_t就变成了一种Block类型了,例如:

typef int(^blk_t)(int);
blk_t bk = ^(int count){return count+1};//很明显省略了返回值

4.截获的自动变量(自动变量==局部变量)

通过前面的知识,我们已经大部分理解了Block了,这里引入截获的自动变量,什么是截获的局部变量?先看一段代码:

int main() { int dmy = 256; int val = 10; const char *fmt = "val = %d\n"; void (^blk)(void) = ^{printf(fmt, val);}; val = 2; fmt = "These values were changed. val = %d\n"; blk(); return 0; }
执行结果:val = 10

解释:在该源代码中,Block语法的表达式使用的是它之前声明的自动变量fmt 和val.Block语法中,Block表达式截获的自动变量,级保存该自动变量瞬间的值。因为Block表达式保存了自动变量的值,所以在执行Block语法之后,即使概念Block中的自动变量的值也不会影响Block执行时自动变量的值。这就是所谓的截获

5._ _block修饰符咱们来尝试着,在Block中修改自动变量的值:

int val = 0; void (^blk)(void) = ^{val = 1;}; blk(); printf("val = %d\n", val);
执行结果:error: variable is not assignable (missing __block type specifier) void (^blk)(void) = ^{val = 1;};~~~ ^

很显然,光这样的话是不允许在Block内部修改外面的自动变量的值的。如果强势要改呢,所以这会儿就该_ _block出场了:若想在Block语法的表达式中将赋值给在Block语法外声明的自动变量,需要在该自动变量上加上 _ _block修饰符,如下:

__block int val = 0; void (^blk)(void) = ^{val = 1;}; blk(); printf("val is %d",val);
执行结果:val is 1

所以,使用_ block修饰的变量,就可以在Block语法内部进行修改了,该变量称为 _block变量。但这里还有另一种情况,见如下代码:

id array = [[NSMutableArray alloc] init]; void (^blk)(void) = ^{id obj = [[NSObject alloc] init]; [array addObject:obj]; };

这会出错吗?其实是不会的,咱们在这里是没有向arry赋值,向他赋值才会产生编译错误,在这里,咱们截获到了NSMutableArray类对象的一个结构体指针(后面会讲),咱们没有对它赋值,只是使用而已,所以不会出错。

Block的实现

1.Block的实质

Block看上去看特别,但实际上是作为极普通的C语言源代码来处理的,通过支持Block的编译器,含有Block语法的源代码转换为一般C语言编译器能够处理的代码,并作为极普通的C语言代码来编译。概念上可以这样理解,但在实际编译时无法转化为我们能够理解的源代码,但是Apple的LLVM具有转换为我们可读源代码的功能转换为C++.说是C++,其实也是使用了struct的结构,其本质还是C语言.

下面引入在《Pro Multithreading and Memory Management for iOS and OS X with ARC, Grand Central Dispatch, and Blocks》中提到的一段源代码:

int main() { void (^blk)(void) = ^{printf("Block\n");}; blk(); return 0; }
经过转换后:
struct __block_impl
{
void *isa;
int Flags;
int Reserved;
void FuncPtr;
};
struct __main_block_impl_0
{
struct __block_impl imply;
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 *)bulk)->FuncPtr)((struct __block_impl *)bulk);
return 0; }

几个重要的的已用MarkDown下面的代码格式标记了出来,略过所有的讲解过程,直接抛出结论:

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

  • 1.将Block语法生成的Block值赋值给Block类型的变量blk,实质上是将struct __main_block_impl_0结构体实例的指针赋给变量blk,该源代码中的Block就是struct __main_block_impl_0结构体类型的自动变量,即在栈上生成的__main_block_impl_0结构体实例。
  • 2.上面的struct __block_impl,struct __main_block_desc_0,__main_block_func_0都是为了__main_block_impl_0构造函数或者成员变量而准备的参数。
  • 3.struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;}结构体包含了两部分,impl对应在内存中的实现,desc对应于实现所需要的资源。

将struct __main_block_impl_0 结构体全部展开,将其明显的显示出来:

truct __main_block_impl_0 { void *isa; int Flags; int Reserved; void *FuncPtr; struct __main_block_desc_0* Desc; }该结构体根据构造函数会想下面这样进行初始化:
isa = &_NSConcreteStackBlock; Flags = 0; Reserved = 0; FuncPtr = __main_block_func_0; Desc = &__main_block_desc_0_DATA;
blk();

//调用的时候,转为其在C++下面的实现其实是:

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

至此,基本上已经摸清了Block的实质,唯一还有个未解决的问题就是isa = &_NSConcreteStackBlock,所以,下面来解释解释

2.&_NSConcreteStackBlock , 类与对象的runtime关系

为了解决这个问题,首先得搞清楚Runtime下类和对象的实质,其实Block就是Objective-C对象。"id"这一变量类型用于存储ObjectIve-C对象,在ObjectIve-C源代码/usr/include/objc/runtime.h中如下声明:

typedef struct objc_object { Class isa; } *id;

//再看看class

typdef struct objc_class *ClassClass为指向objc_class结构体的指针类型
objc_class结构体在/usr/include/objc/runtime.h中如下声明:>struct objc_class { Class isa;};

所以objc_class中也有一个指向自己所代表的类,“Objective-C中由类生成对象”,意味着,对于结构体来说,类对应的结构体应该“生成由该类生成的对象的结构体实例”,通过每个结构体的成员变量isa保持该类的结构体实例指针,即维持类与对象的关系链:


关系链.png

各类的结构体就是基于objc_class结构体的class_t结构体(前面提到的objc_class结构体不是指的类所对应的结构体,别混了),接下来看看class_t结构体在obj4y运行时库的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; };

在ObjectIve-C中,比如NSObject的class_t的结构体实例以及NSMtableArray的class_t结构体实例等,均能生成保持各个类的class_t结构体实例。该实例持有声明的成员变量,方法的名称,方法的实现(即函数指针),属性以及父类的指针,并被Objective-C运行时库所使用。所以这里简单的提了一下类与对象,这个时候就可以继续回到我们之前想解决的问题了,看一下Block对应于内存中的结构体:

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

是不是和之前的对象结构体很类似,其实,此struct __main_block_impl_0结构体就是相当于之前Objective-C对象的结构体objc_object,另外对其成员变量的isa做了一个初始化:

isa = &_NSConcreteStackBlock;

根据之前的类比,这个_NSConcreteStackBlock是不是就相当于class_t结构体实例了呢。也就是说,在将Block作为Objective-C的对象处理时,关于类的信息就在_NSConcreteStackBlock中。所以就理解Block也是ObjectIve-C对象了。

3.截获自动变量值的实现

回到之前最开始将截获自动变量值的时候那个例子:

int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
val = 2;
fmt = "These values were changed. val = %d\n";
blk();
return 0;
}

同样,转换一下代码:

struct __main_block_impl_0
{
struct __block_impl imply;
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->fat;
int val = __cself->val;
printf(fmt, val);
}
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()
{
int dmy = 256;
int val = 10;
const char fmt = "val = %d\n";
void (
blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val);
return 0;
}

__main_block_impl_0结构体内声明的成员变量类型与自动变量类型完全相同,但是注意Block语法表达式中没有使用的自动变量不会追加,如此源代码中的变量dmy:如下,所以只有fmt,和,val

struct __main_block_impl_0 { struct __block_impl imply; struct __main_block_desc_0* Desc; const char *fmt;int val;};
查看 __main_block_impl_0 的构造函数:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "val = %d\n";
val = 10`

由此可知,在main_block_impl_0结构体实例中,自动变量值被截获。总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例中

4.__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);}

从代码中我们可以发现,block在截获自动变量之后,函数体部分只会改变在Block结构体实例中的那个副本,并不会改变原先截获的那个自动变量。在详细介绍__block之前,其实我们还有很多种方式来改变block之外的变量例如:静态局部变量,静态全局变量,全局变量,实现过程由于篇幅限制就只说明一下结果:

1.对于全局变量来说,改变肯定是没有问题的,block根本不需要截获,在改变的时候直接修改即可。
2.对于静态局部变量,是需要截获的,只是截获之后,截获的是该静态变量的指针,后来改变的时候直接通过指针本身来改变原值。
3.我们依然要使用__block描述的原因是,对于静态局部变量来说(其余两种方式无影响),即使是静态局部变量,在局部变量超过其处理域的时,是要被废弃掉的,但是对于Block来说,在由Block语法生成的Block上,是可以存超过其变量作用域名而存在的变量的。所以导致的结果就是,Block中存着变量的地址,但地址所对应的变量本身已经被废弃,所以就不能通过指针来访问原数据了。

接下来,终于可以引进__block说明符了:
大伙都知道C语言中的存储域说明符,如 extern , static ,auto ,register均用于指定变量值设置到哪个存储区中,例如,auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区中,其实__block说明符也是类似于这几个,用于指定变量存在哪个存储区。下面我们来实际使用__block说明符:

__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 imply;
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); } static void __main_block_dispose_0(struct __main_block_impl_0src)
{
_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(struct __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结构体实例,并且在这个结构体中也持有相当于原自动变量的成员变量把该结构体单独提出来:

struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; };

然后,将__block变量赋值的代码进行做的变化:

^{val = 1;}转化成:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; (val->__forwarding->val) = 1; }

通过构造函数也可以看到,__Block_byref_val_0结构体的成员变量__forwarding持有指向该实例自身的指针,通过成员变量__forwarding访问成员变量val(成员变量是该实例自身持有的变量,它相当于原自动变量)


Block_byref_val_0.png

总结:也就是说,加了__block修饰符的变量,就与之前的不痛了,该变量就变成了一个结构体,通过指向自己的指针再来改变对应值。

5.Block存储域( _NSConcreteStackBlock,_NSConcreteGlobalBlock,_NSConcreteMallocBlock)

通过前面的学习,了解到Block转换为Block的结构体类型的自动变量,__block修饰符修饰的变量转换为block变量的结构体类型的自动变量,所谓结构体类型的自动变量,即栈上生成的该结构体的实例变量。:

名称 实质
Block 栈上Block的结构体实例
__block变量 栈上__block变量的结构体实例

根据咱们之前提到的,其实Block也是一种对象,并且Block的类是_NSConcreteStackBlock,和他类似的还有两个如:

  • _NSConcreteMallocBlock
  • _NSConcreteGlobalBlock三个不同的类名称决定了三个Block类生成的Block对象存在内存中的位置:

| 类名 | 对象的存储域 |
| :--------: |: --------:|
| _NSConcreteStackBlock | 栈上 |
|_NSConcreteMallocBlock|堆上|
|_NSConcreteGlobalBlock|程序的数据区域(.data区)|
到目前为止,出现的Block例子都是_NSConcreteStackBlock类,所以都是设置在栈上,但实际上并非是这样,在记述全局变量的地方使用Block语法时生成的Block为_NSConcreteGlobalBlock对于Block对象分配在数据区的情况,略过分析过程,直接总结:

  • 当把Block声明为全局变量的时候,Block分配在数据区:void (^blk)(void) = ^{printf("Global Block\n");}; int main() {}
  • Block语法表达式中不使用截获的自动变量的时候:typedef int (^blk_t)(int); for (int rate = 0; rate < 10; ++rate) { blk_t blk = ^(int count){return count;}; }

以上两种情况,Block分配在数据区。最后一个问题,什么时候Block会分配在堆上呢?此时可以引出之前说的一个问题,“Block可以超出变量作用域而存在”,换句话说就是,Block倘若作为一个局部变量存在,结果他居然可以在超出作用域之后不被废弃,同样的,由于__block修饰的变量也是放在栈上的,如果其变量作用域结束,那么__block修饰符修饰的变量也应该结束。解决方案如下:
将Block和__block修饰的变量从栈上复制到堆上来解决,将配置在栈上的Block复制到堆上,这样即使Block语法记述的变量作用域结束时,堆上的Block还可以继续存在


栈复制到堆.png

复制到堆上之后,将Block内部的isa成员变量进行改变:

impl.isa = &_NSConcreteMallocBlock;

当ARC有效时,大多数情况下编译器会进行恰当地进行判断,自动生成将栈上复制到堆上的代码,并且最后复制到堆上的Block会自动的加入到autoRealeasePool中,编译器不能进行判断的情况便是:

  • 向方法或函数的参数中传递Block时但是在向方法或函数的参数中传递Block时也有不需要手动复制的情况如下:
  • Cocoa框架的方法且方法名中含有usingBlock等时
  • GCD中的API

举个栗子:在使用NSArray类的enumeratObjectsUsingBlock实例方法以及dispatch_async函数时,不用手动复制,相反的,在NSArray类的initWithObjects实例方法上传递时需要手动复制,看代码:

- (id) getBlockArray { int val = 10; return [[NSArray alloc] initWithObjects: ^{NSLog(@"blk0:%d", val);}, ^{NSLog(@"blk1:%d", val);}, nil]; }接下来,调用:
`id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];

  blk();` 结果就是Block在执行时发生异常,应用程序强制结束,这是由于在getBlockArray函数执行结束时,栈上的Block被废弃的缘故。而此时编译器恰好又不能判断是否需要复制。 注:但将Block从栈上复制到堆上是相当消耗CPU的,当Block设置在栈上也能够使用时,将Block从栈上复制到堆上只是在浪费CPU资源,能少复制,尽量少复制。

将以上代码修改一下即可运行:

- (id) getBlockArray { int val = 10; return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk0:%d", val);} copy], [^{NSLog(@"blk1:%d", val);} copy], nil]; }

小结:

Block的类 副本源的配置存储域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序数据区域 什么都不做
_NSConcreteMallocBlock 引用计数增加

6. __block变量存储域(Block移动对__block变量的影响)

使用__block变量的Block从栈复制到堆上时,__block修饰的变量也会受到影响。

__block变量的配置存储域 Block从栈复制到堆时的影响
从栈复制到堆并被Block持有
被Block持有
  • 1.多个Block使用一个__block变量时,因为会将所有的Block配置在栈上,所以__block变量也会配置在栈上,其中任何一个Block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该Block持有,当剩下的Block从栈复制到堆时,被复制的Block会依次持有__block变量,并增加__block变量的引用计数。
  • 2.在这里,读者可以采用Objective-C的引用计数的方式来考虑。使用block变量的Block持有__block变量,如果Block被废弃,它所持有的__block变量也就被释放在这里,回到之前讲到的“__block变量使用结构体成员变量__forwarding的原因”,不管__block变量配置在栈上还是在堆上,都能够正确的访问该变量(通过指针),通过Block的复制,__block变量也从栈复制到堆,此时可同时访问栈上的__block变量和堆上的__block变量,下面解释一下:看代码:

__block int val = 0; void (^blk)(void) = [^{++val;} copy]; ++val; blk(); NSLog(@"%d", val);
结果是:2
^{++val;}和++val;都可以转化为++(val.__forwarding->val);

在变换Block语法的函数中,该变量val为复制到堆上的__block变量结构体实例,而另外一个(++val)与Block无关的变量val,为复制前栈上的__block变量结构体实例。但是栈上的__block变量结构体实例在__block变量从栈复制到堆上时,会将成员变量__forwarding指针替换为复制目标堆上的__block变量用结构体实例的地址,如图:


block指针转换.png

下面总结栈上的Block复制到堆的情况:

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

在调用Block的copy方法时,如果Block配置在栈上,那么该Block会从栈上赋值到堆;将Block作为函数返回值时、将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时,编译器将自动地将对象的Block作为参数并调用_Block_copy函数,这与调用Block的copy实例方法的效果相同;在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时,在该方法或函数内部对传递过来的Block调用Block的copy实例方法或者_Block_copy函数。

推荐阅读更多精彩内容