「OC」小白书读书笔记——Block的相关知识(上)
前言
在之前的OC学习之中使用了很多关于Block的内容,包括但不限于网络请求后的回调操作,block的跨界面传值等,但是本人还是对block这个概念比较模糊,于是就在网上学习一些相关的知识进行补充
Block的底层实现
我们在main函数之中写下以下代码
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
printf("block~~~\n");
};
block();
}
return 0;
}
首先我们进入当前文件之中,使用命令行命令clang main.m -rewrite-objc -o dest.cpp
,将我们的main.m转化为c++文件,去除前置性的代码,我们以上的OC代码转化为以下内容
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) {
printf("block~~~\n");
}
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;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
我们暂且抛开那些复杂的修饰符,我们先看总体的程序本身
//这一段是构造函数,创建并返回一个对象,相当于OC之中的init
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
我们可以在文件之中找到它的定义
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
看了构造函数本身,再来看看构造函数的调用,我们直接化简得到
struct __main_block_imp1_0 tmp = __main_block_impl_0 ( __main_block_func_0, &__main_block_desc_0_DATA) ;
struct __main_block_imp1_0 *blk = &tmp;
//直接对应void (^blk) (void) - ^(printf ("Block\n*); };
假设我们这个block之中捕获了一个类型为int的age变量,这个block的C语言实现如下
//分装block之中的相关操作语句
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block~~~\n");
}
impl->isa
:就是isa指针,可见它就是一个OC对象。
impl->FuncPtr
:是一个函数指针,也就是底层将block中要执行的代码封装成了一个函数,然后用这个指针指向那个函数。
Desc->Block_size
:block占用的内存大小。
age
:捕获的外部变量age,可见block会捕获外部变量并将其存储在block的底层结构体中。
在main函数当中:
//调用函数返回block对象
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//执行创建block之后的FuncPtr方法
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
全局变量的捕获
我们给出以下代码
int c = 1000; // 全局变量
static int d = 10000; // 静态全局变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 局部变量
static int b = 100; // 静态局部变量
void (^block)(void) = ^{
NSLog(@"a = %d",a);
NSLog(@"b = %d",b);
NSLog(@"c = %d",c);
NSLog(@"d = %d",d);
};
a = 20;
b = 200;
c = 2000;
d = 20000;
block();
}
return 0;
}
// ***************打印结果***************
2020-01-07 15:08:37.541840+0800 CommandLine[70672:7611766] a = 10
2020-01-07 15:08:37.542168+0800 CommandLine[70672:7611766] b = 200
2020-01-07 15:08:37.542201+0800 CommandLine[70672:7611766] c = 2000
2020-01-07 15:08:37.542222+0800 CommandLine[70672:7611766] d = 20000
通过将代码转化为C++我们可以看到
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
};
block里面打印了四个变量,但是最后却只捕获了两个,这是怎么回事呢?
全局变量的捕获
因为全局变量,无论静态全局变量或者是普通全局变量,在哪里都可以被直接访问,所以在block内部即便是不捕获也是可以直接访问的,所以在我们通过block打印这些变量的值时,就是最新更改的值
静态局部变量的捕获
我们发现定义的静态局部变量b被block捕获后,在block结构体里面是以int *b;
的形式来存储的,也就是说block其实是捕获的变量b的地址,block内部是通过b的地址去获取或修改b的值,所以block外部更改b的值会影响block里面获取的b的值,block里面更改b的值也会影响block外面b的值。所以上面会打印b = 200
。
普通局部变量的捕获
所谓的普通局部变量就是在一个函数或代码块中定义的类似int a = 10;
的变量,它其实是省略了auto
关键字,等价于auto int a = 10
,所以也叫auto变量
。和静态局部变量不同的是,普通局部变量被block捕获后再block底层结构体中是以int a;
的形式存储,也就是说block捕获的其实是a的值(也就是10),并且在block内部重新定义了一个变量来存储这个值,这个时候block外部和里面的a其实是2个不同的变量,所以外面更改a的值不会影响block里面的a。所以打印的结果是a = 10
。
至于为什么使用一个变量直接存取,而不是跟静态局部变量一样用一个指针存储呢?其实道理很简单,就是普通局部变量出了main函数就会被释放。
那为什么静态局部变量一直都不会被释放,那block为什么还要捕获它,直接拿来用不就可以了吗?这其实就是在作用域外,我们无法直接从外面去访问这个静态局部变量。
变量捕获小结
- 全局变量–不会捕获,是直接访问。
- 静态局部变量–是捕获变量地址。
- 普通局部变量–是捕获变量的值。
那么我们来判断一下,以下代码中的block是否捕获了self
- (void)blockTest{
// 第一种
void (^block1)(void) = ^{
NSLog(@"%p",self);
};
// 第二种
void (^block2)(void) = ^{
self.name = @"Jack";
};
// 第三种
void (^block3)(void) = ^{
NSLog(@"%@",_name);
};
// 第四种
void (^block4)(void) = ^{
[self name];
};
}
其实要回答这个问题,实际上就是探究self
究竟是局部变量还是全局变量。我们可以联想到我们学习OC方法调用之中的objc_msgSend
,比如我调用[self blockTest]
,它转成C语言后就变成了objc_msgSend(self, @selector(blockTest))
。其实就不难看出我们是通过传参才能够使得方法能够正确获取调用对象。作为函数参数,self
存储在栈帧中,只在方法调用期间存在,并在方法执行完毕后自动销毁。
这个问题解决了,那上面几种情况就简单了,这4中情况下block都会捕获self
。
block类型总结
前面我们知道,block的结构体之中时候isa
指针的,这其实意味着,block其实是作为一个类存在的,那我们就来探究一下block的类。
以其他人博客之中为例子:
- (void)test{
int age = 10;
void (^block1)(void) = ^{
NSLog(@"-----");
};
NSLog(@"block1的类:%@",[block1 class]);
NSLog(@"block2的类:%@",[^{
NSLog(@"----%d",age);
} class]);
NSLog(@"block3的类:%@",[[^{
NSLog(@"----%d",age);
} copy] class]);
}
// ***************打印结果***************
block1的类:__NSGlobalBlock__
block2的类:__NSStackBlock__
block3的类:__NSMallocBlock__
__NSGlobalBlock__
如果一个block之中没有访问任何变量,那这个block就是__NSGlobalBlock__
。__NSGlobalBlock__
类型的block在内存中是存在数据区
的(也叫全局区或静态区,全局变量和静态变量是存在这个区域的)。__NSGlobalBlock__
类型的block调用copy方法的话什么都不会做
。其实就是相当于一个单例。我们简单的探究一下这个类的继承链
- (void)test{
void (^block)(void) = ^{
NSLog(@"-----");
};
NSLog(@"--- %@",[block class]);
NSLog(@"--- %@",[[block class] superclass]);
NSLog(@"--- %@",[[[block class] superclass] superclass]);
NSLog(@"--- %@",[[[[block class] superclass] superclass] superclass]);
}
// ***************打印结果***************
--- __NSGlobalBlock__
--- __NSGlobalBlock
--- NSBlock
--- NSObject
__NSStackBlock__
如果一个block里面访问了普通的局部变量,那它就是一个__NSStackBlock__
,顾名思义,它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,都是由系统管理释放操作的,所以在调用__NSStackBlock__
类型block时要注意,一定要确保它还没被释放。
- (void)test{
int age;
void (^block)(void) = ^{
NSLog(@"----%d",age);
};
NSLog(@"--- %@",[block class]);
NSLog(@"--- %@",[[block class] superclass]);
NSLog(@"--- %@",[[[block class] superclass] superclass]);
NSLog(@"--- %@",[[[[block class] superclass] superclass] superclass]);
}
// ***************打印结果***************
--- __NSStackBlock__
--- __NSStackBlock
--- NSBlock
--- NSObject
__NSMallocBlock__
一个__NSStackBlock__
类型block做调用copy
,那会将这个block从栈复制到堆上,堆上的这个block类型就是__NSMallocBlock__
,所以__NSMallocBlock__
类型的block是存储在堆区。如果对一个__NSMallocBlock__
类型block做copy
操作,那这个block的引用计数+1。那其实就是一个在堆区指向栈区内存的指针
__NSMallocBlock__
的继承链是:__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject
。
以下四种操作自动将栈上的 block 复制到堆上
1. Block 作为函数/方法的返回值时
当一个 block 作为返回值从一个方法返回时,编译器会自动复制它到堆上。例如:
typedef void (^MyBlock)(void);
- (MyBlock)createBlock {
int a = 10;
return ^{
NSLog(@"****** %d", a);
};
}
说明:
- 虽然代码中看起来直接返回的是一个栈上 block,但因为它是返回值,所以编译器会在返回前自动调用 copy 操作,将其从栈复制到堆上。
2. 将 Block 赋值给强引用(strong)时
如果你将一个栈上的 block 赋值给一个使用 strong 修饰符的变量,编译器同样会自动复制到堆上。例如:
- (void)test {
int a = 10;
// 定义一个 block,并赋值给一个强指针变量 myBlock
void (^myBlock)(void) = ^{
NSLog(@"a = %d", a);
};
myBlock(); // 调用 block
}
说明:
- 当 block 被赋值给一个 strong 类型的变量时,ARC 会确保 block 的内存安全,因此自动将其复制到堆上。
3. 当 Block 作为参数传给 Cocoa API 时
许多 Cocoa API 都要求传入的 block 必须是堆上的 block。如果你传入一个栈上的 block,系统会自动对其进行复制。例如:
[UIView animateWithDuration:1.0f animations:^{
// 这里传入的 block 会被自动复制到堆上
}];
说明:
- Cocoa 框架内部会对传入的 block 做 copy 操作,从而保证异步动画执行期间 block 不会失效。
4. 当 Block 作为参数传给 GCD(Grand Central Dispatch)API 时
GCD 的 API 也要求 block 必须在堆上,这样才能在异步执行中长期有效。例如:
dispatch_async(dispatch_get_main_queue(), ^{
// 传递给 GCD 的 block 会自动从栈复制到堆上
});
说明:
- 在调用诸如 dispatch_async 等 GCD API 时,编译器会自动将 block 复制到堆上,以便在调度队列中安全使用。
区别
我们想要分清_NSConcreteGlobalBlock
和_NSConcreteStackBlock
,小白书还给出了一个有趣的例子
例子 a:捕获自动变量
typedef int (*blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
blk_t blk = ^(int count) {
return rate * count;
};
}
- 解释:
在这个例子中,Block 内部使用了rate
这个变量。由于rate
是 for 循环的局部变量,每次循环时rate
的值不同,所以每个 Block 实例都需要捕获当前的rate
值。 - 结果:
尽管 Block 的语法看起来一样,但因为捕获了不同的局部变量值,所以在每次循环中生成的 Block 实例都是不同的,它们属于 _NSConcreteStackBlock 类型(或者当被复制到堆上后,可能是 _NSConcreteMallocBlock)。
例子 b:不捕获自动变量
typedef int (*blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
blk_t blk = ^(int count) {
return count;
};
}
- 解释:
这个例子中,Block 内部没有使用任何外部的自动变量(如rate
),因此不需要捕获任何上下文。 - 结果:
这种 Block 的内容在编译时就已经固定下来,可以放置在程序的数据区中,与全局变量类似。整个程序中,只需要一个 Block 实例,这个 Block 的类别为 _NSConcreteGlobalBlock。
Block 属性在 MRC 与 ARC 下的写法区别
在 MRC 环境下
-
建议使用 copy
因为在 MRC 中,栈上的 block 不会自动复制到堆上,所以在定义 block 属性时必须使用 copy 关键字:@property (copy, nonatomic) void(^block)(void);
原因:
- 只有 copy 属性能确保 block 被复制到堆上,否则可能会出现因栈内存释放而导致的崩溃或未定义行为。
在 ARC 环境下
-
copy 与 strong 均可
在 ARC 环境中,无论你使用 copy 还是 strong 修饰 block 属性,编译器都会自动将栈上的 block复制到堆上:@property (strong, nonatomic) void (^block)(void); @property (copy, nonatomic) void (^block)(void);
说明:
- 两种写法都能保证 block 在赋值时正确复制到堆上,因此在 ARC 下二者效果相同。不过,为了表明语义(即 block 需要复制),我们还是仍然倾向于使用 copy。
总结
本文大致讲了一些block在OC底层用C的实现,用了例子大致的了解了block捕获变量的底层机制,以及block三种类型的具体区别。
因为小白书之中的block内容大多数都是探究其OC的底层实现,不太熟悉,还是有些费时费力,先将一部分整理出来,巩固一下再总结下半部分的笔记吧