iOS底层 - 谈Objective-C block的实现(下)

730 阅读11分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战
  21. iOS底层原理探索 之 GCD函数和队列
  22. iOS底层原理探索 之 GCD原理(上)
  23. iOS底层 - 关于死锁,你了解多少?
  24. iOS底层 - 单例 销毁 可否 ?
  25. iOS底层 - Dispatch Source
  26. iOS底层 - 一个栅栏函 拦住了 数
  27. iOS底层 - 不见不散 的 信号量
  28. iOS底层 GCD - 一进一出 便成 调度组
  29. iOS底层原理探索 - 锁的基本使用
  30. iOS底层 - @synchronized 流程分析
  31. iOS底层 - 锁的原理探索
  32. iOS底层 - 带你实现一个读写锁
  33. iOS底层 - 谈Objective-C block的实现(上)

以上内容的总结专栏


细枝末节整理


前言

上一篇,我们所讲述的内容其实都是 Block 的最基础的部分(如何定义、如何使用)。今天我们 重点从面试题出发,看 Block 有哪些内容,是我们平时开发中所并没有注意到的细节。以及面试中经常遇到的问题。

你没注意到的 Block 细节

Block 有几种类型

我们通过下面的代码打印,来看一下:

  • 我们定义一个无参数无返回值的Block:
    void(^myBlock)(void) = ^(){};
    NSLog(@"%@", myBlock);

打印内容:
    <__NSGlobalBlock__: 0x1043b0160>

这是一个全局Block,位于全局区, 在 Block 内部不使用外部变量或只使用静态变量和全局变量。

  • 我们定义一个无参数无返回值的Block,在内部打印下外部的变量:
    int a = 20;
    void(^myBlock)(void) = ^(){

        NSLog(@"myBlock -- %d", a);
    };

打印内容:
    <__NSMallocBlock__: 0x600003767120>

这是一个堆Block,位于堆区, 在Block内部使用了变量或者OC的属性,并赋值给强引用或Copy修饰的变量。( 它捕获了外部变量,是一个默认的强持有; block 持有的是 block 实现部分的内存空间。)

  • 在上面的基础上,我们在myBlock之前 加上一个 __weak :
    int a = 20;
    void(^__weak myBlock)(void) = ^(){

        NSLog(@"myBlock -- %d", a);
    };

打印内容:
    <__NSStackBlock__: 0x7ffeecd3e028>

这是一个栈Block, 位于 栈区, 与 MallocBlock一样,可以在内部使用局部变量或者OC属性,但是不能赋值给强引用或者Copy修饰的变量。

Block - 引用计数问题

我们看一到面试题:

    NSObject *objc = [NSObject new];
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); // 1

    void(^strongBlock)(void) = ^{ 
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    strongBlock();

    void(^__weak weakBlock)(void) = ^{ 
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    weakBlock();
    
    void(^mallocBlock)(void) = [weakBlock copy];
    mallocBlock();

我们分析一下,首先,在第一个打印的地方, 输出为 1 ,这一点没什么问题吧;

第二处打印, 在 strongBlock 处会打印什么呢? 在这一步,其实 block 对 objc 进行了一个捕获(在底层对于objc的捕获其实是生成了成员变量来 持有) 这里引用计数会 +1(这是一步属性的持有) , 其次,这里的 strongBlock 是一个堆区的block 在底层源码中,我们可以看到,在block捕获外部变量的时候,会进行内存拷贝, 所以引用计数要再 +1 ,所以这里会是3;

第三处,我们这里的block用了 __weak 修饰, 是一个栈 Block 并不会进行内存拷贝,所以这里只会 +1;

最后,我们对 栈block进行了一个 copy, 到了堆上面,所以,会 +1 , 也就是和第三部分加起来,就是第二部分。

好,最后看下打印情况(看到这里,可能你还是不太明白第二行的打印,我们会在下一篇分析Block底层结构的时候详细分析):

1
---3
---4
---5

Block - 内存拷贝理解

再来看一到面试题:

    int a = 0;
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"-----%d", a);
    };
    struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
  
    id __strong strongBlock = weakBlock;
    blc->invoke = nil;
    void(^strongBlock1)(void) = strongBlock;
    strongBlock1();

一般的,我们定义了一个int ,在 __weak weakBlock 中打印了 a, 下面我们自定义了一个block 将weakBlock进行了类型转换, 在下一行,我们通过 __strongBlock 对 weakBlock 进行一个持有;接着对 blc的invoke进行置nil,也就是对block对执行置为nil。

