基本用法
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
//最简单的调用方式:
^{
NSLog(@"this is a block");
}();
//一般通过将其存起来的方式进行调用
void ^(block) (void) = ^{
NSLog(@"this is a block");
};
block();
}
return 0;
}
block的本质
- block本身也是一个OC对象,它内部也有一个isa指针
- block是封装了函数调用以及函数调用环境的OC对象
- block内部有两个基本的东西:
- 一个是
impl
存放着isa
指针,代表block是什么类型的,impl
还存放着FuncPtr
,指向了我们将来要执行的函数地址。 - 另外一个是
Desc
,是block的描述信息,比如block的大小。 - 其他信息则为要捕获的信息。
- 一个是
block底层代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^ {
NSLog(@"Hello World!");
};
block();
}
return 0;
}
我们先通过终端 cd
到程序 main.m
的目录下,执行通过执行命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
将其转换成 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) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5l_0xn052bn6dgb9z7pfk8bbg740000gn_T_main_88f00d_mi_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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
我们把强制转换部分的代码去掉,于是 main
函数里最终会变成这样:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//定义block变量
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
//执行block内部的代码
block->FuncPtr(block);
}
return 0;
}
变量捕获
int age = 10;
static int height = 10;
void (^ block)(void) = ^{
NSLog(@"age is %d",age);
NSLog(@"height is %d",height);
}
age = 20;
height = 20;
block();
上面的结果age是10,height是20,因为在定义block的时候age的值已经被捕获进来了,而height传递的是地址
- 为了保证block内部能够正常访问外部的变量,block有个变量捕获的机制,目前的结论是只要是基本类型变量,一定会进行变量捕获(待定)
之所以这两种变量的捕获方式有区别是因为auto类型的变量可能会被释放,内存会消失,所以要捕获到内存里,而 static
的变量是一直存放在内存中的。
- 局部变量分为两种,一种是
auto
,另外一种是static
,我们定义的局部变量默认是auto
类型的,auto
可以省略,被称为自动变量,离开作用域就销毁,auto只存在于局部变量里 - 捕获到内部就是block内部会自动生成一个变量来存储刚才那个值。
- 之所以会这样是因为局部变量在使用完成后内存就被回收了,为了防止block访问局部变量的时候变量被回收,故要将局部变量的值捕获进block内部了,而static修饰的局部变量还是在内存中存放的,所以可以直接通过地址访问,如果是全局变量则不用捕获。
#import
int age = 10;
static int height = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^ {
NSLog(@"age is %d, height is %d",age , height);
};
age = 20;
height = 20;
block();
}
return 0;
}
这种类型下,输出结果是:age
是20,height
也是20;
auto变量的捕获结构如下图所示:
void (^block)(void) = ^{
NSLog(@"------%p",self);
}
block();
- 在上面的代码里
self
也是有被捕获的,因为self
属于局部变量,是否有捕获只需要关注是局部变量还是全局变量即可。如果是_name
或者也是从self
里进行捕获的,self
是作为默认的参数传递给block的,即使不写self
默认也是有传递的。
block类型
- block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block1)(void) = ^ {
NSLog(@"Hello World!");
};
block();
NSLog(@"%@", [block1 class]);
NSLog(@"%@", [[block1 class] superClass]);
NSLog(@"%@",[[[block1 class] superClass] superClass]);
int age = 10;
void(^block1)(void)=^{
NSLog(@"Hello World!, %@", age);
};
NSLog(@"%@", [block2 class]);
NSLog(@"%@",[ ^{
NSLog(@"Hello World!, %@", age);
} class]);
}
return 0;
}
- __NSGlobalBlock__ (_NSConcreteGlobalStackBlock)
- __NSStackBlock__ (_NSConcreteStackBlock)
- __NSMallocBlock__ (_NSConcreteMallocBlock)
以下图片展示的内存地址依次是由低到高:
- 程序区域也称为
代码段
,平时我们编写的代码都是放在这个区域的 -
数据区域
一般用于放全局变量,GlobalBlock放在这个区域 -
堆
一般用于放我们alloc
和malloc
出来的东西,堆
是动态分配内存的,特点是内存需要开发者代码去申请,也需要程序员自己管理内存,MallocBlock放在堆区
栈
用于存放一些局部变量,特点是系统自动分配内存,而且会自动销毁内存,离开大括号即作用或就销毁,StackBlock则放在栈区 -
代码段
和数据区域
这两个也是由编译器决定好的,一旦代码编译成功则会被放在这两个地方,如果是代码也放在第一个,如果是全局变量则放在第二个。 - 函数调用栈即在栈区开辟一块区域给函数用,使用完即被回收。
如果想要知道一个对象存放在内存中的什么位置,可以使用如下方法,看它跟哪个对象的地址值相对即可推测出来
#import
#import "AppDelegate.h"
int age = 10;
int main(int argc, char * argv[]) {
@autoreleasepool {
int a = 20;
NSLog(@"数据段:age %p",&age);
NSLog(@"栈:a %p",&a);
NSLog(@"堆:obj %p", [[NSObject alloc]init]);
NSLog(@"class %p",[AppDelegate class]);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
确定block属于哪种类型根据下图:
但是验证这一步骤的时候要将Build Settings -> automatic Reference Counting
中将YES必为NO才可以,也就是切换成非ARC模式下。放在栈上的block有个问题,其内部访问的变量可能早就被释放了,栈上的block经过copy
之后就会变成放在堆上。global类型的block经过copy
后还是global类型的,什么也不做,堆
上的block经过copy
后引用计数增加,还是在堆
上。
block的copy
- 在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
- block作为函数返回值时
- 将block赋值给__strong指针时
- block作为Cocoa API中方法名含有usingBlock的方法参数时
- block作为GCD API的方法参数时
参考如下:
#import
#import "AppDelegate.h"
typedef void (^ myBlock) (void);
myBlock blockTest(void)
{
int age = 10;
myBlock block = ^{
NSLog(@"----%d",age);
};
return block;
}
int main(int argc, char * argv[]) {
@autoreleasepool {
//情况1
myBlock block = blockTest();
block();
NSLog(@"%@",[block class]);
//情况2
int age = 10;
myBlock block1 = ^{
NSLog(@"----%d",age);
};
NSLog(@"%@",[block1 class]);
//情况3
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
//情况4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
对象类型的auto变量
我们先来看下在ARC环境下这段代码里 person
对象何时释放
#import
typedef void (^ myBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJBlock block;
{
Person *person = [[Person alloc]init];
person.age = 10;
block = ^{
NSLog(@"---------%d",person.age);
};
//[person release]; //MRC时加上这句
}
NSLog(@"--------");
//block();
}
return 0;
}
上面这段代码,在 NSLog
调用的时候,person
对象是没有被释放的,但是在 MRC
下,加上上面的 [person release]
以后,在 NSLog
调用的时候,person
对象已经被释放,原因是在MRC上时,这个block是放在栈上的,而在ARC下此时block是存放在堆上的,MRC时对block拷贝后,person对象也不再释放,总结:存放在栈空间的block是不会保住person对象的命的,堆空间的block可以
- 弱引用这种技术需要运行时的支持,因此如果在ARC模式下把OC代码转为C++代码时,可能会遇到以下问题:
can not create __weak reference in file using manual reference
。解决方案:支持ARC,指定运行时系统版本,比如:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
下面的操作是在 ARC
下
-
当block内部访问了对象类型的auto变量时
- 如果block是在栈上,将不会对auto变量产生强引用,因为block自己可能随时被释放。
-
如果block被拷贝到了堆上(如果block被强指针引用着则自动会进行copy操作)
- 会自动调用block内部的
copy
函数 - copy函数内部会调用
_Block_object_assign
函数,这个函数会根据auto
变量的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,类似于retain
(形成强引用、弱引用)
- 会自动调用block内部的
-
如果block从堆上移除
- 会自动调用block内部的
dispose
函数 - dispose函数会调用
_Block_object_dispose
函数,_Block_object_dispose
函数会自动释放引用的auto
变量,类似于release
- 会自动调用block内部的
函数 | 调用时机 |
---|---|
copy函数 | 栈上的Block复制到堆时 |
dispose函数 | 堆上的Block被废弃时 |
block如何在内部修改局部变量的值
默认情况下,block只可以读取外面的变量的值,不能修改值,如下面代码
#import
typedef void (^ myBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
myBlock block = ^{
//加上这句代码就编译不通过
//age = 20;
NSLog(@"age is %d",age);
};
block();
}
return 0;
}
- 将局部变量声明为static(变量永久在内存中),如
static int age = 10;
- 声明为全局变量
- 加__block,如下所示
#import
typedef void (^ myBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
myBlock block = ^{
age = 20;
NSLog(@"age is %d",age);
};
block();
}
return 0;
}
__block修饰符
-
__block
可以用于解决block内部无法修改auto
变量值的问题 -
__block
不能修饰全局变量、静态变量(static
) - 编译器会将
__block
修饰的(基本)变量包装成一个对象(内部有isa
指针),通过指针找到结构体,对象内部有这个基本类型变量,通过指针来访问这个变量,之前不加__block
的时候是被捕获进block内
__block 会在编译时候将修饰的变量包装成一个对象。我们来验证一下,我们把以下代码转成 C++ 代码:
__block int age = 18;
void (^block)(void) = ^{
NSLog(@"Hello Block %d", age);
};
block();
可观察 int 类型的 age 被 __block 包转成了一个对象:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding; // 指向自己的指针
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
...
}
下面这种情况是不需要加 __block
的,这种是使用 array
这个指针,并不是修改 array
这个指针
#import
typedef void (^ myBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [NSMutableArray array];
myBlock block = ^{
[array addObject:@"123"];
//这种情况才需要添加 __block
//array = nil;
};
block();
}
return 0;
}
__block内存管理
基本类型的变量默认是分配在栈上,block默认也是在栈上的,在ARC环境下,一旦block被强指针引用着,会对在栈上的block进行copy操作,并且对block内部用到的__block修饰的变量也拷贝到堆上,而且对该变量形成的是强引用
- 当block在栈上时,并不会对__block变量产生强引用
- 当block被copy到堆时:
- 会调用block内部的
copy
函数 -
copy
函数内部会调用_Block_object_assign
函数 -
_Block_object_assign
函数会根据所指向的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,形成强引用(retain
)或者弱引用(注意:这里仅限于ARC时会retain
,MRC时不会retain
)
- 会调用block内部的
- 当block从堆中移除时
- 会调用block内部的
dispose
函数 -
dispose
函数会调用_Block_object_dispose
函数, -
_Block_object_dispose
函数会自动释放引用的__block
变量(release
)
- 会调用block内部的
- 当block在栈上时,并不会对
__block
修饰的变量和无__block
修饰的基本类型变量产生强引用 - 当block拷贝到堆上时,都会通过
copy
函数来处理它们:- __block变量(假设变量名叫做a),
_Block_object_assign((void *)&dst -> a
,(void *)src -> a, 8)
; - 对象类型的
auto
变量(假设变量名叫做p),_Block_object_assign((void *)&dst -> p
,(void *)src -> p, 3)
;
- __block变量(假设变量名叫做a),
- 当
block
从堆上移除时,都会通过dispose
函数来释放它们- __block变量(假设变量名叫做a)
-
_Block_object_dispose((void *)src -> a, 8)
;
- 对象类型的auto变量(假设变量名叫做p)
-
_Block_object_dispose((void *)src -> p, 3)
;
-
对象 | BLOCK_FIELD_IS_OBJECT |
---|---|
__block变量 | BLOCK_FIELD_IS_BYREF |
对象类型的auto变量、__block变量
总结:
- 当block在栈上时,对它们都不会产生强引用
- 当block拷贝到堆上时,都会通过 copy 函数来处理它们:
- __block变量(假设变量名叫做a)
- _Block_object_assign((void)&dst->a,(void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
- 对象类型的auto变量(假设变量名叫做p)
- _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_0BJECT/);
- 当block从堆上移除时,都会通过djspose函数来释放它们
_block变量(假设变量名叫做a)
_Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void)src->p, 3/BL0CK_FIELD_IS_0BJECT/);
对象
BLOCK_FIELD_IS_OBJECT
block变量
BLOCK_FIELD_IS_BYREF
__block的__forwarding指针
__block修饰的对象类型
#import "Person.h"
#import
typedef void (^ myBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc]init];
myBlock block = ^{
NSLog(@"%@",person);
};
block();
}
return 0;
}
-
__block __weak
不能用来修饰基本类型变量,但是可以修饰对象类型
Person *person = [[Person alloc]init];
__block __weak Person *weakPerson = person;
block循环引用
代码如下所示:
typedef void (^ myBlock) block;
@interface Person : NSObject
@property (assign, nonatomic) int age;
@property (copy, nonatomic) myBlock block;
@end
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
person.ag = 10;
person.block = ^{
//这里即使只写_age也是会产生循环引用的,因为_age存在于person内部
NSLog("age is %d",person.age);
};
}
return 0;
}
解决循环引用问题 - ARC
- 用
__weak
、__unsafe_unretained
解决,如下所示:
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
person.ag = 10;
__weak Person *weakPerson = person;
//或者用这种方式:__weak typeof(person) weakPerson = person; 其中 typeof(person) 就等于 Person *
//还有__unsafe_unretained Person *weakPerson = person; 这种方式
//__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil
//__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变
person.block = ^{
//这里即使只写_age也是会产生循环引用的,因为_age存在于person内部
NSLog("age is %d",weakPerson.age);
};
}
return 0;
}
- 用
__block
来解决,这种方式的弊端是必须要执行block,如下所示:
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc]init];
person.ag = 10;
person.block = ^{
NSLog("age is %d",weakPerson.age);
person = nil;
};
person.block();
}
return 0;
}
解决循环引用问题 - MRC
- MRC下虽然weak不能用,但是__unsafe_unretained是可以使用的,所以可以使用它解决循环引用问题。
//用__unsafe_unretained解决
__unsafe_unretained id weakSelf = seif;
self.block = ^{
NSLog(@"%p",weakSelf);
};
//用__block解决
__block id weakSelf = self;
self.block = ^{
printf("%p",weakSelf);
};
面试题
- block的原理是怎样的?本质是什么?
- __block的作用是什么?有什么使用注意点?
- block的属性修饰词为什么是copy?使用block有哪些使用注意?
- block在修改NSMutableArray时,需不需要添加__block?