Block源码地址

1. Block实质:

Block从C语言角度实质上是能够捕获上下文变量的匿名函数,在创建的时候会捕获所需要的上下文局部自动变量到闭包内部。Block的底层是作为C语言源代码来处理的,支持Block的编译器会将含有Block语法的源代码转换为C语言编译器能处理的源代码,当作C语言源码来编译。Block和__block 最终都会转换为一个C语言的结构体对象。

Block 在OC中的实现如下

struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

Block 需要了解如下几个方面:

(1) 我们怎么把带有Block的 Objective C 代码转化为 C 代码
(2) 转换后的代码结构是怎样的
(3) Block对局部自动变量,局部静态变量,全局变量,对象,__block变量的捕获
(4) Block 与 __block 的存储特性
(5) Block的循环引用
(6) Block的内存布局

我们使用的转换block的命令行如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

一个简单常见的例子:

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic) int testProperty;
@end

@implementation Person
@end

int globleIntValue = 2;
static int staticGlobleIntValue = 5;
int main(int argc, char * argv[]) {
int intValue = 5;
static int staticIntValue = 3;
__block int blockInt = 5;
Person *person = [Person new];
void (^blockTest)(int) = ^ (int value){
printf("Hello %d",value);
printf("Hello %d",intValue);
printf("Hello %d",staticIntValue);
printf("Hello %d",globleIntValue);
printf("Hello %d",staticGlobleIntValue);
printf("Hello %d",person.testProperty);
blockInt = 34;
};
blockTest(2222);
return 0;
}

转换后的代码:

int globleIntValue = 2;
static int staticGlobleIntValue = 5;

struct __Block_byref_blockInt_0 {
void *__isa;
__Block_byref_blockInt_0 *__forwarding;
int __flags;
int __size;
int blockInt;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int intValue;
int *staticIntValue;
Person *person;
__Block_byref_blockInt_0 *blockInt; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intValue, int *_staticIntValue, Person *_person, __Block_byref_blockInt_0 *_blockInt, int flags=0) : intValue(_intValue), staticIntValue(_staticIntValue), person(_person), blockInt(_blockInt->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int value) {
__Block_byref_blockInt_0 *blockInt = __cself->blockInt; // bound by ref
int intValue = __cself->intValue; // bound by copy
int *staticIntValue = __cself->staticIntValue; // bound by copy
Person *person = __cself->person; // bound by copy

printf("Hello %d",value);
printf("Hello %d",intValue);
printf("Hello %d",(*staticIntValue));
printf("Hello %d",globleIntValue);
printf("Hello %d",staticGlobleIntValue);
printf("Hello %d",((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("testProperty")));
(blockInt->__forwarding->blockInt) = 34;
}
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((void*)&dst->blockInt, (void*)src->blockInt, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->blockInt, 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, char * argv[]) {

int intValue = 5;
static int staticIntValue = 3;

__attribute__((__blocks__(byref))) __Block_byref_blockInt_0 blockInt = {(void*)0,(__Block_byref_blockInt_0 *)&blockInt, 0, sizeof(__Block_byref_blockInt_0), 5};

Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("new"));

void (*blockTest)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intValue, &staticIntValue, person, (__Block_byref_blockInt_0 *)&blockInt, 570425344));
((void (*)(__block_impl *, int))((__block_impl *)blockTest)->FuncPtr)((__block_impl *)blockTest, 2222);

return 0;
}

2. Block定义

Block类型变量,一般结合用typedef定义:

typedef int (^blockType)(int,int)
@property (nonatomic, asign, readonly) blockType block;

Block 变量可以作为自动变量,函数参数,函数返回值,静态局部变量,静态全局变量,全局变量
Block 定义:
block定义和普通的C语言函数定义类似,只不过多了一个^省去函数名称

^返回值类型 (参数列表) {
表达式
}

3. __block_impl 结构体

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
}
void (^blk)(void) = ^ {

}

转换成:

void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,auto_peram1,auto_peram2);

blk() 实际上执行的是

(*blk->impl.FuncPtr)(blk);

一个block的组成:

struct __block_impl impl;
struct __main_block_desc_0 *Desc;
---->
捕获的自动变量和局部静态变量指针

