iOS底层探索(二十二)Block
iOS底层探索(二十二)Block
Block相信都会用到,他是回传数据的一种方法,并且它本身是一个函数,只不过没有名字
Block的三种类型
自定义代码,如下
void (^block)(void) = ^{
NSLog(@"Name");
};
NSLog(@"%@",block);
这么一个简单的Block
就写完了。运行一下。查看
当前Block
的类型为NSGlobalBlock
类型。这是Block
的其中一种类型,为全局类型。
当在Block
访问外部变量时,再次打印。
int a = 10;
void (^block)(void) = ^{
NSLog(@"age - %d",a);
};
NSLog(@"%@",block);
运行结果为
当前的Block
为NSMallocBlock
为堆区
的block。在iOS 14之前
,当我们没有进行拷贝时,则是栈区类型。
那iOS 14之后
怎么获取栈区
的block呢?
代码做如下修改,
int a = 10;
void (^__weak block)(void) = ^{
NSLog(@"age - %d",a);
};
NSLog(@"%@",block);
运行查看结果
当前的Block
为NSStackBlock
为栈区
的block。
从上方可知,如果没有捕获外部变量时,block会放在全局区
如果访问外部变量
时,会将block进行copy,拷贝到相应的位置而是在堆区还是栈区则根据是否持有而改变,当前对象持有
block时,block在堆区
,否则在栈区
。
总结
- NSGlobalBlock 全局区
- NSMallocBlock 堆区
- NSStackBlock 栈区
循环引用问题
正常释放
A对象持有B对象,当A将要被释放时,将调用dealloc给B对象发信号。B对象执行dealoc方法进行释放。
循环引用
其根本原因在于retainCount无法为0,dealloc方法无法调用。
循环引用的条件必须是,A对象强引用B对象,并且B对象强引用A对象才能成立循环引用。
weak-strong-dance 强弱共舞
自定义下面的循环引用代码。
self.testBlock = ^{
NSLog(@"name = %@", self.name);
};
self.testBlock();
上面代码就是一个循环引用,在敲出这段代码时,编译器也会报循环引用的警告,不去理他。
怎样打破它
我们知道,循环引用的根本原因在于self
强引用block
,并且block
强引用self
,那么在属性设定中不可以使用weak
对block
进行修饰,因为使用weak
修饰时,出了创建block
的作用域外,就会被自动释放。
让block
弱引用self
修改代码如下
__weak typeof(self)weakSelf = self;
self.testBlock = ^{
NSLog(@"name = %@", weakSelf.name);
};
self.testBlock();
将当前对象self
使用__weak
修饰一下,然后让block
持有弱引用对象。__weak
会将当前对象加入到弱引用表
中。那么__weak
是否会对当前对象的引用计数进行处理呢。
在__weak
的前后对引用计数进行打印。
打印结果为一直,也就是说__weak
并没有对当前对象的引用计数进行处理,也就是没有+1
的操作。
但是这种方式有一个问题,就是weakSelf
的对象当当前对象被释放时会自动释放,不管block
内部是否完成了对weakSelf
对象的访问。即做如下操作时,还没有打印name
,就被释放了。
__weak typeof(self)weakSelf = self;
self.testBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"name = %@", weakSelf.name);
});
};
self.testBlock();
这种情况需要使用__strong
对weakSelf
对象进行强引用,即在block
的作用域结束前,当前对象不释放。
__weak typeof(self)weakSelf = self;
self.testBlock = ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"name = %@", strongSelf.name);
});
};
self.testBlock();
这种写法有个名字weak-strong-dance
即强弱共舞。
__block捕获self
使用__block
也可以进行打破循环引用的操作。代码如下
__block ViewController *vc = self;
self.testBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"name = %@", vc.name);
vc = nil;
});
};
self.testBlock();
这种方式为block
强引用vc
,vc
强引用self
,那么在这个状态下依然存在循环引用,因此需要在vc
使用完后,进行对vc
置nil
,打破循环引用,其也是为手动打破循环引用。
传递self
我们将self
变成函数的一个参数传入到block
内部,那么参数的作用域即为当前block
,当block
执行完毕后,将自动置为nil
,代码如下
self.block = ^(ViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.block(self);
Block探索
普通Block
创建.c
C语言文件,做如下自定义代码。
#include "stdio.h"
int main() {
void(^block)(void) = ^{
printf("name");
};
block();
return 0;
}
对上方的文件进行clang
编译查看。
找到main
函数,源码如下
int main(){
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// 使用函数式进行调用。这里是block需要调用的原因。
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); // block为隐藏参数,类似于oc中的 id self, SEL _cmd一样。
return 0;
}
对照源码查看。我们知道,block
等于__main_block_impl_0
,搜索__main_block_impl_0
查看这个函数,源码如下:
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
是一个结构体,这也是block
能够使用%@
进行打印的原因。当前的构造体也是一个函数,只不过没有名字。
查看__main_block_func_0
方法,源码如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("name");
}
当前方法的隐藏参数为block
自己对象。
捕获变量Block
修改.c
文件为如下格式,再次clang
。查看编译后的结果
int main(){
int a = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
查看__main_block_impl_0
查看是否有变化。
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;
}
};
从代码中可知,在__main_block_impl_0
结构体中可知,多了一个a
的参数。也就是在编译时,就自动生成了相应的变量。
查看__main_block_func_0
,即调用的方法中的源码,如下
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("name = %d", a);
}
由上方代码可知,在这个函数中a
与在main
函数中的a
不是同一个变量,在我们的认知中如果是同一个变量应该传&a
,即传递变量的地址,而现在的a
只是与main
函数中的a
的值相同,但地址不同,也就是说,不是同一个a
。也就是说,如果在block
中对a
进行操作,只是对block
内部的a
进行添加,跟main
函数中的a
基本无关。
__block捕获值
修改.c
文件为如下。
int main(){
__block int a = 10;
void(^block)(void) = ^{
printf("name = %d", a);
};
block();
return 0;
}
进行clang
编译查看main
函数
int main(){
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
从上面的代码中,我们可以很清晰的看到&a
的字样,这与上面的一致,即只有传递变量的地址时,才能真正的捕获变量。
查看__main_block_impl_0
结构体。源码如下:
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_byref_a_0
结构体的格式,源码如下:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
与main
中的赋值进行对照。可知将原来的a
封装成对象。
查看__main_block_func_0
方法源码,如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
printf("name = %d", (a->__forwarding->a));
}
在上面的a
与main
中的a
为同一个a
,因为是对指针进行了拷贝。因此可以进行操作。
Block的签名
通过上方的分析,我们知道,block是对象,但是在上方代码中却没有感知到global
、malloc
、stack
三种类型的变化。以及block的调用。
自定义block,源码如下:
void (^block1)(void) = ^{
NSLog(@"block");
};
block1();
对block1
进行断点。并且开启汇编调试。
进入到这块的汇编证明是第一次没有缓存的,如果有缓存则无法进入这块汇编中。
我们看到了objc_retainBlock
方法的调用,做一个符号断点,重新运行。
跳入到了_Block_copy
中,
该方法存于libsystem_blocks.dylib
中,至此我们知道了block
的源码的位置。源码为libclosure-74
。
查找Block
的结构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
};
这是Block底层的真正形式。
我们继续断点研究。读取当前的寄存器。
在这里我们可以看到当前的Block
为NSGlobalBlock
,并且Block
的签名为aaa@qq.com?0
然后对block
做如下修改,源码如下
int a = 10;
void (^block1)(void) = ^{
NSLog(@"block = %d", a);
};
block1();
打印寄存器的第一个值。如下
它的类型为NSStackBlock
,可是我们知道,当前的Block
应该在堆区,即NSMallocBlock
,因此我们可知在objc_retainBlock
中将栈区拷贝到了堆区。
进入__Block_copy
中,在retq
中做断点,查看。
这也就验证了__Block_copy
中进行将block从栈区拷贝到堆区。
这两块代码用于研究。
接下来的操作就是获取Block_layout
中的invoke
进行调用操作。Block_descriptor_1
为必选参数。
而Block_descriptor_2
与Block_descriptor_3
为可选参数。
查看Block_descriptor_2
的获取,在runtime.cpp
文件中,源码如下:
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;
}
这里就用到了flags
的值。如果没有则直接返回空,该函数为cpoy
和dispose
函数
查看Block_descriptor_3
的获取函数,源码如下
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的结构图如下
其中2
和3
可能不存在。那么我们进行获取时,需要进行平移获取。
确认当前的Block
获取当前block
的内存信息
获取Block_descriptor_2
打印结果可知,Block_descriptor_2
为空。
获取Block_descriptor_3
获取可知有值,并且可以获取到签名。
Block_copy
我们已经知道了Block的调用,可是我们还不知道Block
怎么从栈区
拷贝的堆区
的,从上面的分析中我们知道在底层调用_Block_copy
函数之后出现了变化,我们直接查看Block
的源码,找到_Block_copy
方法,源码如下
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) {
// 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,因为无法直接创建堆区的block
// 创建
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.
// invoke的拷贝
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的三层拷贝
从之前clang
编译出的的.cpp
文件中,我们可以看到如下的代码
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
这两个函数中,从名字中我们可知是来自于Block_descriptor_2
,因为在Block_descriptor_2
中包含了两个方法copy
与dispose
。
从编译后的源码中,可知,在进行copy
时,使用_Block_object_assign
进行对象的引用,我们在源码中找到该引用方式,源码如下。
// 捕获外界变量的操作。即 __block 的操作。
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: // 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: // 使用__weak修饰对象
/*******
// 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;
}
}
查看BLOCK_FIELD
的类型源码如下
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ... 普通对象
BLOCK_FIELD_IS_BLOCK = 7, // a block variable block变量
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers 弱引用对象
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
查看_Block_byref_copy
方法源码,如下
static struct Block_byref *_Block_byref_copy(const void *arg) {
// Block_byref 结构体
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 生成一份
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
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;
// 内部持有的Block_byref对象与外界持有的对象是同一个。
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
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) {
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_2
与Block_byref_3
的源码,如下
// 使用__block修饰就会存在的结构体
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
// 存在copy与dispose时存在
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;
};
修改对象类型,从clang
文件中查找main
函数,源码如下
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
__attribute__((__blocks__(byref))) __Block_byref_string_0 string = {(void*)0,(__Block_byref_string_0 *)&string, 33554432,
sizeof(__Block_byref_string_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"),
sel_registerName("stringWithFormat:"),
(NSString *)&__NSConstantStringImpl__var_folders_nb_ghsrc7cn0xb_hgl5rt12y4340000gn_T_main_b3a94a_mi_0)
};
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_string_0 *)&string, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
上方代码与之前相比多了__Block_byref_id_object_copy_131
方法。再次查看__Block_byref_string_0
结构体,源码如下
// 编译拷贝
struct __Block_byref_string_0 {
void *__isa;
__Block_byref_string_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSString *string;
};
查看__Block_byref_id_object_copy_131
方法,如下
// __block 修饰的变量,对byref结构体的拷贝
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
// dst 是当前的结构体,+40是为了进行内存平移,评一个的大小为__Block_byref_string_0找到最后的string的值
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
而__Block_byref_string_0
结构体中的string
是在初始化中进行赋值的,赋值内容为
((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_nb_ghsrc7cn0xb_hgl5rt12y4340000gn_T_main_b3a94a_mi_0)
总结
-
block
自身的拷贝 - 对
__block
修饰的变量变成bref
的结构体进行拷贝 - 如果
bref
中存在对象,即捕获的变量是对象变量时,通过bref
结构体的内存平移,并且使用_Block_object_assign
方法进行指针拷贝。
上一篇: IDEA常用设置