对于Block的使用,相信大家都不会很陌生。但是对于其底层的原理可能了解的就不够透彻了,本章节来全面讲解一下Block相关知识
基本概述
概念
闭包 = 一个函数「或指向函数的指针」+ 该函数执行的外部的上下文变量「也就是自由变量」
Block 是 Objective-C 对于闭包的实现。
简单来说,block就是将一些代码封装起来,以便在将来某个时候被使用,如果你不去调用block,block内部封装的代码就不会执行。
使用
block的使用格式如下:
1.普通使用
返回值类型(^block的名称)(参数类型)=^返回值类型(参数){...};
// 无参数无返回值
void(^MyBlockOne)(void) = ^(void){
NSLog(@"无参数,无返回值");
};
MyBlockOne();//block的调用
// 有参数无返回值
void(^MyblockTwo)(int a) = ^(int a){
NSLog(@"@ = %d我就是block,有参数,无返回值",a);
};
MyblockTwo(100);
// 有参数有返回值
int(^MyBlockThree)(int,int) = ^(int a,int b){
NSLog(@"%d我就是block,有参数,有返回值",a + b);returna + b;
};
MyBlockThree(12,56);
// 无参数有返回值(很少用到)
int(^MyblockFour)(void) = ^{NSLog(@"无参数,有返回值");
return45;
};
MyblockFour();
2.当做属性使用
@property (nonatomic, copy) 返回值类型 (^block的名称)(参数类型);
//有返回值有参数的属性
@property (nonatomic, copy) int (^MyBlock)(NSString *name);
//没返回值没参数的属性
@property (nonatomic, copy) void (^MyBlock1)();
3、作为方法声明的参数
-(void)方法名:(返回值类型 (^)(参数类型))block的名称;
-(void)myBlock:(int(^)(NSString *name))completion;
4、实际开发中常用typedef 定义Block
typedef 返回值类型 (^类型名称)(参数类型);
typedef int (^MyBlock)(int , int);
这时,MyBlock就成为了一种Block类型
在定义类的属性时可以这样:
@property (nonatomic,copy) MyBlock myBlockOne;
在定义方法时可以这样:
-(void)myBlock:(MyBlock)completion;
Block的底层结构
首先我们通过clang命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
将下面代码编译成C++代码,看其底层的实现
void(^block)(void) = ^{
printf("MyBlock");
};
block();
转换后代码:
int main(){
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;
}
Block的定义
可以看到转化为的主要代码如下,在C++代码中,(void (*)()表示强制转换,去掉相关代码后,可以发现定义block时,主要就是调用了__main_block_impl_0构造函数
接下来查看__main_block_impl_0函数的定义,我们发现其内部其实就是进行了函数式的保存,转换为了__block_imp,最终会返回一个同名的结构体对象。
也就是说在定义block时,会调用__main_block_impl_0构造函数,并将生成的同名结构体指针赋值给block。
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;
}
};
接下来我们看一下传入的两个参数分别代表的是什么意思。
1. __main_block_func_0
寻找源码,我们可以发现__main_block_func_0就是封装了我们代码块中执行的相关逻辑,也就是说这是对代码块逻辑的一层封装。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("MyBlock");
}
2. &__main_block_desc_0_DATA
通过源码,我们可以发现其调用__main_block_desc_0_DATA构造函数,返回了一个结构体,里面主要是包含了结构体__main_block_impl_0所占用的大小,最终又把他作为参数,传递给__main_block_impl_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)};
3. __block_impl
最终我们看一下生成的__block_impl结构体是什么样子的。通过源码我们发现:
- 存在isa指针,和对象一样,表示block类型,下面会详细讲解
FuncPtr代表__main_block_func_0封装的代码块
所以__block_impl就主要包含了block的类型和代码块的封装
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
Block的执行
关于Block的执行,简化完的代码如下,即调用__block_impl中的FuncPtr,而FuncPtr就是封装的执行的代码块。
block->FuncPtr
总结一下,关于block的本质:
- block本质也是一个OC对象,其内部也有
isa指针和大小 - block是封装了函数调用和函数调用环境的OC对象
Block的变量捕获
在使用block时,我们一般都会使用一些外部的变量,那么block内部是如何处理这些变量的呢?
自动捕获
block的自动捕获有一个经典的小栗子,如下代码所示,可以很好的发现,block内部此时并不会改变age的值。
int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
---------------------------------------
打印结果:
10
我们转换代码后,发现再生成__main_block_impl_0结构体时,增加了一个age成员变量,并将外部的值赋值给它,做了相应的值拷贝的操作。且我们在调用代码块中的逻辑,进行打印时,生成了一个同名的临时变量,所以是不能够进行修改的。