4. Block的变量捕获

  • block接受的参数属于值传递,可以在block内修改,不对其进行捕获

  • 局部变量的捕获: 当定义block的时候,Block中所使用的自动变量值被保存到Block的结构体实例中,也就是Block自身中,并且保存后就不能改写该值。但是还是可以调用变更该对象的方法。比如NSMutableArray 的 addObject

  • static局部变量的捕获: 局部静态变量 作用域在block之外,这种情况下block中存储的是静态局部变量的地址,所以当FuncPtr指向的函数调用的时候会通过取地址中存储的值

  • 全局变量的捕获
    全局变量并没有被block所捕获,因为全局变量存放在全局区,随时都可以访问,所以当FuncPtr指向的函数调用的时候就会直接取全局变量的值使用。而局部变量超过作用域就会自动回收所以block需要在自身存放一份,以保证其能准确访问。

  • 对象类型的捕获:
    在没有调用copy的情况下,还没有调用block()的时候对象就已经释放了,说明在栈上的block并没有对所使用的对象强引用,对block 进行一次copy操作会发现在block没有release之前,所引用的对象没有被释放,所以堆上的block强引用了所使用的对象,对对象执行一次release之后,对象的引用计数依然没有成为0,因为block还引用着它。这是因为当对block进行copy操作的时候,block会执行内部的__main_block_copy_0方法。__main_block_copy_0方法执行_Block_object_assign根据变量的修饰符判断对捕获的对象的引用情况(retain或者弱引用)。而当block从堆中移除的时候,会调用与__main_block_copy_0对应的_Block_object_dispose函数,该函数会自动释放引用的auto变量。(栈上block访问了对象类型的auto变量的时候不会对其发生强引用)

block从栈上copy到堆上的时候,block内部会执行copy操作,_Block_object_assign函数回通过auto变量的修饰符判断发生强弱引用。block从堆中移除的时候,block内部会执行dispose,将引用的对象进行释放。

简而言之:
局部变量在block中使用的时候会被block捕获,auto变量是值捕获,而static变量是地址捕获。全部变量不会被捕获。当Block从栈复制到堆上时,block会对id类型的变量产生强引用

5. __block 说明符

在block中允许修改静态局部变量,静态全局变量和全局变量这几种类型,但是对于局部自动变量如果在block里面修改编译器会发出警告,这时候需要在局部自动变量之前添加__block 说明符,__block 可以指定任何类型的自动变量,通过__block修饰的变量将会变成一个结构体实例,只有这样这个值才能被block共享、并且不受栈帧生命周期的限制、在block被copy后,能够随着block复制到堆上

这时候被捕获的对象会转换为

struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 * __forwarding;
int __flags;
int __size;
// 这后面是__block变量
int val;
}

__main_block_impl_0中也会多出一个

__Block_byref_val_0 *val;

也就是说局部自动变量添加了__block后该变量将会以__Block_byref_val_0 形式被捕获

有时候__block变量配置在堆上的情况下,也可以访问栈上的__block变量,在这种情况下只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从堆上的__block变量还是从栈上的__block变量都能正确得访问。简而言之:
__block 变量结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是在堆上都能正确得访问__block变量


__block id __strong obj = [[NSObject alloc] init]; 和 id __strong obj = [[NSObject alloc] init] 一样在block没有copy的时候是不持有的,copy后block会持有对象引用

id array = [[NSMutableArray alloc] init];
id __weak array2 = array;

array2 是弱引用,当变量作用域结束,array 所指向的对象内存被释放,array2 指向 nil

如果 __weak 再改成 __unsafe_unretained ,__unsafe_unretained 修饰的对象变量指针就相当于一个普通指针。使用这个修饰符有点需要注意的地方是,当指针所指向的对象内存被释放时,指针变量不会被置为 nil。所以当使用这个修饰符时,一定要注意不要通过悬挂指针(指向被废弃内存的指针)来访问已经被废弃的对象内存,否则程序就会崩溃。

如果 __unsafe_unretained 再改成 __autoreleasing 会怎样呢?会报错,编译器并不允许你这么干!如果你这么写
__block id __autoreleasing obj = [[NSObject alloc] init];
编译器就会报下面的错误,意思就是 __block 和 __autoreleasing 不能同时使用。

6. Block 存储属性

Block 有如下几种存储类型:

_NSConcreteGlobalBlock 程序的数据区域

  • Block当作全局变量使用时(block 字面量写在全局作用域时)
  • 当 block 字面量不获取任何外部变量时(只使用全局变量以及block参数的时候)
    _NSConcreteGlobalBlock不持有对象

_NSConcreteStackBlock 栈

  • 除了上述两中情况下Block配置在程序的数据区中以外(换种说法如果只用到外部局部变量、成员属性变量,且没有强指针引用的block就是StackBlock),Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。配置在栈上的Block,如果其所属的变量作用域结束,该Block就被自动废弃。_NSConcreteStackBlock不持有对象

_NSConcreteMallocBlock 堆

  • 有强指针引用或copy修饰的成员属性引用的block,配置在全局变量上的Block,从变量作用域外也可以通过指针访问。但是设置在栈上的Block,如果其所属的作用域结束,该Block就被废弃;并且__block变量的也是配置在栈上的,如果其所属的变量作用域结束,则该__block变量也会被废弃。那么这时需要将Block和__block变量复制到堆上,才能让其不受变量域作用结束的影响。_NSConcreteMallocBlock持有对象。

整个存储区域如下图所示:

