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

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

Block简介

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

  • 匿名函数
    • 匿名函数式指不带函数名称的函数
  • 带有自定变量
    • Block拥有捕获外部变量的功能,在Block中访问一个外部的局部变量,Block会持有它的临时状态,自动捕获变量值,外部局部变量的变化不会影响它的状态(这个下面会讲到)。

Block类型

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

block主要有三种类型

  • 1.__NSGlobalBlock__:全局block,存储在全局区

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

  • 2.__NSMallocBlock__:堆区block,因为block既是函数,也是对象

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

  • 3.__NSStackBlock__:栈区block

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

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

总结

  • 1.block是直接存储在全局区
  • 2.block如果访问外界变量,并进行block相应copy
    • 如果此时的block是强引用,则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相互持有

下面来解决循环引用

  • 1.weak-strong-dance(最常用的方法)
  • 2.__block修饰对象,同时置nil
  • 3.传递对象self作为block的参数,提供给block内部使用
  • 4.使用NSProxy

weak-strong-dance(弱强共舞)

  • 1.如果block内部并未嵌套block,直接使用__weak修饰self即可
@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)]

  • 2.如果block内部嵌套block,则需要同时使用__weak和__strong

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

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

__block修饰变量

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

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

对象self作用参数

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

使用NSProxy虚拟类

  • OC是只能单继承的语言,但它是基于运行时的机制,所以可以通过NSProxy来实现伪多继承,填补多继承的空白
  • NSProxyNSObject是同级的类,是个虚拟类,只是实现了NSObject的协议
  • NSProxy其实是一个消息重定向封装的一个抽象类,类似一个代理人,中间件,可以通过继承它,并重新写下面的两个方法来实现消息转发到另一个实例

使用场景

  • 1.实现多继承功能
  • 2.解决NSTimer&CADisplayLink创建时对self强引用问题,这个在YYKit中YYWeakProxy有所使用的

循环引用解决原理

主要是通过自定义的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的强引用问题

运行打印:

总结

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

  • 1.打破self对block的强引用,这需要对block进行声明的时候使用weak修饰,但是这会导致block提前释放,所以这种方式不可行
  • 2.打破block对self的强引用,主要就是self的作用域block作用域数据交换问题,我们可以通过代理通知传值等几种方式,用于解决循环,我们对上面讲的列一下
    • weak-strong-dance(弱强共舞)
    • __block修饰变量
    • 对象self作用参数使用
    • 使用NSProxy子类代替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内部实现的代码块将无法执行,可以总结为以下两点

  • 1.函数声明:即block内部实现声明成了一个函数__main_block_func_0
  • 2.执行具体的函数实现:通过调用block的FuncPtr指针,调用block执行

block是如何获取外界变量的

我们将上面的代码当中调用block

再将它编译成.cpp文件

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

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

__block原理

将上面代码的局部变量a使用__block修饰

再将它编译成.cpp文件

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

  • 1.main中的a是以__Block_byref_a_0结构体的形式出现的,是封装的对象
  • 2.在结构体__Block_byref_a_0中,a的值存在int a中
  • 3.在__main_block_impl_0中,将对象a的地址&a给构造函数
  • 4.在__main_block_func_0内部对a的处理时指针拷贝,此时创建的对象a传入对象的a指向的是同一片内存空间

总结

  • 1.外界变量通过__block生成__Block_byref_a_0结构体
  • 2.结构体用来保存原始变量的指针和值
  • 3.将变量生成的结构体对象的指针地址传递给block,然后在block内部就可以对外界变量进行操作了

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

  • 非__block修饰:值拷贝 - 浅拷贝,只是拷贝数值,且拷贝的值不可更改指向不同内存空间,非__block修饰的变量a就是值拷贝
  • __block修饰:指针拷贝 - 深拷贝生成的对象指向同一片内存空间,通过__block修饰的变量a就是指针拷贝

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底层实现