局部静态变量捕获
int age = 10;
static int height = 10;
void (^block)(void) = ^(){
NSLog(@"Age is %d, height is %d", age, height);
};
//先修改age和height的值
age = 20;
height = 20;
//Block的调用
block();
--------------------------------------------------
打印结果:
Age is 10, height is 20
通过上述的打印结果,我们发现局部的静态变量的值,在block内部是被修改了的。那说明局部变量并不是简单的值拷贝,接下来我们继续看源码。
通过源码,我们可以发现,对于静态的局部变量,block内部其实是生成的是一个指针地址,在调用方法时,也是通过其指针地址,获取的值,所以外部的修改,在其内部也是生效的。



全局变量
对于全局的变量,我们通过源码可以发现,block内部并没有进行捕获,只需要在要用的时候,直接通过变量名访问就行了,因为全局变量时跨函数的,可以直接通过变量的名字直接访问。

__block关键字的捕获
int main(){
__block int age = 10;
static int height = 10;
void(^block)(void) = ^{
age++;
printf("Age is %d, height is %d", age, height);
};
block();
return 0;
}
--------------------------------------------------
打印结果:
Age is 11, height is 10
通过上面的例子,我们知道局部变量在block内部是值传递,是不能进行修改的。但是当我们在变量前加入__block关键字时,在其内部就可以对值进行修改,那么到底是怎么实现的呢,我们继续探究源码。


__block修饰的变量,会转变成__Block_byref_age_0类型的结构体对象,并带有5个变量。
__isa:结构体中存在的这个isa指针也就说明了__Block_byref_a_0本质是一个对象__forwarding:__forwarding是__Block_byref_a_0结构体类型的,并且__forwarding存储的值为(__Block_byref_a_0 *)&a,即结构体自己的内存地址指针。__flag:C++预留参数,为0__size:sizeof(__Block_byref_a_0)即__Block_byref_a_0所占用的内存空间。age:用来存储原变量值,这里存储10。

__Block_byref_age_0对象,然后获取到__forwarding中指针地址,最后对其值进行修改。
总计一下,关于block的捕获机制
auto局部变量是直接在栈上保存的,所以block如果要使用的话,必须值拷贝到其内部空间中,使用的时访问的是内部生成的临时变量static的变量在运行的生命周期中都存在,所以block使用时,会进行指针拷贝,获得其值进行修改全部变量不会被捕获,在使用的时候直接根据变量名进行访问__block修饰的局部变量,回来内部生成__Block_byref_a_0结构体对象,里面保存着isa指针,自己的指针地址,内存大小和局部变量的值,并保存在block空间中,使用时会获取到对象的指针地址的值进行操作。

Block的内存管理
程序内存小概念
程序内存主要分为以下几种:
- 代码段 占用空间很小,一般存放在内存的低地址空间,我们平时编写的所有代码,就是放在这个区域
- 数据段 用来存放全局变量
- 堆区 是动态分配内存的,用来存放我们代码中通过
alloc生成的对象,动态分配内存的特点是需要程序员申请内存和管理内存。例如OC中alloc生成的对象需要调用releas方法释放【MRC下】,C中通过malloc生成的对象必须要通过free()去释放。 - 栈区 系统自动分配和销毁内存,用于存放函数内生成的局部变量
Block类型
我们的Block在平时开发中,根据内存的存储,主要就是分为3种类型,但是其实根据libclosure-73的源码,我们发现还有另外3中系统使用的block类型,我们就不做研究了,主要是看日常开发用到的3种。