通过上述结果我们可以看出当block访问了auto变量的时候会变成__NSStackBlock__类型。而其他情况下是__NSGlobalBlock__类型,比较典型的是Block声明在全局区域,以及Block虽然不声明在全局区域但是Block不截获自动变量。
__NSGlobalBlock__类型存在于数据区,__NSStackBlock__存在于栈区。

而在ARC环境下原本__NSGlobalBlock__的block依然是__NSGlobalBlock__类型,而原本是__NSStackBlock__却变成了__NSMallocBlock__存放在堆区。这是因为当我们定义block的时候ARC默认为我们做了一次copy操作。

在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,只有当
block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法这时候需要手动调用copy,但是比如Cocoa框架的方法且方法名中包含usingBlock等,或者GCD API的情况下,这些在方法或者函数中对传递过来的参数做了复制操作所以不需要copy

copy 的结果:

如果原先存储域处于_NSConcreteGlobalBlock 那么什么都不做
如果原先存储域处于 _NSConcreteMallocBlock 那么将会导致引用计数增加
如果原先存储域处于_NSConcreteStackBlock 那么会将block从栈复制到堆

下面是Block复制和释放的源码:

Block 复制

 void *_Block_copy(const void *arg) {

//1. 声明一个Block_layout结构体类型的指针,如果传入的Block为NULL就直接返回。
struct Block_layout *aBlock;
if (!arg) return NULL;

// 如果Block有值就强转成Block_layout的指针类型。
aBlock = (struct Block_layout *)arg;
//如果Block的flags表明该Block为堆Block
if (aBlock->flags & BLOCK_NEEDS_FREE) {
//对block的引用计数递增后返回Block
latching_incr_int(&aBlock->flags);
return aBlock;
}
//如果Block为全局Block就不做其他处理直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 如果是堆栈类型就对block执行一次copy
else {
//分配空间,这里的控件大小为descriptor->size
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;

//将Block从栈上复制到堆上
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
//将新Block的引用计数置零。
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.
//将堆Block的isa指针置为_NSConcreteMallocBlock,返回新Block
result->isa = _NSConcreteMallocBlock;
return result;
}
}

Block 释放

void _Block_release(const void *arg) {

struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;

//如果是Globle类型的 Block 就不做任何操作
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
//如果Block flags 标志位指示 block 不需要释放 就直接返回
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;

//判断aBlock的引用计数是否需要释放内存
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
//释放block
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
free(aBlock);
}
}

简而言之:
访问了auto变量的block是__NSStackBlock__类型,没有访问auto变量的block是__NSGlobalBlock__类型。而对__NSStackBlock__类型进行copy操作就会变为__NSMallocBlock__类型。

在如下情况下栈上的block会被复制到堆上:

在调用Block 的copy 实例方法的时候
Block 作为函数返回值返回时
将Block赋给带有__strong修饰符id 类型的类或者Block类型成员变量时(block作为强指针引用的时候也会自动调用copy)
在方法名中含有usingBlock的Cocoa框架方法,或者GCD API中传递Block时候

在谁都不持有Block的时候block将会被释放

copy 函数会持有截获的对象,以及所使用的__block变量,dispose函数会释放截获的对象以及__block变量,
所以Block中使用的赋给赋有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可以超出其变量作用域而存在。

再简而言之:也就是block 被copy到堆上的时候,它所使用的strong类型的对象以及__block变量都会超出作用域而存在。

  1. __block 变量存储属性

当Block从栈复制到堆上的时候,它所使用的所有__block变量也会被复制到堆上,并被Block持有。在多个Block中使用__block变量的时候,因为最先会将所有的Block配置在栈上,所以__block变量最初也会配置到栈上,在任何一个Block从栈上复制到堆上的时候,__block变量也会一起从栈复制到堆上,并被该Block 持有,当剩下的Block从栈复制到堆的时候,被复制的Block持有__block变量,并增加__block变量的引用计数。
如果配置在堆上的Block被废弃,那么它所使用的__block变量也会被释放。





  1. 避免循环引用:

如果用self引用了block,block又捕获了self,这样就会有循环引用。因此,需要用weak来声明self,如果捕获到的是当前对象的成员变量对象,同样也会造成对self的引用,同样也要避免。

使用__weak来声明self
- (instancetype)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
self.blk = ^{
NSLog(@"%@", weakSelf.name);
};
}
return self;
}

- (void)configureBlock {
id tmpIvar = _ivar; //临时变量,避免了self引用
self.block = ^{
[tmpIvar msg];
}
}

当struct第一次被创建时,它是存在于该函数的栈帧上的,其Class是固定的_NSConcreteStackBlock。其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成后,捕获到的变量不能更改。

当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除。所以在函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上。这个方法的核心动作很简单:申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数。

Contents
  1. 1. Block源码地址
  2. 2. 1. Block实质:
  3. 3. 2. Block定义
  4. 4. 3. __block_impl 结构体
  5. 5. 4. Block的变量捕获
  6. 6. 5. __block 说明符
  7. 7. 6. Block 存储属性