前言
- 本文只是作为自己对学习的一个总结,如果有错误的地方欢迎各位大神提出
- 文笔不行,可能会写的有点乱,请见谅!
先看几个面试题
- block内部会捕获到外部局部变量吗?那static修饰的呢?全局变量呢?
- block的原理是怎样的?本质是什么?
- __block的使用注意点?
开始block
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void(^block)(void) = ^{
NSLog(@"%d",a);
};
block();
}
return 0;
}
定义一个简单的block,然后通过clang指令编译成c++代码
clang -rewrite-objc main.cpp
生成了大概10W行C++代码,我们直接找重点
struct __block_impl {
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;
int a;
//构造函数,实例化block结构体,传入fp(block代码块函数)到impl这个结构体中
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_rq5pw07x2615y7592pjt2__c0000gn_T_main_a0acf2_mi_0,a);
}
//Block_size:通过sizeof(struct __main_block_impl_0)获取block的大小
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;
int a = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
定义block &__main_block_impl_0()
把结构体的地址赋值给block变量
__main_block_func_0
:存储block代码块的执行函数
__main_block_desc_0_DATA
:存放block信息的结构体(内存大小)
a
: block内部访问的外部变量
其实block就是一个__main_block_impl_0这个结构体,在定义的时候传入block代码块的执行函数,和在block内部访问的局部变量等信息,并在构造的时候把执行函数保存到__block_impl里面的FuncPtr
在执行block的时候,通过block对象找到FuncPtr进行调用
接下来修改一些代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void(^block)(void) = ^{
NSLog(@"%d",a);
};
a = 20;
block();
}
return 0;
}
控制台打印结果: 10;
为什么会这样呢?我明明是修改了变量a
的值;
因为block内部只是对变量a
的值进行了捕获,在__main_block_func_0
中
int a = __cself->a; //bound by copy
只是把原来存储在block结构体中的值赋值给了一个新的变量,所以在block外部修改变量的值,block内部的变量值不受影响
修改代码
static int static_Global = 10;
int global = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int a = 10;
int autoA = 10;
void(^block)(void) = ^{
a = 30;
static_Global = 20;
global = 30;
NSLog(@"autoA = %d,a= %d ,static_Global = %d,global =%d",autoA,a,static_Global,global);
};
a = 20;
block();
}
return 0;
}
分别定义了全局变量global
,全局静态变量static_Global
, 局部变量 autoA
, 局部静态变量a
,接下来看看编译后的代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *a;
int autoA;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int _autoA, int flags=0) : a(_a), autoA(_autoA) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *a = __cself->a; // bound by copy
int autoA = __cself->autoA; // bound by copy
(*a) = 30;
static_Global = 20;
global = 30;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_rq5pw07x2615y7592pjt2__c0000gn_T_main_4217e2_mi_0,autoA,(*a),static_Global,global);
}
从上面代码中可以看出,block内部并没有把全局变量和全局静态变量进行捕获,因为是全局的,作用域很广,block内部可以获取到直接进行修改,而局部静态变量和局部变量是进行了捕获,静态变量捕获的是地址,而局部变量捕获的是值,都作为了__main_block_impl_0
这个结构提的成员变量
小节一下: block会对在其内部用到的局部变量和局部静态变量进行捕获,局部变量捕获的是值,静态局部变量捕获的是地址,所以可以在其内部修改静态局部变量和全局变量,却不能对局部变量进行修改,并且只对其内部使用到的局部变量或者局部静态变量进行捕获
block的类型
在OC中一般情况下就分为以下三种block
__NSMallocBlock__
,__NSGlobalBlock__
,__NSStackBlock__
- NSGlobalBlock : 没有访问外部的局部变量(访问全局变量或者全局静态变量也是如此)
- NSStackBlock : 内部访问了外部局部变量,系统自动管理内存
- NSMallocBlock : NSStackBlock类型的block调用了copy操作,或者有强指针指向,需要程序员自己管理内存
在这里解答一下:block的本质是什么?
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class]superclass]);
NSLog(@"%@",[[[block class]superclass]superclass]);
NSLog(@"%@",[[[[block class]superclass]superclass]superclass]);
我们发现block可以调用class方法,而且还是NSObject的子类,所以说block的本质就是一个对象啊
__blcok
我们试着对block内部的局部变量进行修改,直接就会编译报错"Variable is not assignable (missing __block type specifier)"
如果我们换成static int age = 10;
这样就可以在block内部进行修改了,因为用static修饰的变量传递的是age的地址
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
还有一种就是用__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
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_np_rq5pw07x2615y7592pjt2__c0000gn_T_main_82c4e5_mi_0,(age->__forwarding->age));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
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;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
从上面的代码中我们看到用__block修饰的局部变量生成了一个结构体__Block_byref_age_0
,
__block int age = 10;
就编译成了上面的样子,并把值都传进了结构体中;
在定义block的时候,把上面定义的结构体指针传递给了
__main_block_impl_0
,并进行初始化;
调用block的时候,通过
block->FuncPtr((__block_impl *)block)
,找到block的执行函数,并把block的结构体指针传递过去;
block的执行函数
__main_block_func_0
,根据
__cself
结构体指针找到结构体中的
age(__Block_byref_age_0)
;
通过
age(__Block_byref_age_0)
结构体指针找到
__Block_byref_age_0
结构体,再找到
__forwarding
,再找到其中的
age
成员,对他进行修改
我们通过下面的代码进行验证
__forwarding
指向的到底是哪
typedef void(^MyBlock)(void);
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __Block_byref_age_0 {
void *__isa;
struct __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;
struct __Block_byref_age_0 *age; // by ref
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
//运行在ARC环境下
__block int age = 10;
NSLog(@"%p",&age);
MyBlock block = ^{
age = 20;
NSLog(@"%p",&age);
};
struct __main_block_impl_0 *newBlock = (__bridge struct __main_block_impl_0 *)block;
NSLog(@"%p",&age);
block();
}
return 0;
}
控制台打印
我们可以看到第一次打印的和后面打印的地址是不一样的,因为在ARC下block被强指针引用或者赋值给__strong类型,都会被copy到堆上,__forwarding
指针也就指向了被copy到堆上的新地址,所以会出现上面的打印地址不同的情况.
newBlock
中的age地址是0x1007519a0
,可以根据这个去计算一下
struct __Block_byref_age_0 {
void *__isa; // 8 0x1007519a0
struct __Block_byref_age_0 *__forwarding;//8 0x1007519a0+8=0x1007519a8
int __flags; //4 0x1007519a8+8=0x1007519b0
int __size; //4 0x1007519b0+4=0x1007519b4
int age;// 4 0x1007519b4+4=0x1007519b8
};
从上面可以看出在block内部和最下面的&age
取的是__Block_byref_age_0
这个结构体里面的int age
的地址;
但是上面的代码跑在MRC的环境下控制台打印就不一样了,每次打印的地址都是一样的,说明__forwarding
一直指向的是自己,一直在栈上,如果调用了copy操作,还会出现上面的情况
那什么情况下才会把栈上的block拷贝到堆上呢?
- 栈上的block手动调用copy函数
- block作为函数的返回值
- block被强引用,或者赋值给__strong修饰的类型
block 的copy和dispose
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->objc, (void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);}
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};
_Block_object_assign和_Block_object_dispose就对应着retain和release方法;
看下面的代码:
// 运行在MRC
auto NSObject *objc = [[NSObject alloc]init];
NSLog(@"%lu",(unsigned long)objc.retainCount);
void(^block)(void) = ^{
NSLog(@"%lu",(unsigned long)objc.retainCount);
};
NSLog(@"%lu",(unsigned long)objc.retainCount);
block();
如果block在栈上不会对objc产生强引用,打印结果一直为1
如果block调用了copy就会被copy到堆上,就会调用block内部的__main_block_copy_0
函数,__main_block_copy_0
函数内部会调用
_Block_object_assign函数,根据objc变量的修饰符(__strong
、__weak
、__unsafe_unretained
),做出相应的操作,形成强指针或者弱指针
如果block要销毁,会调用block内部的dispose函数,函数内部会调用
__main_block_dispose_0
函数,调用_Block_object_dispose
进行release操作;
注意哦如果上面的对象使用了__block进行修饰,引用计数是不会+1的
NSObject *objc = [[NSObject alloc]init];
NSLog(@"%@",[objc valueForKey:@"retainCount"]);
void(^block)(void) = [^{
NSLog(@"%@",[objc valueForKey:@"retainCount"]);
} copy];
NSLog(@"%@",[objc valueForKey:@"retainCount"]);
block();
MRC下:打印结果 1,2,2
ARC下: 打印结果 1,3,3
添加上__block以后;
MRC下:打印结果 1,1,1
ARC下: 打印结果 1,1,1
从上面的结果可以看出:
- __block 不会让对象的引用计数发生改变
- ARC和MRC下copy函数起到的作用是不一样的,具体什么原因,有知道的朋友可以留言,谢谢
block的循环引用
@interface Person : NSObject
@property (nonatomic, copy) void(^block)(void);
@property (nonatomic, assign) int age;
@end
@implementation Person
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
Person *p = [[Person alloc]init];
p.block = ^{
NSLog(@"%d",a);
};
p.block();
}
return 0;
}
在出了作用域以后,Person对象得到释放
修改代码
p.block = ^{
NSLog(@"%d",p.age);
};
Person对象没有得到销毁,因为循环引用
通过上图可以看出,存在互相引用,所以内存就得不到释放
int a = 10;
Person *p = [[Person alloc]init];
p.age = a;
__weak Person *person = p;
p.block = ^{
NSLog(@"%d",person.age);
};
p.block();
通过__weak,把捕获到block内部的对象指针改变给弱指针就ok了
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
以上就是我学习block的过程,如果有不正确的地方欢迎指出和交流
接下来解决一下上面的面试题
- block内部会捕获到外部局部变量吗?那static修饰的呢?全局变量呢?
block内部会捕获外部的局部变量和static修饰的局部变量,不会对全局变量和全局静态变量进行捕获 - block的原理是怎样的?本质是什么?
block主要就是把执行block需要用的数据进行封装(比如:外部变量,block的执行函数),其实block的本质就是一个OC对象 - __block的使用注意点?
使用block最主要的就是防止循环引用,可以使用__weak进行操作