我们当前确实是将类型强转了过来,如果 blc 和 weakBlock 同为 栈上的同一片内存空间,后面我们对 blc->invoke = nil,之后,也达到了对 weakBlock 操作的效果,所以weakBlock最后也是无法调用的。

image.png 接下来执行验证一下:

image.png

调用的时候就崩溃了哦。

那么,如何不奔溃呢? 我们需要进行一步 copy (赋值一份新的内容到 strongBlock) ,这样就到了堆上(他们的内存都不在一样了):

    id __strong strongBlock = [weakBlock copy];

image.png 再运行就不会崩溃了。

Block - 堆栈释放差异

同样的面试题开始:

- (void)blockDemo {
    NSObject *a = [NSObject alloc];
    void(^__weak weakBlock)(void) = nil;
    {
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"---%@", a);
        };
        weakBlock = strongBlock;
        NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
    }
    weakBlock();
}

此处,打印什么?

1 - <__NSStackBlock__: 0x7ffee27d3000> - <__NSStackBlock__: 0x7ffee27d3000>
---(null)

和你想的一样吗?

首先, weakBlock 的声明周期 在我们的 blockDemo 方法内, 在我们 blockDemo 方法内部, strongBlock 定义在 方法内的一个代码块内,将其复赋值给 weakBlock 后,它就没什么意义了。

接下来,我们做一下修改(这个时候,会打印什么内容):

    //把这里的 __weak 去掉
    void(^strongBlock)(void) = ^{
        NSLog(@"---%@", a);
    };

打印如下:

image.png

打印完 1- 之后,会奔溃。

我们来分析下,为什么会是这样。 首先,此时 strongBlock 是一个堆区 Block, 其生命周期 存在于 我们方法内部的 代码块中:

    {
        void(^strongBlock)(void) = ^{
            NSLog(@"---%@", a);
        };
        weakBlock = strongBlock;
        NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
    }

image.png

在这里 weakBlock 和 strongBlock 指向同一片内存空间。 随着 断点这一行,走出代码块之后, strongBlock所指向的内存空间 生命走到尽头,会被系统回收,那么,

image.png

我们在调试一下,看看为什么加上 __weak 就可以都打印呢?

image.png

这里都是栈Block,它们存在与栈内存空间,栈的栈帧在 当前的函数 栈帧区域内。 关于栈帧,我们在 iOS 底层原理探索 之 阶段总结 与你分享一份面试题关于iOS底层原理 中有详细的探索,如有需要,请移步阅读(参数入栈,结构体入栈)。

Block - 拷贝到堆 Block

  • 手动copy
  • Block作为返回值
  • 被强引用 或 Copy 修饰
  • 系统 API 包含 usingBlock

通过有无捕获外界变量可以区分堆Block和全局Block, 堆Block和栈Block的区别是内部使用局部变量或者属性,有没有赋值给强引用或者Copy修饰的变量。

Block 循环引用问题

循环引用

我们通过一个图,来看下循环引用问题

循环引用问题图例 Block循环引用问题.001.jpeg

  • 正常情况下,A对象持有B对象的时候,B对象retainCount会+1,当A对象释放的时候,发送dealloc给B,这个时候,如果B的retainCount==0,则B对象调用dealloc释放。

  • 如果A对象和B对象互相持有,这种情况,A和B对象均无法调用dealloc给对方发送释放信号,这就导致了循环引用的问题。

我们举一个最常见的循环引用的例子:

例1

typedef void (^myBlock)(void);

...
@interface ViewController ()

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) myBlock block;

@end
...
- (void)viewDidLoad {

    [super viewDidLoad];
    self.name = @"superman";
    
    self.block = ^{
      
        NSLog(@"%@", self.name);
    };
    
    self.block();
}

...

- (void)dealloc {
   
    NSLog(@"销毁咯~~~");
}

但是,我们在下面的block中,并不会引发循环引用:

例2

    [UIView animateWithDuration:1 animations:^{
        NSLog(@"%@", self.name);
    }];

那么,这两点有什么不一样的呢?

因为 例2 中,是UIView持有了block,并不是self做的持有。 而,例1 中,是self 持有的 block,block在执行过程中捕获了self。这样就是循环引用图例中循环引用的问题。

如何解决

要解决 循环引用 的问题,就要打破这种双发互相持有的状态。

