将来跳槽用selector

OC底层面试知识点之 —— Block底层原理!

2021-04-24  本文已影响0人  iOS鑫

本文将介绍block的类型,循环引用的解决方法以及block底层分析

Block简介

Block定义:带有自动变量的匿名函数,它是C语言的拓展功能,之所以是扩展,是因为C语言不允许存在这样的匿名函数

Block类型

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

block主要有三种类型

此时block无参也无返回值,属于全局block

此时block会访问外界变量,即底层拷贝a,所以是堆区block

其中局部变量a在没有处理之前(即没有拷贝之前)是 栈区block, 处理后(即拷贝之后)是堆区block ,所以栈区block越来越少了

这个情况下,可以通过__weak不进行强持有,block就还是栈区block

总结

Block循环引用

【正常释放】:当A持有B,当A调用dealloc方法,给B发送release信号,B收到release信号,如果此时B的引用计数为0时,则会调用B的dealloc方法,此时A,B都能正常释放 【循环引用】:当A持有B,B同时也持有A时,此时A销毁需要B先销毁,而B销毁同样需要A先销毁,就导致相互等待销毁,此时A,B的引用计数都不为0,所以A,B此时都无法释放

解决循环引用

举个循环引用的例子:如下图

上面代码发生了循环引用,因为在block内部使用了self的name变量,导致block持有self,而self本来就持有block,就导致了self和block相互持有

下面来解决循环引用

weak-strong-dance(弱强共舞)

@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@property (nonatomic, copy) NSString *name;
@end

- (void)viewDidLoad {
    [super viewDidLoad];  
    self.name = @"man";
    __weak typeof(self) weakSelf = self;
    self.block = ^(void){
        NSLog(@"%@",weakSelf.name);
    };
    self.block();
}

由于此时的weakSelf和self指向同一片内存空间,而且使用__weak不会导致self的引用计数发生变化,可以通过打印weakSelf和self的指针地址,以及self的引用计数来验证 [图片上传失败...(image-74b93d-1619163053376)]

如果只用weak修饰,则可能出现block内部持有的对象被提前释放,为了防止block内部变量被提前释放,使用strong对引用计数+1,防止提前释放

其中strongSelf是一个临时变量,在block的作用域内,当block执行完就会释放strongSelf,这种方式属于打破self对block的强引用,依赖于中间者模式,属于自动置为nil,也就是自动释放

__block修饰变量

这种方式同样依赖于中介者模式,属于手动释放,是通过__block修饰对象,主要是因为__block修饰的对象是可以改变的

这里的block必须调用,如果不调用blockvc就不会置空,那么依旧是循环引用,self和block都不会释放

对象self作用参数

主要是将对象self作用参数提供给block内部使用不会有引用计数问题

使用NSProxy虚拟类

使用场景

循环引用解决原理

主要是通过自定义的NSProxy类的对象来代替self,并使用方法实现消息转发,下面是NSProxy子类的实现以及使用的场景

@interface LjProxy ()

@property(nonatomic, weak, readonly) NSObject *objc;

@end

@implementation LjProxy

- (id)transformObjc:(NSObject *)objc{
   _objc = objc;
    return self;
}

+ (instancetype)proxyWithObjc:(id)objc{
    return  [[self alloc] transformObjc:objc];
}

// 有了方法签名之后就会调用方法实现
- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.objc respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.objc];
    }
}

// 查询该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    NSMethodSignature *signature;
    if (self.objc) {
        signature = [self.objc methodSignatureForSelector:sel];
    }else{
        signature = [super methodSignatureForSelector:sel];
    }
    return signature;
}

- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.objc respondsToSelector:aSelector];
}

@end

自定义Man和Teacher类

@implementation Man

- (void)likeFood {
    NSLog(@"%@-->牛肉", self);
}

@end

@implementation Teacher

- (void)likeWork {
    NSLog(@"%@->教书育人", self);
}

@end
通过LjProxy实现多继承功能 通过LjProxy解决定时器中self的强引用问题 运行打印:

总结

循环引用解决的根本方式:

上面介绍了block的定义用法以及如何解决循环引用,下面我们来探寻下block的C++实现

Block C++实现

研究底层可以先从C++,断点调试开始

本质

创建block.c文件

通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc block.c -o block.cpp,将block.c编译成block.cpp,其中block在底层被编译成了以下的形式

相当于block等于__main_block_impl_0,是一个函数。下面查看__main_block_impl_0

通过上图我们可以知道__main_block_impl_0是一个结构体,同时可以说明block是一个__main_block_impl_0类型的对象,这也是为什么block能够%@打印的原因

我们用一张图来说明他们之间的联系

下面我们来解释几个问题

block为什么需要调用

底层block的类型__main_block_impl_0结构体,通过其同名构造函数创建,第一个传入的block的内部实现代码块,即__main_block_func_0,用fp表示,然后赋值给impl的FuncPtr属性,然后在main中进行了调用,这也是block为什么需要调用的原因。如果不调用block内部实现的代码块将无法执行,可以总结为以下两点

block是如何获取外界变量的

我们将上面的代码当中调用block 再将它编译成.cpp文件

__main_block_func_0中的a是值拷贝,如果此时在block内部实现中作 a++操作是有问题的,会造成编译器的代码歧义,即此时的a是只读的

【总结】:block捕获外界变量时,在内部会自动生成同一个属性来保存

__block原理

