在Objective-C:探索block(一)中简单地说了下block的基本上面貌,包括它的语法和底层定义,了解block的底层定义对在项目开发中正确使用block极为重要。
本篇文章要探索block使用中不可避免的几个方面
- 基本类型变量与对象的截取
- __block修饰符
- block存储域
- copy的使用
- 相互引用问题
一. 基本数据类型变量与对象的截取
1.block截获基本数据类型变量
在这小节会解答:为什么给定义在代码体外面的局部变量重新赋值会引起编译错误
下面是block截取基本数据类型的示例代码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int aInteger = 3;
char *str = "hello world";
void (^myprint)(void) = ^void(void){
printf("%s-%d\n",str,aInteger);
};
myprint();
}
return 0;
}
注意:由于控制台标准输出有缓存,当有行刷新标志或者行缓存已满时,系统才会把缓存数据输出到控制台,Xcode8编译printf打印函数结束加上\n换行符,后台才有打印输出
示例代码中,代码块myprint的代码体中使用了变量aInteger,str
,使用终端命令反编译:clang -rewrite-objc main.m
, 可以在main.cpp文件的底部看到代码块myprint的底层定义,主要有:
//1.存储与block实现相关的信息
struct __block_impl {
void *isa; //指向block结构体实例所在的内存区域
int Flags; //系统默认值为0
int Reserved; //构造方法里没看到赋值,用来存储block保留内存空间大小
void *FuncPtr; //指向block代码体实现的函数指针,block的调用关键就它来寻址了
};
//2.此结构体记录block的描述信息,它在定义时顺便初始化了个实例__main_block_desc_0_DATA
static struct __main_block_desc_0 {
size_t reserved; //指明block在内存中要保留一块内存空间的大小,这块内存区暂没用途。
size_t Block_size;//指明block的结构体实例的大小:sizeof(struct __main_block_impl_0)
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
//3.代码块myprint的真身,就是一个结构体
struct __main_block_impl_0 {
struct __block_impl impl; //存储与block实现(代码体)相关的信息
struct __main_block_desc_0* Desc;//存储block的描述信息
char *str; //截获到的str变量
int aInteger; //截获到的aInteger变量
//结构体的构造方法
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int _aInteger, int flags=0) : str(_str), aInteger(_aInteger) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//4.block代码体的实现函数,就是一个C函数,可以通过函数指针来调用,传入的参数为block的结构体实例本身
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//编译器在编译过程中会在block代码体的实现函数中,声明与截获的外部局部变量名称相同的局部变量,并进行赋值
char *str = __cself->str; // cself为block的结构体实例本身
int aInteger = __cself->aInteger; //
printf("%s%d\n",str,aInteger);
}
//程序入口,主函数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//这两个变量虽然与block的结构体实例的成员变量名称相同,但根本是不同,所以在block代码体的实现函数里给它们赋值,会编译报错:Variable is not assignable,因为超出了变量的作用域
int aInteger = 3;
char *str = "hello world";
/*-----------重点-----------*/
//1.void (*myprint)(void):声明一个返回类型为空,参数为空,名称叫myprint的函数指针,它指向了block的构造方法,构造方法将创建一个block结构体实例
void (*myprint)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, aInteger));
//2.(void (*)(__block_impl *))这是一个返回值为void,参数类型为__block_impl *的指针类型,用来修饰实例myprint的成员变量FuncPtr(即myprint->FuncPtr), ((__block_impl *)myprint)为FuncPtr的参数
((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);
}
//类型转换简化后,1和2相当于下面:
struct __main_block_impl_0 tmp = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA,str,aInteger);
struct __main_block_impl_0 *myprint = &tmp;
(*myprint->impl.FuncPtr)(myprint);
/*-----------重点-----------*/
return 0;
}
显然,代码块myprint经过编译后,代码块myprint底层结构体会新增成员变量aInteger,str
并在构造函数中进行初始化。同时可以看到代码块myprint的实现函数static void __main_block_func_0(struct __main_block_impl_0 *__cself)
中重新声明了相同名称的变量int aInteger和char *str
,此时的aInteget,str与声明在主函数的局部变量是两个完全没关系的不同变量,它们不在同一作用域(同一函数)并且编译器有它自己的一套编译规则(1.支持截获变量的瞬间值,2.不支持在代码体内给外部的局部变量赋值,因为不允许在block的实现函数里给main函数的局部变量赋值,尽管两个变量名称相同),这也解释为什么给定义在代码体外面的局部变量重新赋值会引起编译错误。想要给aInteger,str
重新赋值,可以把它们声明为全局变量或静态变量来解决作用域问题,但一般不选择这样做,而是在它们前面加上修饰符__block,这样就可以通过编译(将在第二小节探索)。
2.block截获对象
下面是block截取对象的oc源代码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
NSMutableArray *array = [[NSMutableArray alloc] init];
void(^myprint)(void)=^{
printf("数组Count=%ld\n",array.count);
};
myprint();
}
}
return 0;
}
示例中,myprint截获了可变数组array,以下是转换成C++的代码:
//代码块myprint的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *array; //截获的array
//代码块myprint结构体的构造方法
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock; //isa指向结构体实例本身,实例分配在栈区
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//myprint代码体对应的实现函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSMutableArray *array = __cself->array;
//array.count反编译成runtime的消息发送objc_msgSend(arry,@selector(count));
printf("数组Count=%ld\n",((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}
//拷贝操作函数,
//1.这个函数用来拷贝myprint结构体实例的array对象
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
//废弃操作函数,废弃myprint结构体实例的array对象,相当对象realse
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
//存储代码块myprint的结构体的描述信息并实例化一个实例__main_block_desc_0_DATA
static struct __main_block_desc_0 {
size_t reserved;
size_t 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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
{
//(NSMutableArray *(*)(id, SEL))把objc_msgSend(id,SEL)转成返回值为NSMutableArray *,参数为id,SEL的函数指针。
//简化如下:
NSMutableArray *aZone = objc_msgSend(objc_getClass("NSMutableArray"),sel_registerName("alloc"));
NSMutableArray *array = objc_msgSend(aZone,sel_registerName("alloc"));
//
NSMutableArray *array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
void(*myprint)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);
}
}
return 0;
}
由此可知,编译器对block截获对象与截获变量的处理基本一样:在block的结构体中生成一个与截获的变量名称相同的成员变量。与截获基本数据类型变量不同的是,截获对象时,编译器会生成block_copy
和block_dispose
函数用于对象的内存管理。
二. __block修饰符
1.编译器对__block的处理
我们知道在block代码体中不能给截获的变量重新赋值,但可以用__block来修饰被截获的变量,再进行重新赋值,示例如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int aInteger = 1;
void(^myprint)(void)=^{
aInteger = 3;
printf("aInteger=%d\n",aInteger);
};
myprint();
}
return 0;
}
为了给block截获的变量重新赋值,可使用__block修饰符,下面是编译器对加了__block修饰符的变量的处理,与上面反编译后的代码相差不大:
//编译器会为被__block修饰的变量生成一个结构体类型,
struct __Block_byref_aInteger_0 {
void *__isa;
__Block_byref_aInteger_0 *__forwarding; //__forwarding指向__block变量结构体实例本身
int __flags;
int __size;
int aInteger; //与外部的局部变量名称相同的成员变量
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_aInteger_0 *aInteger; // 截获的__block变量aInteger
//block结构体的构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_aInteger_0 *_aInteger, int flags=0) : aInteger(_aInteger->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; //函数指针指向block代码体的实现函数
Desc = desc;
}
};
//block代码体的实现函数,以block的结构实例本身作为参数传递
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//结构体aInteger的__forwarding指向自身,绕了一步再取成员变量int aInteger
__Block_byref_aInteger_0 *aInteger = __cself->aInteger;
(aInteger->__forwarding->aInteger) = 3;
printf("aInteger=%d\n",(aInteger->__forwarding->aInteger));
}
//block变量的copy函数用于对象的内存管理 (拷贝对象,引用计数+1)
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->aInteger, (void*)src->aInteger, 8/*BLOCK_FIELD_IS_BYREF*/);
}
//block变量的dispose函数用于对象的内存管理 (废弃对象)
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->aInteger, 8/*BLOCK_FIELD_IS_BYREF*/);}
//存储block的描述信息,在声明同时实例化一个__main_block_desc_0_DATA实例
static struct __main_block_desc_0 {
size_t reserved;
size_t 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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
{
//实例化aInteger结构体
__attribute__((__blocks__(byref))) __Block_byref_aInteger_0 aInteger = {
(void*)0 //void *isa
(__Block_byref_aInteger_0 *)&aInteger, //__forwardind
0,
sizeof(__Block_byref_aInteger_0),
1
};
//函数指针myprint指向myprint代码体的构造函数即指向了结构体实例
void(*myprint)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_aInteger_0 *)&aInteger, 570425344));
//下面去掉类型转换:(myprint->FuncPtr)(myprint),即结构体实例myprint引用成员变量FuncPtr指针,来调用block代码体的实现函数__main_block_func_0
((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);
}
}
return 0;
}
2.对象的内容可以进行操作
不能对没有__block修饰符修饰的变量进行重新赋值,但截获的变量如果是对象类型的话,是可以对其内容进行操作的,例如:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:2];
void(^myprint)(void)=^{
[mArray addObject:@"Hello"];
printf("mArray.count=%ld\n",mArray.count);
};
myprint();
}
return 0;
}
三. block存储域
下面是内存分配的一些说明:
一:声明在函数内部的局部变量,系统自动分配到栈上,且在函数执行结束后,被释放。
二:由程序员调用alloc,malloc,new,copy等方法初始化的变量,会分配到堆上,程序员可手动调用free来释放。否则,等程序结束后,由系统释放回收
三:一些静态变量,全局变量,常量会在编译阶段分配在数据 区(data区)
四:操作指令会分配到文本区(text区)
以下是一个应用在内存中进程空间的布局(线程thread共享进程的内存空间)
1.分配在栈上的block
声明在函数内部的block代码体会分配在栈上。
在上面的c++代码中,block的结构体实例构造方法会给impl.isa赋值为 &_NSConcreteStackBlock,我们知道isa指针是指向了block结构体实例自身,即&_NSConcreteStackBlock指向了block结构体实例在内存中的起址地址,NSConcreteStackBlock,见文知义,Stack是栈的意思,可以推测系统将block的结构体实例分配在栈上。
2.分配在堆上的block
当block代码体赋值给拥有strong,__strong,copy修饰的变量时,block会在堆上生成一份副本,并将副本赋值给变量,block结构体实例的impl.isa=&_NSConcreteMallocBlock,isa指针指向堆上的实例
3.分配在数据区(全局)的block
在全局地方声明的block代码体(在@interface外面声明),会分配在数据区,impl.isa=&_NSConcreteGlobalBlock 指向数据区。
四.copy的使用
1.为什么要用copy呢?
一般情况下,block的代码体会声明在某个方法内而极少声明在@interface外面(可理解为全局的地方,因数在全局地方block截获不了有用的变量),我们知道,在方法内声明的(不是由all,new初始化的)会分配到栈上,并且会在方法执行完结时被释放,这样就极容易引用了被释放的对象,从而抛出内存读取错误的异常,所以会为block声明一个copy属性,使它指向堆上的对象以确保其生命周期。
在项目开发,为某个类声明一个block属性时,通常会这样写:
typdef void (^Myblock)(void)
@interface MyClass
@property (copy,nonatomic) Myblock oneBlock;
...
{
self.oneBlock=^{ NSLog(@"Hello world!"); };
}
当为属性指明copy属性时,^{ NSLog(@"Hello world!"); }
的结构体实例将被从栈上copy一份副本到堆上,且_oneBlock指向了堆上的副本。其实,声明onBlock属性也可以这样写 @property (strong,nonatomic) Myblock oneBlock;
或者@property (nonatomic) Myblock oneBlock;
因为属性strong作用相当于修饰符__strong(__strong是对象的隐式说明),编译器会为__strong的对象分配到堆上作内存管理。但如果非要作死,写成@property (weak,nonatomic) Myblock oneBlock;
,就会报错了bad_address_access。
2.不用手动copy的情况
在一些情况下,出于内存管理的原因,编译器会自动帮我们copy, 不我们自己去copy,情况如下:
一:block作为函数的返回值被传递时,如
-(OneBlock)someMethod{
OneBlock aBlock = ^{...};
return aBlock; //不需要写成return [aBlock copy];
}
二:Cocoa框架方法或GCD的API方法中含有usingBlock来传递block时
三:将block赋值给带有__strong修饰符的id类型的类或__block说明符的变量。
五.相互引用问题
由于block会截获出现在代码体的外部对象(定义在代码体{}外部的对象),并在block的结构体内声明一个与之名称相同的成员变量并在实例化时指向并截获的外部对象,如果被截获的对象又拥有或间接拥有(持有)该block,就会形成闭环,引起相互引用的问题,不能被有效释放。
1.相互引用
引起循环引用的示例如下:
#import "MyClass.h"
typedef void(^Myprint)(void);
@interface MyClass ()
{
Myprint _myprint;
}
@end
@implementation MyClass
-(void)myPrintMethod{
_myprint = ^{
NSLog(@"%@",self); //编译器警告:Capturing 'self'strongly in this block is likely to lead to a retain cycle
};
}
@end
2.间接相互引用
#import "MyClass.h"
typedef void(^Myprint)(void);
@interface MyClass ()
{
Myprint _myprint;
}
@property(strong,nonatomic) NSString *str;
@end
@implementation MyClass
-(void)myPrintMethod{
_myprint = ^{
NSLog(@"%@",_str);//编译器警告:Capturing 'self'strongly in this block is likely to lead to a retain cycle
};
}
@end
3.__weak解决相互引用
形成相互引用的两个对象,不能被有效释放,dealloc方法也不会执行。
A对象要释放,首先要释放B,B要释放,那么A对象就要先释放,这样会形成死锁,导致A,B对象都不能释放。__weak修饰符或__unsafe_unretained修饰符可解决循环引用问题,示例如下:
#import "MyClass.h"
typedef void(^Myprint)(void);
@interface MyClass ()
{
Myprint _myprint;
}
@end
@implementation MyClass
-(void)myPrintMethod{
__weak typeof(self) weakSelf = self; //__weak修饰符让weak可引用self,但并不持有self
_myprint = ^{
NSLog(@"%@",weakSelf);
};
}
@end