最常用的方法,就是使用 __weak :

    __weak __typeof__(self) weakSelf = self;
    self.block = ^{
      
        NSLog(@"%@", weakSelf.name);
    };
    
    self.block();

成功的销毁了 image.png

处理到这一步其实是不完善的,举例如下:

例3

    __weak __typeof__(self) weakSelf = self;
    self.block = ^{
              dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            NSLog(@"%@", weakSelf.name);
        });
    };
    
    self.block();

当我们的self释放的在这个延迟函数之前的话,会有如下情况的打印:

销毁咯~~~
(null)

怎么解决呢? 加一个 strong (weak_strong_dance 强弱共舞):

    __weak __typeof__(self) weakSelf = self;
    self.block = ^{
        __strong __typeof(weakSelf) strongSelf = weakSelf;

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            NSLog(@"%@", strongSelf.name);
        });
    };
    
    self.block();

这样就解决了上面打印为null的问题(这里的strongSelf只是一个零食变量,作用域范围是 block 实现的部分,代码执行完毕后,就释放了,释放后,循环引用互相持有的状态就打破了)。

其他解决方法探索

我喜欢手动挡🚗

当前我们的selfblock互相持有,那么,我们会考虑是否可以手动来将self置为nil,以达到将这个互相持有的状态打破的目的。

如何实现这个想法呢 ? 也很简单:

    __block UIViewcontroller *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();

同样完美的解决了(这里vc是一个临时变量,对self持有;在对vc置nil之前block实现中是它的作用域范围,之后,self就是一个正常的self,所以 可以正常的释放)。

参数传递方式

很显然,循环引用是因为在block中我们要使用self的属性。 其实在 viewDieLoad 方法中,self是通过栈帧传参过来的,我们是要做其实就是一个通讯的操作, 既然是一个通讯的操作,那么我们可以通过 代理 、 协议 、参数、 通知等方式来操作。 这里我们就使用一下参数:

例4

typedef void (^myBlock)(UIViewController *);

...

    self.name = @"superman";
    
    self.block = ^(UIViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            NSLog(@"%@", vc.name);
        });
    };
    
    self.block(self);

同样,完美的解决了循环引用的问题(block能够对外界变量进行捕获,然而我们通过参数传递过去,就不会有捕获的问题了)。

Block 面试题

static ViewController *staticSelf_;

...

// 1
- (void)blockWeak_static {

    __weak typeof(self) weakSelf = self;
    staticSelf_ = weakSelf;
}

// 2
- (void)block_weak_strong {

    __weak typeof(self) weakSelf = self;
    self.doWork = ^{
        __strong typeof(self) strongSelf = weakSelf;
        weakSelf.doStudent = ^{
            NSLog(@"%@", strongSelf);
        };
       weakSelf.doStudent();
    };
   self.doWork();
}

问: 1 和 2 是否会引起循环引用?

通过实际测试, ViewController并不会释放掉,都会引起循环引用。 why ?


第一:__weak 我们是一个弱引用,为什么还是会循环引用呢? static全局静态变量weakSelf 进行持有 weakSelfself 进行持有。

weakSelf 和 self 是一个 映射关系 ;它们是 同一片内存空间;怎么解释呢?看下图: self, weakSelf,staticSelf_都指向同一片内存空间, staticSelf对self所指向的内存空间进行了持有,就是相当于对self进行了持有。

image.png


第二:我们的 doWork 这个 block 进来后 strongSelf(stongSelf 是一个临时变量) 对 weakSelf 有一个持有, strongSelf 对生命周期在 doWork 的实现范围内。那么代码会执行到 weakSelf 的 doStudent 。在这里 doStudent 中会对 strongSelf 有一个持有, 那么strongSelf的retainCount 会 +1;尽管,在doWork作用域范围外,会对 strongSelf 进行 release 操作,strongSelf 的 retainCount 并没有 =0 ;所以 释放不掉;

我们最后,在 doStudent 中使用完 strongSelf 后,手动将 strongSelf 置为 nil,就可以。 或者 不要在 doStudent中 使用 strongSelf。

总结

今天,我们结合面试题,对Block对分类进行了探索,对于Blcok使用中的问题以及解决方法也都有探索。本片重点的篇幅还是对于Block的面试题进行了详尽的分析,肯定到现在,大家还是对于理解存在一定的问题;那么,下一篇,我们深入到Blcok的底层实现源码来分析,我想那个时候,大家心中很多的问题就会引刃而解了。好的,大家加油!!!!