在 Block的类型和循环引用一文中,我们简单探索了block
的类型和其循环引用。在本文中,我们将继续探索block的结构和原理。
Block结构
我们在代码中定义如下函数:
void (^block)(void) = ^{
printf("Hello");
};
block();
我们先将其编译其c++
文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
我们将其main函数
部分的c++
代码单独拿出来:
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;
}
};
__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello ");
}
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)};
我们可以看出其为__main_block_impl_0
的结构体,在其构造函数中,运用了函数式编程思想
将函数作为参数传入到block
构造函数中。
Block捕获变量
无__block修饰
我们修改代码,打印变量a
的值
int a = 10;
void (^block)(void) = ^{
printf("Hello %d", a);
};
block();
同样将其编译为c++
文件
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__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
printf("Hello %d", a);
}
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)};
在编译时期,就会捕获变量,将捕获的变量作为结构体的属性
。在block进行调用时,会调用FuncPtr
函数
block->FuncPtr(block)
在调用时,block会作为参数
,在block调用时,能够拿到block对象,通过block对象拿到属性值
。此时 a的值是通过int a = __cself->a;
的方式获取的。是一个 值拷贝
。
使用__block修饰
下面我们将变量使用__block
修饰,从而修改变量a的值,然后我们来看下转化为c++文件后的变化。
__block int a = 10;
void (^block)(void) = ^{
a++;
printf("Hello %d", a);
};
block();
进行编译后
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a)++;
printf("Hello %d", (a->__forwarding->a));
}
经过__block
修饰 之后多了一个 __Block_byref_a_0
结构体,在block的构造函数中将__Block_byref_a_0
的内存地址进行传入。
在函数调用的时候修改变量a的值,是通过如下方式进行修改变量a的值的
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a)++;
printf("Hello %d", (a->__forwarding->a));
通过 __Block_byref
结构体里面保存的 变量指针,找到该变量进行操作。这样达到了在block调用时,能够修改外界变量的值。
Block的内存变化
在开始这一结探索之前我们先想一下以下问题:
- 1,block为什么用
copy
修饰比用strong
修饰更好一些? - 2,block是如何从栈区拷贝到堆区的?
- 3,block从栈区拷贝到堆区的时机是什么?
我们接着以上面的代码做为示例进行探讨。
int a = 10;
void (^block)(void) = ^{ \\ 代码1
NSLog(@"Hello - %d", a);
};
block(); \\ 代码2
我们分别在 代码1
和代码2
行设置两个断点。在Xcode的 Debug ->Debug Workflow
中选中 Always Show Disassembly
,来查看其混编代码。
我们使用的是
真机设备
,查看的是arm64
架构下的混编。在第一个断点处,我们来查看其寄存器的值:
(lldb) register read
General Purpose Registers:
x0 = 0x00000001eb34ef70 libsystem_blocks.dylib`_NSConcreteStackBlock
x1 = 0x00000001d9800598
x2 = 0x0000000000000001
x3 = 0x000000016b8d1e00
x4 = 0x0000000000000010
x5 = 0x000000016b8d1a0f
x6 = 0x000000016b8d1b00
x7 = 0x0000000000000000
x8 = 0x0000000104534010 BlockTest`__block_descriptor_36_e5_v8�?0l
x9 = 0x0000000104531fec BlockTest`__29-[ViewController viewDidLoad]_block_invoke at ViewController.m:20
x10 = 0x000000000000000a
x11 = 0x00000002830fa308
x12 = 0x0000000000000000
x13 = 0x0000000000000000
x14 = 0x00000001d97fce46
x15 = 0x00000001df8ae420 (void *)0x00000201bc228e46
x16 = 0x00000001a14ddbbc UIKitCore`-[UIViewController viewDidLoad]
x17 = 0x00000001046dd0a4 libMainThreadChecker.dylib`__trampolines + 15992
x18 = 0x0000000000000000
x19 = 0x0000000000000000
x20 = 0x0000000115d0a5e0
x21 = 0x00000001eb392000 _MergedGlobals.9 + 16
x22 = 0x00000001d970fcb7
x23 = 0x0000000000000001
x24 = 0x0000000000000001
x25 = 0x00000001e8377000 UIKitCore`_UITouchForceMessage._maximumPossibleForce
x26 = 0x0000000115e07cb0
x27 = 0x0000000280bde380
x28 = 0x00000001d97192ee
fp = 0x000000016b8d1c70
lr = 0x0000000104531f6c BlockTest`-[ViewController viewDidLoad] + 64 at ViewController.m:18:5
sp = 0x000000016b8d1c10
pc = 0x0000000104531f8c BlockTest`-[ViewController viewDidLoad] + 96 at ViewController.m:20:27
cpsr = 0x60000000
读取通用寄存器x0
的值:
(lldb) register read x0
x0 = 0x00000001eb34ef70 libsystem_blocks.dylib`_NSConcreteStackBlock
x0寄存器
现在存放的是 block,其类型为 _NSConcreteStackBlock
,该数据结构在 libsystem_blocks.dylib
里面。
我们跳过第一个断点,进入第二个断点
我们从汇编代码中可以看出,在block调用前,调用了
objc_retainBlock
函数,此时,我们来查看block的类型。
(lldb) register read x0
x0 = 0x000000028204d7d0
(lldb) po 0x000000028204d7d0
<__NSMallocBlock__: 0x28204d7d0>
signature: "v8@?0"
invoke : 0x104e61fec (/private/var/containers/Bundle/Application/F19DA240-D120-4161-8FAA-AC62918B124D/BlockTest.app/BlockTest`__29-[ViewController viewDidLoad]_block_invoke)
在经过 objc_retainBlock
函数后,block由 _NSConcreteStackBlock
类型变为 __NSMallocBlock__
类型。
我们通过objc4
和libclosure
源码来查看一下objc_retainBlock
函数究竟做了什么?
objc_retainBlock
通过查看 objc4
源码我们可知其实现为:
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
_Block_copy
函数在libclosure
中
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock; \\1
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) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) { \\ 2
return aBlock;
}
else {
// Its a stack block. Make a copy. \\ 3
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
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.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
Block_layout
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
- 1,block的底层数据结构为
Block_layout
结构。 - 2,通过
BLOCK_IS_GLOBAL
标识来判断是否为NSGlobalBlock
,如果是,不做任何处理。 - 3,如果是
非NSGlobalBlock
,就会将其从栈区拷贝到堆区。
具体流程如下: - 3.1: 根据block的
size
申请内存空间。 - 3.2: 对block的
标志符flag
,invoke函数
等进行拷贝。 - 3.3: 修改已拷贝block的
isa指针
,指向_NSConcreteMallocBlock
。
通过对上面的探索,我们可以知道我们前面提出的三个问题的答案,
1: block捕获外界变量时,会由栈区拷贝堆区,因此使用copy
修饰,更好一些。
2: block是通过_Block_cppy
函数,拷贝过去的。
3: block拷贝的时机是:该block将要调用之前。