这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
复制代码
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa – 类的底层原理结构(上)
- iOS 底层原理探索 之 isa – 类的底层原理结构(中)
- iOS 底层原理探索 之 isa – 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
- iOS 底层原理探索 之 分类的加载
- iOS 底层原理探索 之 关联对象
- iOS底层原理探索 之 魔法师KVC
- iOS底层原理探索 之 KVO原理|8月更文挑战
- iOS底层原理探索 之 重写KVO|8月更文挑战
- iOS底层原理探索 之 多线程原理|8月更文挑战
- iOS底层原理探索 之 GCD函数和队列
- iOS底层原理探索 之 GCD原理(上)
- iOS底层 – 关于死锁,你了解多少?
- iOS底层 – 单例 销毁 可否 ?
- iOS底层 – Dispatch Source
- iOS底层 – 一个栅栏函 拦住了 数
- iOS底层 – 不见不散 的 信号量
- iOS底层 GCD – 一进一出 便成 调度组
- iOS底层原理探索 – 锁的基本使用
- iOS底层 – @synchronized 流程分析
- iOS底层 – 锁的原理探索
- iOS底层 – 带你实现一个读写锁
- 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最后也是无法调用的。
接下来执行验证一下:
调用的时候就崩溃了哦。
那么,如何不奔溃呢? 我们需要进行一步 copy (赋值一份新的内容到 strongBlock) ,这样就到了堆上(他们的内存都不在一样了):
id __strong strongBlock = [weakBlock copy];
复制代码
再运行就不会崩溃了。
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);
};
复制代码
打印如下:
打印完 1- 之后,会奔溃。
我们来分析下,为什么会是这样。
首先,此时 strongBlock 是一个堆区 Block, 其生命周期 存在于 我们方法内部的 代码块中:
{
void(^strongBlock)(void) = ^{
NSLog(@"---%@", a);
};
weakBlock = strongBlock;
NSLog(@"1 - %@ - %@",weakBlock,strongBlock);
}
复制代码
在这里 weakBlock 和 strongBlock 指向同一片内存空间。 随着 断点这一行,走出代码块之后, strongBlock所指向的内存空间 生命走到尽头,会被系统回收,那么,
我们在调试一下,看看为什么加上 __weak 就可以都打印呢?
这里都是栈Block,它们存在与栈内存空间,栈的栈帧在 当前的函数 栈帧区域内。
关于栈帧,我们在 iOS 底层原理探索 之 阶段总结 与你分享一份面试题关于iOS底层原理 中有详细的探索,如有需要,请移步阅读(参数入栈,结构体入栈)。
Block – 拷贝到堆 Block
- 手动copy
- Block作为返回值
- 被强引用 或 Copy 修饰
- 系统 API 包含 usingBlock
通过有无捕获外界变量可以区分堆Block和全局Block, 堆Block和栈Block的区别是内部使用局部变量或者属性,有没有赋值给强引用或者Copy修饰的变量。
Block 循环引用问题
循环引用
我们通过一个图,来看下循环引用问题
循环引用问题图例
-
正常情况下,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();
复制代码
成功的销毁了
处理到这一步其实是不完善的,举例如下:
例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 实现的部分,代码执行完毕后,就释放了,释放后,循环引用互相持有的状态就打破了)。
其他解决方法探索
我喜欢手动挡?
当前我们的self
和block
互相持有,那么,我们会考虑是否可以手动来将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
进行持有 weakSelf
对 self
进行持有。
weakSelf 和 self 是一个 映射关系
;它们是 同一片内存空间
;怎么解释呢?看下图: self, weakSelf,staticSelf_都指向同一片内存空间, staticSelf对self所指向的内存空间进行了持有,就是相当于对self进行了持有。
第二:我们的 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的底层实现源码来分析,我想那个时候,大家心中很多的问题就会引刃而解了。好的,大家加油!!!!