OC底层原理36:内存管理(二)強引用分析

张建 lol

前言

本文主要是通过 定时器 来梳理 强引用 的几种解决方案

強应用(強持有)

假设有 A和B 两个页面,从A push 到B页面,在B页面中有如下定时器代码,当从B pop 回到A界面时,发现定时器没有停止,其方法仍然在执行,为什么?

1
2
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

其主要原因是 B页面没有释放,即 没有执行dealloc方法,导致timer也无法停止和释放

解决方式一

  • 重写 didMoveParentViewController 方法
1
2
3
4
5
6
7
8
9
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}

解决方式二

  • 定义timer时,采用 闭包 的形式,因此不需要指定target
1
2
3
4
5
- (void)blockTimer{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire - %@",timer);
}];
}

现在,我们从底层来深入研究,为什么 B 页面有了 timer 之后,导致 B 页面释放不掉,即不会走到 dealloc 方法,我们可以通过官方文档查看 timerWithTimeInterval:target:selector:userInfo:repeats: 方法中对target的描述

从文档中可以看出,timer对传入的target具有強持有,即 timer 持有 self。由于timer是定义在B页面中,所以 self也持有timer,因此 self- > timer -> self 构成了 循环引用

OC底层原理30:Block底层原理 文章中,针对循环引用提供了几种解决方式,我们可以尝试通过 __weak弱引用 来解决,代码修改如下:

1
2
3
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

我们再次运行程序,运行 push-pop 跳转,发现 问题还是存在,即定时器方法仍然在执行,并 没有执行B的dealloc方法,为什么呢?

  • 我们使用 __weak 虽然打破了 self -> timer -> self 之前的循环引用,即引用链变成了 self -> timer -> weakSelf -> self。但是在这里我们分析的并不全面,此时还有一个 Runloop对timer的強持有,因为 Runloop生命周期B 页面 更长,所以导致了 timer无法释放,同时也导致了 B 页面的 self也无法释放。所以,最初引用链 应该是这样的:

加上 weakSelf 之后,变成了这样

weakSelf 与 self

对于 weakSelfself ,主要有以下两个疑问?

  • weakSelf 会对引用计数进行 +1 操作么?

  • weakSelfself 的指针地址相同么?是指向同一片内存么?

  • 带着疑问,我们在 weakSelf 前后打印 self 的引用计数

因此可以得出一个结论:weakSelf没有对内存进行+1操作

  • 继续打印 weakSelfself 对象,以及指针地址
1
2
3
4
5
po weakSelf
po self

p &weakSelf
p &self

打印结果如下

从打印结果可以看出,当前 self 取地址和 weakSelf 取地址的值是不一样的。意味着 两个指针地址,指向的是同一片内存空间,即 weakSelf和self的内存地址是不一样的,都指向同一片内存空间

  • 从上面打印可以看出,此时 timer 捕获的是 <LGTimerViewController: 0x102c04dd0> ,是一个 对象,所以 无法通过weakSelf来解决強持有。即引用链关系为 :NSRunloop -> timer -> weakSelf(<LGTimerViewController: 0x102c04dd0>) 。所以 RunLoop对整个对象的空间有強持有,runloop没停,timerweakSelf 是无法释放的

  • 而我们在 block 原理中提及的 block循环引用,与 timer有区别 的。通过block底层原理的方法 __Block_object_assign 可知,block捕获的是 对象的指针地址,即 weakSelf是临时变量的指针地址,跟 self 没有关系,因为 weakSelf是新的地址空间。所以此时的weakSelf相当于中间值。其引用关系链为 self -> block -> weakSelf(临时变量的指针地址),可以通过 地址 拿到 指针

所以在这里,我们需要区别下 blocktimer 循环引用的模型

  • timer模型self -> timer -> weakSelf -> self,当前的 timer 捕获的是 B页面的内存,即vc对象的内存,即 weakSelf表示的是vc对象

  • block模型self -> block -> weakSelf -> self,当前的block捕获的是 指针地址,即 weakSelf 表示的是 指向self的临时变量的指针地址

解决 強引用(強持有)

有以下几种解决思路:依赖 中介者模式、打破強持有,其中 推荐思路四

思路一:pop时在其他方法中销毁timer

根据前面的解释,我们知道由于 Runloop对timer的強持有,导致了 Runloop间接的強持有了self(因为timer中捕获的是 vc对象)。所以导致 dealloc 方法无法执行。需要查看在pop时,是否还有其他方法可以销毁 timer。这个方法就是 didMoveToParentViewController

  • didMoveToParentViewController 方法,是用于当一个视图控制器中添加或者移除 viewController后,必须调用的方法。目的是为了告诉iOS,已经完成添加/删除子控制器的操作。

  • 在B界面中重写 didMoveToParentViewController 方法

1
2
3
4
5
6
7
8
9
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}

思路二:中介者模式,即不使用self,依赖于其他对象

在timer模式中,我们重点关注的是 fireHome 能执行,并 不关心timer捕获target 是谁,由于这里 不方便使用self(因为会有强持有问题),所以 可以将target换成其他对象,例如将target换成 NSObject对象,将 fireHome 交给 target 执行

  • 将timer的target由self改成objc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.定义成其他对象
@property (nonatomic, strong)id target;

