OC底层原理36:内存管理(二)強引用分析
前言
本文主要是通过 定时器
来梳理 强引用
的几种解决方案
強应用(強持有)
假设有 A和B
两个页面,从A push
到B页面,在B页面中有如下定时器代码,当从B pop
回到A界面时,发现定时器没有停止,其方法仍然在执行,为什么?
1 | self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; |
其主要原因是 B页面没有释放
,即 没有执行dealloc方法
,导致timer也无法停止和释放
解决方式一
- 重写
didMoveParentViewController
方法
1 | - (void)didMoveToParentViewController:(UIViewController *)parent{ |
解决方式二
- 定义timer时,采用
闭包
的形式,因此不需要指定target
1 | - (void)blockTimer{ |
现在,我们从底层来深入研究,为什么 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 | __weak typeof(self) weakSelf = self; |
我们再次运行程序,运行 push-pop
跳转,发现 问题还是存在
,即定时器方法仍然在执行,并 没有执行B的dealloc方法
,为什么呢?
- 我们使用
__weak
虽然打破了self -> timer -> self
之前的循环引用,即引用链变成了self -> timer -> weakSelf -> self
。但是在这里我们分析的并不全面,此时还有一个Runloop对timer的強持有
,因为Runloop
的生命周期
比B
页面更长
,所以导致了timer无法释放
,同时也导致了B
页面的self也无法释放
。所以,最初引用链
应该是这样的:
加上 weakSelf
之后,变成了这样
weakSelf 与 self
对于 weakSelf
和 self
,主要有以下两个疑问?
weakSelf
会对引用计数进行+1
操作么?weakSelf
和self
的指针地址相同么?是指向同一片内存么?带着疑问,我们在
weakSelf
前后打印self
的引用计数
因此可以得出一个结论:weakSelf没有对内存进行+1操作
- 继续打印
weakSelf
和self
对象,以及指针地址
1 | po weakSelf |
打印结果如下
从打印结果可以看出,当前 self
取地址和 weakSelf
取地址的值是不一样的。意味着 两个指针地址,指向的是同一片内存空间
,即 weakSelf和self的内存地址是不一样的,都指向同一片内存空间
的
从上面打印可以看出,此时
timer
捕获的是<LGTimerViewController: 0x102c04dd0>
,是一个对象
,所以无法通过weakSelf来解决強持有
。即引用链关系为 :NSRunloop -> timer -> weakSelf(<LGTimerViewController: 0x102c04dd0>)
。所以RunLoop对整个对象的空间有強持有
,runloop没停,timer
和weakSelf
是无法释放的而我们在
block
原理中提及的block循环引用
,与timer
是有区别
的。通过block底层原理的方法__Block_object_assign
可知,block捕获的是对象的指针地址
,即weakSelf是临时变量的指针地址
,跟self
没有关系,因为weakSelf是新的地址空间
。所以此时的weakSelf相当于中间值。其引用关系链为self -> block -> weakSelf(临时变量的指针地址)
,可以通过地址
拿到指针
所以在这里,我们需要区别下 block
和 timer
循环引用的模型
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 | - (void)didMoveToParentViewController:(UIViewController *)parent{ |
思路二:中介者模式,即不使用self,依赖于其他对象
在timer模式中,我们重点关注的是 fireHome
能执行,并 不关心timer捕获target
是谁,由于这里 不方便使用self
(因为会有强持有问题),所以 可以将target换成其他对象
,例如将target换成 NSObject对象
,将 fireHome
交给 target
执行
- 将timer的target由self改成objc
1 | // 1.定义成其他对象 |
查看运行结果如下
运行发现执行 dealloc
之后,timer还是会继续执行。原因是 解决了中介者的释放
,但是 没有解决中介者的回收
,即 self.target的回收
。所以这种方式 有缺陷
可以通过在 dealloc 方法中,取消定时器来解决,代码如下:
1 | - (void)dealloc{ |
运行代码查看结果如下,发现 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释放,打破了vc
对timerWapper
的強持有(vc -> x -> timerWapper
)
1 | //********** .h文件 ********* |
- timerWapper的使用
1 | // 定义 |
运行结果如下
这种方式看起来比较繁琐,步骤很多,而且针对 timerWapper
,需要不断的添加method,需要进行一系列的处理
思路四:利用NSProxy虚基类的子类
下面来介绍一种 timer
強引用 最常用
的处理方式:NSProxy子类
可以通过 NSProxy
虚基类,可以交给其子类实现,NSproxy的介绍在前面的文章已经介绍过了,这里不再重复
- 首先定义一个集成自
NSProxy
的子类
1 | //**********NSProxy子类********** |
- 将timer中的target传入NSProxy子类对象,即timer持有NSProxy子类对象
1 | //**********解决timer強持有的问题********* |
这样做的目的是将 強引用的注意力转移成了消息转发
。虚基类只负责消息转发,即使用 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.