Block的本质
- block本质上也是一个
OC对象
,它内部也有个isa指针 - block是封装了
函数调用
以及函数调用环境
的OC对象 - block的底层结构如下图所示
#举个栗子#
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"Hello World");
};
block();
}
return 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;
//定义block变量
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//执行block内部的代码
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 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) = (
&__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA)
);
block->FuncPtr(block);
}
return 0;
}
分析:
定义block就是调用一个__main_block_impl_0方法,
并且传入了两个参数__main_block_func_0和__main_block_desc_0_DATA
#__main_block_func_0 封装了block内部的实现#
#__main_block_desc_0_DATA 方法保存的是block占用内存大小信息#
#__main_block_impl_0函数方法的定义#
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 构造函数(类似于OC的init方法),返回结构体对象
__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;
}
};
struct __block_impl {
// block内存地址
void *isa;
// 固定值 0
int Flags;
// block 占用的内存大小
int Reserved;
// block内部的实现
void *FuncPtr;
};
Block变量捕获机制
#举个栗子#
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 平时定义的局部变量,前面都是有auto修饰词,只是不展示
// auto 自动变量,离开作用域就销毁
int age = 10;
// static 静态变量
static int weight = 20;
void (^testBlock)(void) = ^(){
// age 自动变量 捕获其值
// weight static静态变量 捕获其指针
NSLog(@"========= age: %d weight : %d",age, weight);
};
age = 20;
weight = 20;
testBlock();
}
return 0;
}
输出结果:
========= age: 10 weight : 20
- 再举个栗子:
在对象方法中调用block,并且block中有使用当前对象,比如
@interface Person : NSObject
@property (nonatomic, copy, readwrite) NSString *name;
- (instancetype)initWithName:(NSString *)name;
- (void)test;
@end
#import "Person.h"
@implementation Person
- (instancetype)initWithName:(NSString *)name {
if (self = [super init]) {
self.name = name;
}
return self;
}
- (void)test {
void (^block)(void) = ^{
NSLog(@"----- %p",self);
};
block();
}
@end
分析:对于block是否捕捉变量,第一步是确认这个变量是局部变量还是全局变量
(1)如果全局变量,则直接访问,不捕获
(2)局部变量,自动变量则捕捉其值,静态变量则捕获其指针
那么这个例子中,“self”到底是局部变量还是全局变量了?
clang文件编译,了解test方法编译后是什么样的
在OC中,对象方法编译之后,第一个参数和第二个参数都是固定(第一个是实例对象自己self,第二是当前的方法指针_cmd),比如上图中的self和_cmd,而方法的参数是局部变量,所以block中调用的"self"是一个局部变量,下图是block编译后的结构体,从中可以看到block捕获到的一个对象指针
Block类型
block 有
3
中类型,可以通过class方法或者isa指针查看具体类型,最终都是继承自NSBlock
类型
- NS
Global
Block:没有访问auto变量- NS
Stack
Block:访问了auto变量- NS
Malloc
Block:NSStackBlock调用了copy
(备注:
程序区域(.text)
: 存放的是代码段
数据区域(.data)
: 存放的是全局变量
堆
: 动态分配内存,需要程序员申请内存,也需要程序员自己管理内存(比如通过alloc分配的内存)
栈
: 存放局部变量,系统会自动分配和管理内存
)
备注:
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
- block作为函数返回值时
- 将block赋值给__strong指针时(所以ARC环境下,使用copy或者strong修饰都是一样的效果)
- block作为Cocoa API中方法名含有UsingBlock的方法参数时
- block作为GCD API的方法参数时
对象类型的auto变量
当block内部访问了对象类型的auto变量时
* 如果block是在栈上,将不会对auto变量产生强应用
* 如果block被拷贝到堆上
a:会调用block内部的copy函数
b:copy函数内部会调用_Block_object_assign函数
c:_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,类似于retain(形成强引用、弱引用)(注意:这里仅限于ARC时会retain,MRC时不会retain
)
* 如果block从堆上移除
a:会调用block内部的dispose函数
b:dispose函数内部会调用_Block_object_dispose函数
c:_Block_object_dispose函数会自动释放引用的auto变量,类似于release
__block修饰符
- __block 可以用于解决block内部无法修改auto变量值的问题
- __block 不能修饰全局变量、静态变量(static)
-
编译器会将__block变量包装成一个对象
block循环引用
解决循环引用问题
(1)ARC环境下
- 用__weak、__unsafe_unretained解决
__weak(首选)
:不会产生强引用(指向的对象销毁时,会自动让指针置为nil)
__unsafe_unretained:不会产生强引用,不安全(指向的对象销毁时,指针存储的地址不变)
-
用__block解决(必须调用block)
(2)MRC环境下
-
用__unsafe_unretained 解决
-
用__block解决
面试题
(1)block的原理是怎么样的?本质是什么?
封装了函数调用以及调用环境的OC对象
(2)__block的作用是什么?有什么使用注意点?
将__block变量包装成对象
注意点:当block在栈上,将不会对__block变量产生强引用
当block被copy到堆时,会对__block变量形成强引用(只有在ARC环境下会强引用,MRC不会)
(3)block的属性修饰词为什么是copy?使用block有哪些使用注意?
block一旦没有进行copy操作,内存就是在栈上,而不是在堆上(在堆上,生命周期由程序员控制)
使用注意:循环引用的问题
(4)block在修改NSMutableArray,需不需要添加__block?
不需要。修改内容只是对数组的使用,只有对对象赋值的时候才需要__block