__NSGlobalBlock__ ( _NSConcreteGlobalBlock ):全局Block__NSStackBlock__ ( _NSConcreteStackBlock ):栈Block__NSMallocBlock__ ( _NSConcreteMallocBlock ):堆Block

__NSGlobalBlock__
// 无变量
- (void)viewDidLoad {
[super viewDidLoad];
void (^block)(void) = ^{
};
block();
NSLog(@"%@",[block class]);
}
// 静态变量
- (void)viewDidLoad {
[super viewDidLoad];
static int a = 10;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
block();
NSLog(@"%@",[block class]);
}
// 全局变量
static int a = 10;
- (void)viewDidLoad {
[super viewDidLoad];
void (^block)(void) = ^{
NSLog(@"%d",a);
};
block();
NSLog(@"%@",[block class]);
}
--------------------------------------------------------------
打印结果:
__NSGlobalBlock__
根据以上的例子我们可以知道,当Block没有变量的捕获或者是全局变量时,此时Block是在data段的,即全局的Block。一般情况下我们比较少用到。
__NSStackBlock__
当我们的block捕获外界变量局部变量时,此时Block的类型为__NSStackBlock__。下面我们写个例子:
- (void)viewDidLoad {
[super viewDidLoad];
int a = 10;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
block();
NSLog(@"%@",[block class]);
}
--------------------------------------------------------------
打印结果:
__NSMallocBlock__
根据上面的结论,我们发现貌似是不对的,此时的block是__NSMallocBlock__。那么为什么会产生这种结果呢,这是由于在ARC环境下,会将Block自动copy到堆区,那么为什么要进行这样的操作呢?
我们知道在栈上的变量是超过其作用域之后,会被系统自动释放,而不需要我们手动进行release,我们定义的局部block自然也是要销毁的,但是如果此时我们block被销毁了,此时调用就会有有问题。

那么为了解决上述的问题,在ARC下,系统就会自动将Block复制到堆上,从而延长其生命周期。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈块就会变成堆块。

在copy操作之后,既然__block变量也被copy到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,例如以下的情况
- block作为函数参数返回的时候
- 将block复制给
__strong指针的时候 - block作为Cocoa API中方法名里面含有usingBlock的方法参数时
- block作为GCD API的方法参数的时候
__NSMallocBlock__
当我们直接调用临时变量时,此时block会在堆区。
NSLog(@"GlobalBlock11:%@",^{ NSLog(@"GlobalBlock--%d",a);});
--------------------------------------------------------------
打印结果:
__NSMallocBlock__
总结一下Block的内存管理:

使用Copy后

Block的循环引用
我们都知道,造成循环引用的原因是 self->block->self,这两者之间的相互强引用造成的。
Block对象的捕获
那么首先我们来看一下,Block为啥会对self或者对象进行强引用呢? 我们可以看一下对象在Block中是如何存在的。
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
void (^block1)(void) = ^{
NSLog(@"MyBlock - %d",person.age);
};
block1();
}
return 0;
}
使用上述代码,转换为C++代码可以清晰的看到Person对象被强引用,所以当Person销毁后,如果block不销毁,则Person不会进行释放。

copy和dispose
我们发现在捕获变量和__block类型等局部变量时,系统会自动生成__main_block_copy_0和__main_block_dispose_0函数并调用


