#iOS底层原理 - Block本质探究ios
block 本质是一个OC对象,也存在 isa 指针。或者说Block 是封装了函数调用和函数调用环境的OC对象。c++
编写一段最简单的OC代码顶一个block,代码如:程序员
int main(int argc, const char * argv[]) {
@autoreleasepool {
int abc = 10086;
void(^block)(int number) = ^(int number) {
NSLog(@"%d",number);
};
}
return 0;
}
复制代码
使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
命令将其OC代码转化为底层的C++代码,观察block的底层结构。objective-c
咱们打开编译生成的main.cpp代码,会发现上述代码被转化为以下:bash
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int abc = 10086;
void(*block)(int number) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
}
return 0;
}
复制代码
block 代码块被定义为 __main_block_impl_0
结构体。iphone
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;
}
};
复制代码
结构体中包含两个不一样的结构体变量 __block_impl
和 __main_block_desc_0
函数
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
复制代码
包含isa指针说明,block本质上也是一个OC对象,FuncPtr 指向block所封装的代码块地址,等执行block时会经过FuncPtr寻找将要执行的代码块,而且调用。ui
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
复制代码
其中: Block_size 为当前block 占用内存大小。atom
block 封装的代码块被定义为 __main_block_func_0
结构体spa
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int number) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gf_ct0sq2w17s16j4b1pz5_zx500000gn_T_main_68909d_mi_0,number);
}
复制代码
若是咱们将main函数改成:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int abc = 10086;
void(^block)() = ^() {
NSLog(@"%d",abc);
};
abc = 10010;
block();
}
return 0;
}
复制代码
在block内部引用外部变量,咱们再看看内部组成结构。一样执行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
命令。 咱们能够看到,较以前,__main_block_impl_0
结构体新增一个 int 类型变量abc,用于存储所引用的外部变量的值。由于是值存储,因此在block生成以后,不管外部变量作何更改,abc依然是以前所定义的值。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int abc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _abc, int flags=0) : abc(_abc) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
}
复制代码
由于咱们所定义的外部变量 abc 以前没有任何修饰符,也就是默认的auto变量,此时block是值捕获。若是将外部变量声明为 static 类型再观察底层实现。
int main(int argc, const char * argv[]) {
@autoreleasepool {
int abc = 10086;
static int def = 100;
void(^block)(void) = ^() {
NSLog(@"abc: %d - def: %d",abc,def);
};
abc = 10010;
def = 200;
block();
}
return 0;
}
复制代码
转化为c++底层实现为:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int abc;
int *def;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _abc, int *_def, int flags=0) : abc(_abc), def(_def) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
使用static 修饰的变量在block内部为指针传递,block直接捕获外部变量的内存地址,此时若外部变量在block声明以后修改,block内部也会同步进行修改。
若是使用全局变量,block不会捕获。由于声明全局变量的类型会在程序的整个声明周期都不会被释放,因此在使用block时,直接会去访问全局变量的值。因此捕获就没有意义了,感兴趣的能够自行查看底层实现。
当咱们声明一个block 而且打印他的继承链咱们能够看到:
void(^block)(void) = ^() {
NSLog(@"abc");
};
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
复制代码
输出:
2018-06-28 10:37:23.901162+0800 BlockDemo[17574:719984] __NSGlobalBlock__
2018-06-28 10:37:23.901504+0800 BlockDemo[17574:719984] __NSGlobalBlock
2018-06-28 10:37:23.901522+0800 BlockDemo[17574:719984] NSBlock
2018-06-28 10:37:23.901535+0800 BlockDemo[17574:719984] NSObject
Program ended with exit code: 0
复制代码
从而也进一步证实了block 本质上为 OC对象。而且,在不引用外部变量的状况下,block为 NSGlobalBlock
类型。
咱们定义三个不一样的block,分别打印他们的实际类型:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block1)(void) = ^() {
NSLog(@"abc");
};
NSLog(@"%@",[block1 class]);
int abc = 1;
void(^block2)(void) = ^() {
NSLog(@"abc: %d",abc);
};
NSLog(@"%@",[block2 class]);
NSLog( @"%@", [^(){
NSLog(@"hello %d",abc);
} class]);
}
return 0;
}
复制代码
输出:
2018-06-28 10:48:32.096859+0800 BlockDemo[17719:728991] __NSGlobalBlock__
2018-06-28 10:48:32.097224+0800 BlockDemo[17719:728991] __NSMallocBlock__
2018-06-28 10:48:32.097243+0800 BlockDemo[17719:728991] __NSStackBlock__
复制代码
咱们能够得出结论,
__NSGlobalBlock__
__NSMallocBlock__
__NSStackBlock__
他们在内存中位置分别:
那他们是如何区分的呢?可使用以下表格来讲明:
Block 类型 | 条件 |
---|---|
NSGlobalBlock | block 内部没有访问auto变量 |
NSStackBlock | block 内部访问了 auto变量 |
NSMallocBlock | NSStackBlock 调用了copy |
NSStackBlock 执行 copy 后会将栈区的block 复制到堆区,便于程序员管理,那其余类型的block执行 copy 会有什么变化呢?以下表所示:
Block 类型 | 存储域 | 执行 copy 后效果 |
---|---|---|
NSGlobalBlock | 程序的数据区域 | 无任何改变 |
NSStackBlock | 栈 | 从栈复制到堆 |
NSMallocBlock | 堆 | 引用计数器加1 |
typedef void(^MyBlock)(void);
MyBlock testFunc() {
int a = 10;
MyBlock myBlock = ^ {
NSLog(@"test --- %d",a);
};
return myBlock;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myB = testFunc();
NSLog(@"%@",[myB class]);
}
return 0;
}
复制代码
若是此代码在MRC 环境下,会崩溃。Block访问的变量已被释放。 若是在ARC环境下,在参数的返回值为block时,系统会对block自动执行一次 copy 操做,使其变为 NSMallocBlock 类型。
int main(int argc, const char * argv[]) {
@autoreleasepool {
int abc = 10;
MyBlock myB = ^ {
NSLog(@"+++ %d",abc);
};
NSLog(@"%@",[myB class]);
}
return 0;
}
复制代码
如上代码,在MRC环境输出:__NSStackBlock__
。在 ARC环境输出:__NSMallocBlock__
例如:
NSArray *array = @[@1];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
复制代码
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
复制代码
因此在 MAC 环境下的block属性必须使用 copy 修饰,而ARC环境下的block属性便可使用 strong 修饰,也可使用 copy 修饰,二者都会对block自动执行copy操做,故无任何区别。
观察以下代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
XWPerson *person = [[XWPerson alloc] init];
person.age = 10;
^{
NSLog(@"person -- %ld",(long)person.age);
}();
}
NSLog(@"*******");
}
return 0;
}
复制代码
会发现当 函数体内 大括号执行完毕后 XWPerson 即被释放,此时的block 是 栈类型的Block 即 __NSStackBlock__
. 存储在栈区的block即使引用了对象,也会跟随大括号一并释放。
若是将以上代码改成:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock myBlock;
{
XWPerson *person = [[XWPerson alloc] init];
person.age = 10;
myBlock = ^{
NSLog(@"person -- %ld",(long)person.age);
};
myBlock();
}
NSLog(@"*******");
}
return 0;
}
复制代码
咱们会发如今执行到 **** 时,person 对象依然没有被释放,此时block 已经对 person 对象进行了强引用。由于 此时 的block 为强指针引用,类型为 堆block __NSMallocBlock__
. 为何堆 block 会对外部对象强引用呢?
此时 使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
命令 观察其底层 c++ 实现:
此时block 定义为:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
XWPerson *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, XWPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
其中 main_block_desc_0 定义为:
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*);
}
复制代码
较以前block引用基本成员类型时,其 main_block_desc_0 多了两个参数分别为 copy 和 dispose。而且传入的都是 __main_block_impl_0
block 自己。
当 block 执行 copy 操做的时候,执行的是
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
复制代码
方法。最终调用的 _Block_object_assign
方法会对block引入的对象 person 进行引用计数操做,当所引入的对象使用 strong 修饰则使其引用计数加1,若使用weak修饰则引用计数不变。
当 block 执行完毕的时候会调用 dispose 方法,而dispose 在底层会调用
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
复制代码
方法,将block内部引用的对象成员引用计数减1,若是此时外部对象使用strong 修饰,引用计数在copy加1后 此时再减1.依然会强引用外部对象,不会释放,若是使用weak修饰,此时由于自身以及被释放,因此不会再持有所引用外部对象,然而此时所引用外部对象是否会被释放取决于它的引用计数是否为 0。
咱们知道,若是block 内部捕获的外部变量为 auto 类型,在block 内部生成的是该变量的值类型变量,没法经过block内部的值修改外部变量。 若是想在block内部修改外部变量的值有几种方法?
使用 static 修饰的变量block内部会直接获取到变量的内存地址,能够直接修改。
若使用 static 变量修饰,该变量的生命周期就会无限延长,这不符合咱们的设计思路,故咱们可使用 __block
来修饰外部变量,从而达到在block内部修改外部成员变量的目的。 那 __block
是如何实现此需求的呢?
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
MyBlock block = ^{
a = 20;
NSLog(@"a --- %d",a);
};
block();
}
return 0;
}
复制代码
使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
命令将其转化为c++ 实现:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
咱们可知 经过 __block
修饰的外部成员变量被定义为 __Block_byref_a_0
对象!它的声明为:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
复制代码
此时在main函数内声明 __block
类型的变量会以此方式初始化:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
复制代码
其中 __forwarding
保存的原变量 a 的内存地址,size
为当前变量的内存大小,10 保存未原变量的值。
如此,咱们在block 内部修改 原变量时:
(a->__forwarding->a) = 20;
复制代码
直接取原变量的地址进行更改,从而实如今block内部更改外部变量。
对于block内部捕获的对象类型的auto变量和__block修饰的变量。若是block在栈区,不会对他们进行内存管理,即不会强引用外部变量
若是block被复制到堆区,则会调用内部 copy 函数对外部 __block 修饰的变量和对象类型的auto变量进行内存管理。
当block从内存中移除时,一样也会调用dispose函数对所引用的外部变量进行释放。
使用 block 很容易造成循环引用,若是一个类中定义的block内部引用了该类的外部属性,包括 类自己的 self, 均会致使 self 强引用 block,block 也强引用 self。致使self不会被释放。以下代码就会形成循环引用:
.h
#import <Foundation/Foundation.h>
@interface XWPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) void(^personBlock)(void);
@end
复制代码
.m
#import "XWPerson.h"
@implementation XWPerson
- (void)test {
self.personBlock = ^{
NSLog(@"%d",self.age); //此处若即使使用 _age 也会产生循环引用。
};
}
- (void)dealloc {
NSLog(@" XWPerson -- dealloc -- age:%ld",(long)_age);
}
@end
复制代码
产生循环引用的本质缘由是,在block内部实现里,会将self 捕获到block内部,而且strong 强引用。以下代码所示:
struct __XWPerson__test_block_impl_0 {
struct __block_impl impl;
struct __XWPerson__test_block_desc_0* Desc;
XWPerson *const __strong self;
__XWPerson__test_block_impl_0(void *fp, struct __XWPerson__test_block_desc_0 *desc, XWPerson *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
XWPerson *person1 = [[XWPerson alloc] init];
person1.age = 18;
__weak typeof(person1) weakPerson = person1;
person1.personBlock = ^{
NSLog(@"%ld",(long)weakPerson.age);
};
person1.personBlock();
}
return 0;
}
复制代码
__unsafe_unretained XWPerson *person1 = [[XWPerson alloc] init];
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block XWPerson *person1 = [[XWPerson alloc] init];
person1.age = 18;
person1.personBlock = ^{
NSLog(@"%ld",(long)person1.age);
person1 = nil;
};
person1.personBlock();
}
return 0;
}
复制代码