什么是Blocks
Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
按理说,C语言的标准不允许存在这样的函数。匿名函数就是不带函数名称的函数,那么带有自动变量是什么意思呢?Blocks提供了类似由C++和OC类生成实例或对象来保持变量值的方法。如“带有自动变量值”,Blocks保持自动变量的值。同时Blocks也被称作闭包、lambda计算。OC的Block在其他程序语言中的名称如图所示:
Blocks模式
Block语法
- 没有函数名。
- 带有“^”。
第一点不同是没有函数名,因为它是匿名函数。第二点不同是返回值类型前带有“^”(插入记号,caret)记号。
BN范式,如图所示:
“返回值类型”同C语言函数的返回值类型,“参数列表”同C语言函数的参数列表,“表达式”同C语言函数中允许使用的表达式。当然与C语言函数一样,表达式中含有return语句时,其类型必须与返回值类型相同。
例如可以写出如下形式的Block语法:
^int (int count) {return count + 1;}
虽然前面出现过省略形式,但Block语法可省略好几个项目。首先是返回值类型。如图所示:
关于返回值类型,如果没有返回值则使用void类型,如果有返回值,无论return语句有多少个,所有return的返回值类型必须相同。
^void (void){printf("Blocksn");}
Block类型变量
在Block语法中,可将Block语法赋值给声明为Block类型的变量中。即源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”。Blocks中由Bloc语法生成的值也被称为“Block”。
Block类型变量仅仅是将声明函数指针类型变量的“*”变成了“^”。该Block类型变量与一般的C语言变量完全相同,可作为以下用途使用:
下面我们就试着使用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 = %dndmy = %dn",val,dmy);
};
val = 2;
dmy = 6;
blk();
return 0;
}
该源代码中,Block语法的表达式使用的是它之前声明的自动变量fmt和val。所以它的输出是2和256,也就是说Blocks中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。
__block 说明符
实际上,自动变量值截获只能保存执行Block语法瞬间的值。保存后就不能改写该值。
_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 = %dn", val);
该源代码运行结果为:
val = 1
使用附有__block说明符的自动变量可在Block中赋值,改变量称为__block变量。
Blocks的实现
Block的实质
Block的实质就是通过编译器,将Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为普通的C语言源代码被编译。
下面,我们转换Block语法:
int main() {
void (^blk)(void) = ^{printf("Blockn");};
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("Blockn");
}
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;
};
我们来看一下:
- void *isa:声明一个不确定类型的指针,用于保存Block结构体。
- int Flags:标识符。
- int Reserved:今后版本更新所需的区域大小。
- void *FuncPtr:函数指针,指向实际执行的函数,也就是block中花括号里面的代码内容。
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 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)};
- 第一个成员变量主要是指以后版本升级所需区域的大小(一般情况下为0)
- 第二个成员变量就是Block的大小
- __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("Blockn");
}
经过变换后的源代码可以看到,通过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
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。虽然该类并没有出现在已变换源代码中,但有三个与之类似的类,如:
这三个有啥区别呢?我们通过一个表来看一下:
我们可以看到不同的类有不同的设置对象的存储域。
接下来我们看一下应用程序中的内存分配情况:
如果Block用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此Blcok用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例。因此将Blcok用结构体实例设置在与全局变量相同的数据区域中即可。
只有截获自动变量时,Block用结构体实例截获的值才会根据执行时的状态变化。即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。
虽然通过clang转换的源代码通常是_NSConcreteStackBlcok类对象,但实际上却有不同,总结如下:
- 记述全局变量的地方有Block语法时
- 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
函数。
那么什么时候栈上的Block会复制到堆呢?
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中传递Block时。
截获对象时和使用__block变量时的不同
通过BLOCK_FIELD_IS_OBJECT
和BLOCK_FIELD_IS_BYREF
参数,区分copy函数和dispose函数的对象类型是对象还是__block变量。
但是与copy函数持有截获的对象dispose函数释放截获的对象相同,copy函数持有所使用的__block变量,dispose函数释放所持有的__block变量。
由此可见,Block中使用的赋值给所富有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可超出其变量作用域而存在。
Block中使用对象类型自动变量时,除以下情形外,推荐调用Block的copy实例方法:
- Block作为函数返回值返回时
- 将Block复制给类的附有__strong修饰符的id类型或Block类型成员变量时
- 向方法名中含有usingBlock的Cocoa框架方法或Grang Central Dispatch的API中传递的Block时
Block变量和对象
使用_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的区别?
- __strong是为了防止block持有的对象提前释放,__weak和__block是解决循环引用
- __block不管是ARC还是MRC都可以使用,可以修饰对象和基本数据类型
- __weak只能在ARC模式下使用,也只能修饰对象,不能修饰基本数据类型
- __block对象可以在Block中被重新赋值,__weak不可以
对于__block变量MRC如何解决循环引用?ARC如何解决?
- MRC解决循环引用用__block,禁止Block对所引用的对象进行retain操作.
- ARC时期__block并不能禁止Block对所引用的对象进行强引用。解决办法可以是在Block中将变量置空,因为需要在Block中对Block进行操作,所以还是需要__block修饰符
- __block在MRC下有两个作用:允许在BLock中访问和修改局部变量,禁止Block对所引用的对象进行retain操作
- ARC下仅仅只有一个作用
允许在BLock中访问和修改局部变量
iOS开发“强弱共舞——weak和strong配套使用解决block循环引用问题
总结:
原文地址:https://blog.csdn.net/weixin_51638861/article/details/123019708
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_32366.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!