我们发现__main_block_copy_0内部调用了_Block_object_assign函数,并传入了person的地址,其内部的原理实现为对根据传入对象的类型,选择进行强引用还是弱引用,并在block进行copy操作时进行调用
- 第一个dst代表拷贝之前栈空间的block,
- 第二个src代表拷贝之后堆空间上的block。
同理__main_block_dispose_0也是在block进行销毁时进行对应的操作
循环引用处理
通过上面的探究,我们基本了解了,在block进行copy到堆上的操作时,会对传入的对象类型进行强引用或者弱引用,如果是强引用,那么必然会导致block在堆上强持有对象,造成对象的不能及时释放。所以我们需要对传入的对象进行弱引用,这样block也会弱引用,并不会增加引用计数,造成不释放。
通常在开发中使用__weak typeof(self) weakSelf = self;将其添加到弱引用表中,在block内部使用weakSelf来处理。但是如果block内部调用时,self已经释放,此时使用weakSelf处理就行不通了,这时候可以添加__strong typeof(weakSelf) strongSelf = weakSelf;来对弱引用进行一下强引用,这样等到出了block作用域之后,就都会销毁了,不造成循环引用。
__weak typeof(self) weakSelf = self; // weakSelf(弱引用表) -> self
// strongSelf(nil) -> weakSelf -> self(引用计数不处理)--nil -> block -> weakSelf
self.block = ^{
// 持有 不能是一个永久持有 - 临时的
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
总结一下,Block的循环引用
Block内部会对对象类型进行持有Block在进行copy操作时,会调用__main_block_copy_0进行处理,对捕获的对象,根据其修饰类型来进行强引用或者弱引用Block在进行dispost操作时,会调用__main_block_dispose_0进行处理,也是根据修饰类型来进行对应的释放- 处理循环引用时,可以采用弱引用对象,block内部强持有弱引用的对象的方式来处理,保证block内部获得的对象为弱引用,不增加引用计数,这样就能避免循环引用
Block底层源码验证
我们通过编译后的C++代码,已经基本掌握了Block的相关知识,但是对于底层源码的处理逻辑还不是很清晰,比如block是如何copy到堆上的,我们可以通过libclosure源码进行查看。
通过查看C++的转换代码,可以发现其重要Block代码都在Block_private.h中

在libclosure源码中,我们找到了Block_layout结构和我们在C++代码中看到的Block是基本相同的,所以这应该就是Block的基类
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor; //
// imported variables
};
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
- isa:指向父类的指针
- flags:记录状态的标志位
- reserved:
- invoke:执行代码块函数
- descriptor:block的附加描述信息
关于flags参数,是以位域的形式存在,主要用来记录block的一切标志信息
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime =(0x0001)释放标记,一般常用
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 存储引用计数的值
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
- 第1位:释放标记,一般常用
BLOCK_NEEDS_FREE做位与操作,一同传入Flags,告知该block可释放 - 第16位:存储引用计数的值,是一个可选用参数
- 第24位:第16位是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的值
- 第25位:是否拥有拷贝辅助函数
- 第26位:是否拥有block析构函数
- 第27位:标志是否有垃圾回收
- 第28位:标志是否是全局block
- 第30位:与
BLOCK_USE_STRET相对,判断是否当前block拥有一个签名。用于runtime时动态调用
我们发现除了Block_descriptor_1,还有Block_descriptor_2和Block_descriptor_3等可选的参数,Block_descriptor_2中含有copy和dispose两个函数,Block_descriptor_3有signature和layout。
copy和dispose两个函数通过上面C++的分析,我们知道在捕获对象等局部变量时,会生成这两个函数
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature; // Block签名
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
在源码runtime.cpp中,我们可以找到这两个方法的实现。通过方法的实现,我们发现两个方法都会先判断flags标志位是否含有对应的值,即BLOCK_HAS_COPY_DISPOSE和BLOCK_HAS_SIGNATURE。
而当满足标志位时,怎么获取到对应的对象呢,我们可以看到两个函数,都是通过Block_descriptor_1加上对应的大小,进行内存偏移得来的,说明这3个对象在内存分布上是连续的。
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
return (struct Block_descriptor_2 *)desc;
}
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
return (struct Block_descriptor_3 *)desc;
}
Block的Copy
通过上面的探究我们知道,当Block为栈Block时,会自动进行copy操作到堆Block,那么这个过程是如何进行的,我们接着使用源码验证
仍然使用上述的代码,打断点看汇编相关代码