说明:

  • 1.isa:指向的是block类型的类

  • 2.flags:标识符,按bit位表示一些block附加信息,类似于isa中的位域,其中flags种类有以下几种,主要重点关注BLOCK_HAS_COPY_DISPOSEBLOCK_HAS_SIGNATUREBLOCK_HAS_COPY_DISPOSE 决定是否有Block_descriptor_2BLOCK_HAS_SIGNATURE 决定是否有Block_descriptor_3

    • 第一位:BLOCK_DEALLOCATING,释放标记,一般常用 BLOCK_NEEDS_FREE位与操作,一同传入Flags,告知该block可释放
    • 第十六位:BLOCK_REFCOUNT_MASK,存储引用计数的值;是一个可选用参数
    • 第二十四位:BLOCK_NEEDS_FREE,第16是否有有效的标志,程序根据它来决定是否增加或是减少引用计数位
    • 第二十五位:BLOCK_HAS_COPY_DISPOSE,是否拥有拷贝辅助函数(a copy helper function)
    • 第二十六位:BLOCK_HAS_CTOR,是否拥有block析构函数
    • 第二十七位:BLOCK_IS_GC,标志是否有垃圾回收 //OS X
    • 第二十八位:BLOCK_IS_GLOBAL,标志是否是全局block
    • 第三十位位:BLOCK_HAS_SIGNATURE,与BLOCK_USE_STRET相对,判断当前block是否拥有一个签名。用于runtime时动态调用
  • 3.reserved:保留信息,可以理解预留位置,猜测是用于存储block内部变量信息

  • 4.invoke:是一个函数指针,指向block的执行代码

  • 5.descriptor:block的附加信息,比如保留变量数block的大小进行copy或dispose的辅助函数指针。有三类

    • Block_descriptor_1必选
    • Block_descriptor_2Block_descriptor_3可选

我们再看下他们底层实现

从上图可以知道: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(拷贝辅助函数)是否有值

  • 1.先通过p/x 1<<25,即1左移25位得到BLOCK_HAS_COPY_DISPOSE
  • 2.再拿flags与上BLOCK_HAS_COPY_DISPOSE(flags是block首地址平移8字节,即:0x00000000c1000002

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

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

  • 1.先通过p/x 1<<30,即1左移30位得到BLOCK_HAS_SIGNATURE
  • 2.再拿flags与上BLOCK_HAS_SIGNATURE(flags是block首地址平移8字节,还是:0x00000000c1000002

看到打印的结果不为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_copy源码,将block栈区拷贝至堆区
    • 如果需要释放,如果需要则直接释放
    • 如果是globalBlock,则不需要copy直接返回
    • 反之,只有两种情况:栈区blockor堆区block,由于堆区block需要申请空间,前面并没有申请空间的相关代码,所以只能是栈区block
      • 通过malloc申请内存空间用于接收block
      • 通过memmoveblock拷贝至新申请的内存中
      • 设置block对象的类型为堆区block,即result->isa = _NSConcreteMallocBlock

_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的源码
  • 1.如果是普通对象,则交给ARC处理,并拷贝对象指针,即引用计数+1,所以外界变量不能释放
  • 2.如果是block类型的,则通过_Block_copy操作,将block从栈区拷贝到堆区
  • 3.如果是__block修饰的变量,调用_Block_byref_copy函数,进行内存拷贝以及常规处理

我们看下_Block_byref_copy源码实现
  • 1.将传入对象,强转Block_byref结构体类型对象,保存一份
  • 2.没有将外界变量拷贝到堆,需要申请内存进行拷贝
  • 3.如果已经拷贝过了,则进行处理并返回
  • 4.其中copysrcforwarding指针都指向同一片内存,这也是为什么__block修饰的对象具有修改能力的原因

代码验证

写如下代码:

进行clang编译结果如下
  • 1.编译后lj_name比普通变量多了__Block_byref_id_object_copy_131和__Block_byref_id_object_dispose_131
  • 2.__Block_byref_lj_name_0结构体中多了__Block_byref_id_object_copy和__Block_byref_id_object_dispose

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

  • 1.通过__block_copy方法,将block拷贝至堆区
  • 2.通过_Block_object_assign方法正常拷贝,因为__block修饰的外界变量在底层是Block_byref结构体
  • 3.发现外部变量还存在一个对象,从bref中取出相应的对象lj_name,拷贝至block控件,才能使用(相同空间才能使用,不同则不能使用)。最后通过内存平移得到lj_name,此时的lj_name和外界lj_name是同一片内存空间(从_Block_object_assign方法中的*dest = object;看出)

三层copy总结

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

  • 【第一层】通过_Block_copy实现对象的自身拷贝,从栈区拷贝至堆区
  • 【第二层】通过_Block_byref_copy方法,将对象拷贝Block_byref结构体类型
  • 【第三层】调用_Block_object_assign方法,对__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有关的疑问,欢迎大家留言。希望大家能够相互交流、探索,一起进步!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容