将上面代码的局部变量a使用__block修饰 再将它编译成.cpp文件

通过上面的截图我们可以得出以下结论:

总结

上面__block和非__block修饰局部变量产生两种不同的拷贝

Block底层原理

确定block源码位置

在main函数中写如下代码 通过在block处打断点,运行block

我们发现走到了objc_retainBlock,我们加符号断点objc_retainBlock

打印符号断点后,我们发现执行了_Block_copy,我们再加符号断点_Block_copy

此时我们需要看_Block_copy实现,它在libsystem_blocks.dylib源码中,我们去苹果官方下载下源码libclosure-74,在源码中搜索_Block_copy

通过查看_Block_copy的源码实现,发现block在底层的真正类型Block_layout

Block真正类型Block_layout

我们查看下Block_layout底层实现

说明:

我们再看下他们底层实现

从上图可以知道:Block_descriptor_2Block_descriptor_3都是通过Block_descriptor_1的地址,经过内存平移得到的

Block内存变化

根据符号断点

我们打断点运行,走到objc_retainBlock,我们打印寄存器x0

我们发现此时的block全局block,即__NSGlobalBlock__类型

我们增加外部变量a,再次运行,在相同的位置再次打印x0

此时读取block发现是栈block__NSStackBlock__

执行到符号断点objc_retainBlock时,我们发现还是栈区block

我们在增加符号断点_Block_copy,继续往下走,来到_Block_copy断点,此时打印

此时的x0地址不变,说明此时的block还是栈区block,我们在_Block_copy尾部ret处打断点,执行到断点处,再次打印

发现经过_Block_copy之后x0地址发生了变化,我们打印x0地址后发现block栈区block变为堆区block,即__NSMallocBlock__

同样上面的结论我们也可以通过读寄存器地址来得出

根据寄存器地址

我们重新运行项目,继续前面的断点,运行前面的断点,打印x0,x8,x9

此时我们看到x0x8指向的是同一块内存空间,用于存储__NSStackBlock__,此时的x9存储的是_block_invoke

我们将代码运行到41行,在次打印上面的地址

此时的x8_block_invokeblr就是跳转进入的意思,也就是要进入_block_invoke

当我们进入_block_invoke中,可以得出是通过内存平移得到block内部实现

前面提到的Block_layout结构体源码中知道其有个属性invoke,即block的执行者,是从isa首地址平移16字节得到invoke,然后进行调用执行的。

Block签名

最开始我们拿到了block的地址,前面底层我们知道block底层Block_layout的结构体

通过上图我们知道descriptor是附加信息,我们打印下它的内容

找到block地址,通过内存平移找到descriptor,然后x/8gx查看descriptor内存情况,我们前面说了descriptor会有_Block_descriptor_2或者_Block_descriptor_3,只有_Block_descriptor_3存在签名

判断是否存在_Block_descriptor_2,即flags的BLOCK_HAS_COPY_DISPOSE(拷贝辅助函数)是否有值

看到打印结果为0,表示没有Block_descriptor_2

判断是否存在Block_descriptor_3,即flags的BLOCK_HAS_SIGNATURE(是否有签名)是否有值

看到打印的结果不为0,说明有值说明是Block_descriptor_3存在签名,看descriptor,其中第三个0x0000000104d63e87表示签名。我们将签名打印出来了

下面我们通过[NSMethodSignature signatureWithObjCTypes:"v8@?0"]看下签名具体内容

下面我们具体来看下签名:

return value: -------- -------- -------- --------
        type encoding (v) 'v'
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 0}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@?' // 类型是否是@
        flags {isObject, isBlock} // @是isObject ,?是isBlock,代表 isBlockObject
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8} // 所在偏移位置是8字节

block的签名信息类似于方法的签名信息,主要体现block返回值参数以及类型等信息上。

Block的三次copy分析

Block_copy源码分析

_Block_object_assign分析

要分析block的三层copy,首先需要知道外部变量的种类有哪些,在__block的cpp文件中,对block修饰__main_block_desc_0_DATA,而__main_block_desc_0_DATA用的__main_block_copy_0,最后对a的修饰_Block_object_assign。对block修饰其中用的最多的是BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF

而_Block_object_assign是在底层编译代码中,外部变量拷贝是调用的方法就是它。看下_Block_object_assign的源码 我们看下_Block_byref_copy源码实现

代码验证

写如下代码: 进行clang编译结果如下

通过上面的分析,我们可以知道这些方法的执行顺序_Block_copy->_Block_byref_copy->_Block_object_assign,正好对应上述的三层copy 综上所述,那么block是如何拿到lj_name的呢?

三层copy总结

通过上面我们看出,block的三层拷贝指的是以下三层:

【注意】只有__block修饰的对象才三层copy

拓展

_Block_object_dispose 分析

__Block_byref_id_object_dispose_131实现中调用的就是_Block_object_dispose,下面我们看下_Block_object_dispose的底层实现:

通过源码我们可以知道_Block_object_dispose是进行release操作,通过不同分区的block,进行不同的释放操作。而_Block_object_assign是进行retain操作的,

下面看看_Block_byref_release实现 下面我们画图来更容易的了解Block的三层copy的流程

写到最后

写的内容比较多,由于本人能力有限,有些地方可能解释的有问题,请各位能够指出,同时对Block有关的疑问,欢迎大家留言。希望大家能够相互交流、探索,一起进步!

上一篇 下一篇

猜你喜欢

热点阅读