objc_retainBlock函数,然后再objc_retainBlock函数中,又调用了_Block_copy函数,通过获取此时block的类型,我们可以发现block在经过此步骤后,变成了堆block,说明其就是copy的主要方法

下面我们看_Block_copy的相关源码,主要的copy逻辑可以总结如下:
- 全局Block在执行copy时:不作任何处理,直接返回
- 堆Block在执行copy时:会增加引用计数,然后返回
- 栈Block在执行copy时:
- 先会申请一片相同大小的内存空间,
- 然后将栈区的Block拷贝到堆区,
- 设置标志位的引用计数,
- 并执行
_Block_call_copy_helper对其中的_Block_descriptor_2进行copy操作 - 最后设置Block的isa指向为堆Block
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
✅// 如果需要对引用计数进行处理,那就直接处理,处理完就返回
✅// block的引用计数是不由runtime下层处理,需要自己处理
✅// 这个地方处理的是堆区block
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
✅// 如果是全局block 直接返回
return aBlock;
}
else {
// Its a stack block. Make a copy.
✅// 栈区block 使用copy
✅// 先在堆区初始化一块内存空间
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
✅// 将栈区的数据copy到堆区的空间
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
✅// 设置标志位
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
✅// 设置为isa为_NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
Block的签名
通过Block_layot的结构,我们知道函数的签名是在Block_descriptor_3中的,如果我们要找到Block_descriptor_3,需要通过内存便宜,找到Block_descriptor_1,然后再偏移找到Block_descriptor_3中的signature,查看其签名。
如上述代码,打印出的签名为v8@?0,其中@?就表示Block
__block修饰原理
通过上面对C++的探究,我们知道,在捕获__block的变量是,在其底部会生成__Block_byref结构体对象。
通过源码我们可以发现,其结构和Block有异曲同工之妙,都有可选的Block_byref_2和Block_byref_3参数,而且在使用__block修饰的时候,也会相对应的copy和dispose函数,与Block_byref_2对应,说明其也会进行对应的copy等操作。
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
我们发现在生成的函数中,也会调用对应的_Block_object_assign和_Block_object_dispose函数,我们还是以copy来探究_Block_object_assign函数是如何进行拷贝的

//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
// hold objects - 自动捕获到变量
// lgname
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
default:
break;
}
}
通过查看源码,我们发现内部针对传入对象的类型不同,做了不同的处理,针对不同的枚举,其含义如下:
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // 截获的是对象__attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // 截获的是block变量
BLOCK_FIELD_IS_BYREF = 8, // 截获的是__block修饰的对象
BLOCK_FIELD_IS_WEAK = 16, // 截获的是__weak修饰的对象
BLOCK_BYREF_CALLER = 128, // 处理block_bref内部对象内存的时候会加一个额外的标记,配合上面的枚举一起使用
};
1.BLOCK_FIELD_IS_OBJECT
如果修饰的是对象类型的,此时会调用_Block_retain_object(object);函数,通过远么可以发现,并没有做任何的处理,因为在ARC环境下,是有runtime底层来处理的
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void _Block_retain_object_default(const void *ptr __unused) { }
2.BLOCK_FIELD_IS_BLOCK
如果修饰的是block,则直接调用_Block_copy方法
3.BLOCK_FIELD_IS_BYREF
如果是__block类型,会调用_Block_byref_copy方法,通过源码,我们可以知道,主要做了一下几部操作:
- 申请堆内存空间
- 给新申请的空间赋值
- copy的对象和源对象都指向堆内存的拷贝地址
- 理desc2和3的内存偏移取值
- 如果已经在堆,则直接增加引用计数
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
✅// 1.申请堆内存空间
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
✅// 2. 给新申请的空间赋值
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
✅// 3.copy的对象和源对象都指向堆内存的拷贝地址
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
✅// 4.处理desc2 内存偏移取值
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
✅// 处理desc2 内存偏移取值
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
✅// 如果已经在堆,则直接增加引用计数
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
我们发现,在拷贝完_Block_byref后,还调用了(*src2->byref_keep)(copy, src);这个方法,那么这个方法到底是干啥的呢,通过C++中生成_Block_byref对象时传入的参数,如果有cop函数时,这两个方法是会赋值给Block_byref_2的,也就是对应的拷贝(__Block_byref_id_object_copy_131)和析构函数(__Block_byref_id_object_dispose_131)