// 2.修改target
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class],@selector(fireHome),(IMP)fireHomeObjc,"v@:");
self.timer = [];

// 3.imp
- (void)fireHome{
num++;
NSLog(@"hello word - %d",num);
}
void fireHomeObjc(id obj){
NSLog(@"%s -- %@",__func__,obj);
}

// 4.dealloc
- (void)dealloc{
NSLog(@"%s",__func__);
}

查看运行结果如下

运行发现执行 dealloc 之后,timer还是会继续执行。原因是 解决了中介者的释放,但是 没有解决中介者的回收,即 self.target的回收。所以这种方式 有缺陷

可以通过在 dealloc 方法中,取消定时器来解决,代码如下:

1
2
3
4
5
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}

运行代码查看结果如下,发现 pop 之后,timer释放,从而中介者也会进行回收释放

思路三:自定义封装timer

这种方式是根据思路二的原理,自定义封装timer,其步骤如下:

  • 自定义timerWapper

    • 在初始化方法中,定义一个timer,其target是自己。即 timerWapper 中的 timer,一直监听自己,判断 selector,此时的selector已交给了传入的target(即vc对象),此时有一个方法 fireHomeWapper,在方法中,判断target是否存在

      • 如果 target存在,则需要让vc知道,即向传入的target发送selector消息,并将此时的timer作为参数也一并传入,所以vc就可以得知 fireHome 方法,这种方式是定时器方法能够执行的原因

      • 如果 target不存在,已经释放了,则释放当前的 timerWapper ,即打破了Runloop对timerWapper的強持有(timerWapper <- x - RunLoop

  • 自定义 zj_invalidate 方法中释放timer。这个方法在vc的dealloc方法中调用,即 vc释放,从而导致timerWapper释放,打破了 vctimerWapper 的強持有(vc -> x -> timerWapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//********** .h文件 *********
@interface ZJTimerWapper : NSObject
- (instancetype)zj_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)zj_invalidate;
@end

//********** .m文件 ********
@implementation ZJTimerWapper
- (instancetype)zj_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
if (self == [super init]) {
// 传入vc
self.target = aTarget;
// 传入定时器方法
self.aSelector = aSelector;

if ([self.target respondsToSelector:self.aSelector]) {
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
// 给timerWapper添加方法
class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
// 启动一个timer,target是self,即监听自己
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}

// 一直跑 runloop
void fireHomeWapper(ZJTimerWapper *warpper){
// 判断target是否存在
if (warpper.target) {
// 如果存在则需要vc知道,即向传入的target发送selector消息,并将此时的timer参数一并传入,所以vc就可以得知 fireHome 方法,这种方式就是定时器方法能够执行的原因
void (*zj_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
zj_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
}else{
// 如果target不存在,已经释放,则释放当前的timerWrapper
[warpper.timer invalidate];
warpper.timer = nil;
}
}
// 在vc的dealloc方法中调用,通过vc释放,从而让timer释放
- (void)zj_invalidate{
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc{
NSLog(@"%s",__func__);
}

@end

  • timerWapper的使用
1
2
3
4
5
6
7
8
// 定义
self.timerWapper = [[ZJTimerWapper alloc] zj_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];

// 释放
- (void)dealloc{
[self.timerWapper zj_invalidate];
NSLog(@"%s",__func__);
}

运行结果如下

这种方式看起来比较繁琐,步骤很多,而且针对 timerWapper,需要不断的添加method,需要进行一系列的处理

思路四:利用NSProxy虚基类的子类

下面来介绍一种 timer 強引用 最常用 的处理方式:NSProxy子类

可以通过 NSProxy 虚基类,可以交给其子类实现,NSproxy的介绍在前面的文章已经介绍过了,这里不再重复

  • 首先定义一个集成自 NSProxy 的子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//**********NSProxy子类**********
@interface ZJProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

@interface ZJProxy()
@property (nonatomic, weak) id object;

@implementation ZJProxy
+ (instancetype)proxyWithTransformObject:(id)object{
ZJProxy *proxy = [ZJProxy alloc];
proxy.object = object;
return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
  • 将timer中的target传入NSProxy子类对象,即timer持有NSProxy子类对象
1
2
3
4
5
6
7
8
9
10
//**********解决timer強持有的问题*********
self.proxy = [ZJProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];

//在dealloc中将timer正常释放
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}

这样做的目的是将 強引用的注意力转移成了消息转发。虚基类只负责消息转发,即使用 NSProxy 作为 中间代理、中间者

这里有个疑问,定义 proxy 对象,在dealloc释放时,还存在么?

  • proxy对象会正常释放,因为 vc 正常释放了,所以可以释放其持有者,即 timer和proxy,timer的释放也打破了 runloop对proxy的强持有。完美的达到了 两层释放,即 vc -> proxy <- runloop,解释如下:

    • vc释放,导致了 proxy 的释放

    • dealloc方法中,timer进行了释放,所以runloop强引用也释放了

  • Post title:OC底层原理36:内存管理(二)強引用分析
  • Post author:张建
  • Create time:2021-05-29 14:48:15
  • Post link:https://redefine.ohevan.com/2021/05/29/OC底层原理/OC底层原理36:内存管理(二)強引用分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.