今天我们继续探索block底层原理,为什么会造成循环引用,block的原理,block的结构等。
一. block的底层
1、block的循环引用
-(instancetype)init {
? ? if (self = [super init]) {
? ? ? ? void(^block)(void) = ^{
? ? ? ? ? ? _name;
? ? ? ? };
? ? }
? ? return self;
}
复制代码
这段代码会循环引用吗?答应是:会。为什么呢? 现在我们用clang来看看源码:
NSObject *objc = [NSObject new];
? ? void (^block)(void) = ^ {
? ? ? ? NSLog(@"%@",objc);
? ? };
复制代码
clang -rewrite-objc main.m 后查看源码: 第一个问题得到答应:_name在block捕获中又变成了self。所以还是会循环引用。 那要如何解决循环引用呢?
? ? self.block = ^{
? ? ? ? weakself.name = @"lg";
? ? };
复制代码
还有一个小细节,如果在block中使用延迟操作:
__weak typeof(self) weakself = self;
? ? self.name = @"lg";
? ? self.block = ^{
? ? ? ? dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
? ? ? ? ? ? NSLog(@"%@",weakself.name);
? ? ? ? });
? ? };
**2022-06-19 17:41:31.215393+0800 Block的循环引用[13967:532378] -[LGViewController dealloc]**
**2022-06-19 17:41:32.146640+0800 Block的循环引用[13967:532378] (null)**
复制代码
最后打印结果为null,name被提前释放了?怎么办和我们想要的效果不一样啊。为解决这个问题使用了__strong 在block在强引一次。
self.name = @"lg";
? ? __weak typeof(self) weakSelf = self;
? ? self.block = ^{
? ? ? ? __strong typeof(weakSelf) strongSelf = weakSelf;
? ? ? ? NSLog(@"%p",&strongSelf);
? ? ? ? dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
? ? ? ? ? ? NSLog(@"%@",strongSelf.name);
? ? ? ? });
? ? };
? ? self.block();
**2022-06-19 17:45:21.113499+0800 Block的循环引用[14067:535530] 0x7ff7b0cdaf78**
**2022-06-19 17:45:24.113992+0800 Block的循环引用[14067:535530] lg**
**2022-06-19 17:45:24.114181+0800 Block的循环引用[14067:535530] -[LGViewController dealloc]**
复制代码
发现打印先打印了lg,在执行dealloc释放 。还有其他方法可以打破循环引用吗?当然有:使用__block
self.name = @"lg";
? ? __block LGViewController *vc = self;
? ? self.block = ^{
? ? ? ? dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
? ? ? ? ? ? NSLog(@"%@",vc.name);
? ? ? ? ? ? vc = nil; //手动释放
? ? ? ? });
? ? };
? ? self.block();
**2022-06-19 17:55:33.469556+0800 Block的循环引用[14313:543068] lg**
**2022-06-19 17:55:33.469761+0800 Block的循环引用[14313:543068] -[LGViewController dealloc]**
复制代码
还有一种block传参的方式:
self.name = @"lg";
? ? self.block = ^(LGViewController *vc){
? ? ? ? dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
? ? ? ? ? ? NSLog(@"%@",vc.name);
? ? ? ? });
? ? };
? ? self.block(self);
**2022-06-19 17:59:17.416274+0800 Block的循环引用[14413:546339] lg**
**2022-06-19 17:59:17.416459+0800 Block的循环引用[14413:546339] -[LGViewController dealloc]**
复制代码
block不会捕获传入的参数。只会捕获外部的变量。
1.__strong 2.__block 3.block传参数 中方式都能解决block循环引用问题。 现在看几个例题:(1.下面代码会循环引用吗)
static LGViewController *_staticSelf;
-(void)test1 {
? ? __weak typeof(self) weakSelf = self;
? ? _staticSelf = weakSelf;
}
复制代码
答案是:会 ,为什么呢?因为weakSelf 指向了self的内存地址,然后_staticSelf静态的变量又指向了weakSelf=self;_staticSelf静态的变量的生命周期是整个程序的生命周期 ,所以self被_staticSelf所持有 ,无法在当前的controller的生命周期释放。 (2.下面代码会循环引用吗)
__weak typeof(self) weakSelf = self;
? ? self.block1 = ^{
? ? ? ? __strong typeof(weakSelf) strongSelf = weakSelf;
? ? ? ? NSLog(@"%p", &strongSelf);
? ? ? ? weakSelf.block2 = ^{
? ? ? ? ? ? NSLog(@"%@", strongSelf);
? ? ? ? };
? ? ? ? weakSelf.block2();
? ? };
? ? self.block1();
复制代码
答案是:会 ,为什么呢?因为在weakSelf.block2是强引用又在捕获了一次self的地址 。虽然在外部block1中会对weakSelf进行一次计数--,但是block2已经又累加了一次。所以self无法释放。
2、block的原理
1、block的本质
int a = 10;
? ? void (^block)(void) = ^{
? ? ? ? NSLog(@"%d",a);
? ? };
? ? block();
复制代码
同上面一样用clang -rewrite-objc main.m 后查看源码:
去找第一个参数:__main_block_func_0 ->__main_block_impl_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
? int a = __cself->a; // bound by copy
? ? ? ? NSLog((NSString *)&__NSConstantStringImpl__var_folders_v9_ry9xdp0n3mb9mfz00s2ckzt80000gn_T_main_93da1c_mi_0,a);
? ? }
复制代码
最后需要用block()来调用FuncPtr的代码块 。 那么继续分析代码:
int b = 20;
static int c = 30;
int main(int argc, const char * argv[]) {
? ? static int a = 10;
? ? __block NSObject *objc = [NSObject new];
? ? NSLog(@"%@",objc);
? ? void(^block)(void) = ^{
? ? ? ? a ++;
? ? ? ? b ++;
? ? ? ? c ++;
? ? ? ? NSLog(@"%d %d %d %@",a,b,c,objc);
? ? };
? ? NSLog(@"%@",block);
? ? block();
? ? return NSApplicationMain(argc, argv);
}
**2022-06-19 20:54:45.564308+0800 Block[18008:647433] <NSObject: 0x600001664280>**
**2022-06-19 20:54:45.564446+0800 Block[18008:647433] <__NSMallocBlock__: 0x600001a389f0>**
**2022-06-19 20:54:45.564509+0800 Block[18008:647433] 11 21 31 <NSObject: 0x600001664280>**
复制代码
block为什么可以修改,全局和静态变量的值呢? clang -rewrite-objc main.m 后查看源码: 由此可见,static int a 和 int a 捕获的方式不一样,static a是捕获地址指针, a 完全是一个新的a和内存地址。__block不可以?于修饰静态变量和全局变量。
struct __Block_byref_objc_0 {
? void *__isa;
__Block_byref_objc_0 *__forwarding;
?int __flags;
?int __size;
?void (*__Block_byref_id_object_copy)(void*, void*);
?void (*__Block_byref_id_object_dispose)(void*);
?NSObject *objc;
};
复制代码
从源码看到:__attribute__((__blocks__(byref))) __Block_byref_objc_0 objc = 创建的block 进行赋值给__Block_byref_objc_0 ;
查看源码可知:
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 指向的拷贝到堆上的byref
? ? ? ? *dest = _Block_byref_copy(object);
? ? ? ? break;
复制代码
如果参数是__block 修饰的参数就会执行_Block_byref_copy :
block初始化的时候在栈上, 当copy到堆上时,整个block结构体包括捕获的对象进行拷贝到堆上。原来的 __forwording也会指向堆上的 byref。dest 二级指针,最终指向堆上的byref 。 Block_layout 类型的结构体: 小结: block的本质是?个 Block_layout 类型的结构体 。copy和dispose函数 是?来对block内部的对象进?内存管理的,block拷?到堆上会调?copy函数,在block从堆上释放的时候会调?dispose函数。
2、block的底层原理
进入源码:
// Copy, or bump refcount, of a block.? If really copying, call the copy helper if present.
// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
// 运行?stack -》malloc
void *_Block_copy(const void *arg) {
? ? struct Block_layout *aBlock;
? ? // 如果 arg 为 NULL,直接返回 NULL
? ? if (!arg) return NULL;
? ? // The following would be better done as a switch statement
? ? // 强转为 Block_layout 类型
? ? aBlock = (struct Block_layout *)arg;
? ? const char *signature = _Block_descriptor_3(aBlock)->signature;
? ? // 如果现在已经在堆上
? ? if (aBlock->flags & BLOCK_NEEDS_FREE) {
? ? ? ? // latches on high
? ? ? ? // 就只将引用计数加 1
? ? ? ? latching_incr_int(&aBlock->flags);
? ? ? ? return aBlock;
? ? }
? ? // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
? ? else if (aBlock->flags & BLOCK_IS_GLOBAL) {
? ? ? ? return aBlock;
? ? }
? ? else {
? ? ? ? // Its a stack block.? Make a copy.
? ? ? ? // block 现在在栈上,现在需要将其拷贝到堆上
? ? ? ? // 在堆上重新开辟一块和 aBlock 相同大小的内存
? ? ? ? struct Block_layout *result =
? ? ? ? ? ? (struct Block_layout *)malloc(aBlock->descriptor->size);
? ? ? ? // 开辟失败,返回 NULL
? ? ? ? if (!result) return NULL;
? ? ? ? // 将 aBlock 内存上的数据全部复制新开辟的 result 上
? ? ? ? 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
? ? ? ? // 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
? ? ? ? result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);? ? // XXX not needed
? ? ? ? // 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
? ? ? ? result->flags |= BLOCK_NEEDS_FREE | 2;? // logical refcount 1
? ? ? ? // copy 方法中会调用做拷贝成员变量的工作
? ? ? ? _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;
? ? }
}
复制代码
通过下面代码:
int a = 7;
? ? void (^block)(void) = ^{
? ? ? ? NSLog(@"%d",a);
? ? };
? ? block();
复制代码
读取寄存器打印: 添加符号断点:objc_retainBlock->_Block_copy
NSObject *objc = [NSObject new];
? ? void (^block)(void) = ^{
? ? ? ? NSLog(@"%@",objc);
? ? };
? ? block();
复制代码
重复上面操作(查看寄存器):
_Block_copy只会在捕获到外部变量(对象类型的变量)的时候才会有copy和dispose函数 。 看下Block_descriptor_2是如何获取的:
// 取得 block 中的 Block_descriptor_2,它藏在 descriptor 列表中
// 调用者:_Block_call_copy_helper() / _Block_call_dispose_helper
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
? ? // Block_descriptor_2 中存的是 copy/dispose 方法,如果没有指定有 copy / dispose 方法,则返回 NULL
? ? if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
? ? // 先取得 Block_descriptor_1 的地址
? ? uint8_t *desc = (uint8_t *)aBlock->descriptor;
? ? // 偏移 Block_descriptor_1 的大小,就是 Block_descriptor_2 的起始地址
? ? desc += sizeof(struct Block_descriptor_1);
? ? return (struct Block_descriptor_2 *)desc;
}
复制代码
偏移 Block_descriptor_1 的大小,就是 Block_descriptor_2 的起始地址 同理Block_descriptor_3的获取就是 偏移 Block_descriptor_2 的大小,就是Block_descriptor_3的起始地址了。
通过一个小例子(巩固知识体系):
- (void)blockDemo1{
? ? int a = 1;
? ? void(^ __weak weakBlock)(void) = ^{
? ? ? ? NSLog(@"-----%d", a);
? ? };
? ? struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
? ? void(^strongBlock)(void) = weakBlock;
? ? blc->invoke = nil;
? ? strongBlock();
}
//自定义的Block结构
struct _LGBlock {
? ? void *isa;
? ? volatile int32_t flags; // contains ref count
? ? int32_t reserved;
? ? // 函数指针
? ? LGBlockInvokeFunction invoke;
? ? struct _LGBlockDescriptor1 *descriptor;
};
复制代码
分析:栈weakBlock 打印了a,然后赋值给了strongBlock 堆block指针,在把栈weakBlock的invoke = nil; 调用strongBlock() 应该是会崩溃报错。(因为strongBlock其实指向的还是weakBlock的内存地址,调用也是weakBlock->invoke 这时候invoke已经被赋值为nil了)
3、总结
1.block的本质: block的本质是?个 Block_layout 类型的结构体。copy和dispose函数是?来对block内部的对象进?内存管理的 ,block拷?到堆上会调?copy函 数,在block从堆上释放的时候会调?dispose函数。
2.block的底层原理: ?__block修饰的变量在编译过后会变成 __Block_byref__XXX 类型的结构体,在结构体内部有?个 __forwarding 的结构体指针,指向结构体本身 。
block创建的时候是在栈上的,在将栈block拷?到堆上的时候,同时也会将block中捕获的对象拷?到堆上,然后就会将栈上的__block修饰对象的__forwarding指针指向堆上的拷?之后的对象 。这样我们在block内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的__forwarding指针将堆和栈的对象链接起来 。因此就可以达到修改的?的。
expression指令 :简写为 e ,能够在调试时,动态的修改变量的值,同时打印出结果,还可以动态调?函数。它会实时的真正的执?后?的代码。
|