查看其源码,发现其调用了_Block_object_assign,并偏移了40个字节,那么为什么是40呢,看下面的源码结构,我们可以发现,40个字节后,正好是name属性,也就是说,对name属性进行了copy
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
struct __Block_byref_name_0 {
void *__isa; // 8
__Block_byref_name_0 *__forwarding; // 8
int __flags; // 4
int __size; // 4
void (*__Block_byref_id_object_copy)(void*, void*); // 8
void (*__Block_byref_id_object_dispose)(void*); // 8
NSString *name;
};
总结一下,__block的修饰原理
- block的拷贝,从栈内存到堆内存。
- 对新生成的结构体的拷贝。
__block修饰的变量会生成一个名为__Block_byref_XXX_0结构体,将原来的进行了封装,会调用_Block_object_assign根据其类型进行申请内存,拷贝到堆等操作 - 并对原来的对象的内存的拷贝。
关于Block的释放,和copy的原理是基本一样的,只不过是反过来,有兴趣的同学可以自己研究一下。
总结
Block本质也是一个OC对象,封装了函数的调用和调用环境,内部也有isa指针Block的变量捕获- 对于局部变量的捕获实在栈上保存的,ARC下回自动拷贝到堆中使用
- 对于静态变量,会进行指针拷贝,对值进行修改
- 对于全局变量,不会进行捕获,直接访问
Block根据存放位置不同,可以分为3种__NSGlobalBlock__( _NSConcreteGlobalBlock ):全局Block,无临时变量访问__NSStackBlock__( _NSConcreteStackBlock ):栈Block,访问临时变量__NSMallocBlock__( _NSConcreteMallocBlock ):堆Block,栈block调用copy
Block循环引用问题原因及处理- 原因:由于Block会对对象进行捕获,并根据其类型进行对应的copy到堆的处理,如果是强引用,对象则需要再block释放后才可以释放,如果对象也持有了block,则发生了循环引用
- 解决:当发生copy操作时,采用弱引用,则不会增加引用计数,同时在block内部强引用此时弱引用的对象,防止弱引用对象被释放而造成的数据问题,可以有效避免循环引用
Block的Copy操作也会根据Block的类型来进行处理- 全局Block在执行copy时:不作任何处理,直接返回
- 堆Block在执行copy时:会增加引用计数,然后返回
- 栈Block在执行copy时:
- 先会申请一片相同大小的内存空间,
- 然后将栈区的Block拷贝到堆区,
- 设置标志位的引用计数,
- 并执行
_Block_call_copy_helper对其中的_Block_descriptor_2进行copy操作 - 最后设置
Block的isa指向为堆Block
__block修饰原理:3层拷贝- block的拷贝,从栈内存到堆内存。
- 对新生成的结构体的拷贝。
__block修饰的变量会生成一个名为__Block_byref_XXX_0结构体,将原来的进行了封装,会调用_Block_object_assign根据其类型进行申请内存,拷贝到堆等操作 - 并对原来的对象的内存的拷贝。