iOS-底层原理30:内存管理(二)强引用分析
本文主要是通过定时器
来梳理强引用
的几种解决方案
强引用
假设此时有两个界面A、B,从A push
到B界面,在B界面中有如下定时器代码。当从B pop回到A界面时,发现定时器没有停止,其方法仍然在执行,为什么?
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
其主要原因是B界面没有释放
,即没有执行dealloc
方法,导致timer
也无法停止和释放。
现在,我们从底层来深入研究,为什么B界面有了timer之后,导致B界面释放不掉,即不会走到dealloc
方法。我们可以通过官方文档查看timerWithTimeInterval:target:selector:userInfo:repeats:
方法中对target的描述
从文档中可以看出,timer
对传入的target
具有强持有
,即timer持有self
。由于timer是定义在B界面中,所以self也持有timer
,因此 self -> timer -> self
构成了循环引用
在iOS-底层原理28:Block底层原理文章中,针对循环应用提供了几种解决方式。我们我们尝试通过__weak
即弱引用
来解决,代码修改如下
__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
不能解决强引用的问题?
解答:
使用
__weak
虽然打破了self -> timer -> self
之前的循环引用,即引用链变成了self -> timer -> weakSelf -> self
,但是
1、当前timer除了被self持有,还被加入了[NSRunLoop currentRunLoop]
中
2、当前timer直接指向self的内存空间
,是对内存进行强持有,而不是简单的指针拷贝。
最初引用链
应该是这样的
加上weakSelf
之后,变成了这样
weakSelf 与 self
对于weakSelf 和 self,主要有以下两个疑问:
1、
weakSelf
会对引用计数进行+1
操作吗?2、weakSelf 和 self 的
指针地址相同
吗,是指向同一片内存
吗?
先看下weakSelf
是否会对引用计数+1
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
运行结果
两次结果相同,因此可以得出一个结论:
weakSelf没有对内存进行+1操作
继续打印weakSelf
和 self
,以及指针地址
po weakSelf
po self
po &weakSelf
po &self
打印结果
从打印结果可以看出:
weakSelf
和self
内存地址是相同的,指向的是同一片内存空间
;当前
self取地址
和weakSelf取地址的值是不一样的
,意味着有两个指针地址
;
-
从上面打印可以看出,此时timer捕获的是
<LGTimerViewController: 0x7fbf1050d1a0>
,是一个对象,所以无法通过weakSelf来解决强持有
。即引用链关系为:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7fbf1050d1a0>)
。所以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
方法
- (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、定义其他对象**********
@property (nonatomic, strong) id target;
//**********1、修改target**********
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
//**********3、imp**********
void fireHomeObjc(id obj){
NSLog(@"%s -- %@",__func__,obj);
}
运行结果
运行发现执行dealloc
之后,timer还是会继续执行
。原因是解决了中介者的释放
,但是没有解决中介者的回收
,即self.target的回收。所以这种方式有缺陷。
可以通过在dealloc方法中,取消定时器来解决,代码如下
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
运行结果如下,发现pop之后,timer释放,从而中介者也会进行回收释放
思路三:自定义封装timer
-
在初始化方法中,
定义一个timer
,其target是自己
。即timerWapper中的timer,一直监听自己
,判断selector,此时的selector已交给了传入的target(即vc对象),此时有一个方法fireHomeWapper,在方法中,判断target是否存在-
如果
target存在
,则需要让vc知道,即向传入的target发送selector消息
,并将此时的timer参数也一并传入,所以vc就可以得知fireHome方法
,这就是这种方式定时器方法能够执行的原因 -
如果
target不存在
,已经释放了,则释放当前的timerWrapper
,即打破了RunLoop对timeWrapper的强持有 (timeWrapper <-×- RunLoop
)
-
-
自定义
lbh_invalidate方法中释放timer
。这个方法在vc的dealloc方法中调用,即vc释放
,从而导致timerWapper释放
,打破了vc对timeWrapper的的强持有(vc -×-> timeWrapper
)
//*********** .h文件 ***********
@interface LBHTimerWapper : NSObject
- (instancetype)lbh_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)lbh_invalidate;
@end
//*********** .m文件 ***********
#import "LBHTimerWapper.h"
#import <objc/message.h>
@interface LBHTimerWapper ()
@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation LBHTimerWapper
- (instancetype)lbh_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(LBHTimerWapper *wapper){
//判断target是否存在
if (wapper.target) {
//如果存在则需要让vc知道,即向传入的target发送selector消息,并将此时的timer参数也一并传入,所以vc就可以得知`fireHome`方法,就这事这种方式定时器方法能够执行的原因
//objc_msgSend发送消息,执行定时器方法
void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
}else{
//如果target不存在,已经释放了,则释放当前的timerWrapper
[wapper.timer invalidate];
wapper.timer = nil;
}
}
//在vc的dealloc方法中调用,通过vc释放,从而让timer释放
- (void)lbh_invalidate{
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
timerWapper的使用
//定义
self.timerWapper = [[LBHTimerWapper alloc] lbh_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
//释放
- (void)dealloc{
[self.timerWapper lbh_invalidate];
}
运行结果
这种方式看起来比较繁琐,步骤很多,而且针对timerWapper
,需要不断的添加method,需要进行一系列的处理
思路四:利用NSProxy虚基类的子类
可以通过NSProxy
虚基类,可以交给其子类实现,NSProxy的介绍在iOS-底层原理28:Block底层原理已经介绍过了,这里不再重复
首先定义一个继承自NSProxy
的子类
//************NSProxy子类************
@interface LBHProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface LBHProxy()
@property (nonatomic, weak) id object;
@end
@implementation LBHProxy
+ (instancetype)proxyWithTransformObject:(id)object{
LBHProxy *proxy = [LBHProxy alloc];
proxy.object = object;
return proxy;
}
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// 转移
// 强引用 -> 消息转发
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
@end
调用代码
@interface LGTimerViewController ()
@property (nonatomic, strong) LBHProxy * proxy;
@property (nonatomic, strong) NSTimer * timer;
@end
@implementation LGTimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建虚基类代理
self.proxy = [LBHProxy proxyWithTransformObject: self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome{
NSLog(@"hello word" ); // 调用
}
- (void)dealloc{
// 释放
[self.timer invalidate];
NSLog(@"%s",__func__);
}
@end
这样做的主要目的是将强引用
的注意力转移成了消息转发
。虚基类只负责消息转发
,即使用NSProxy作为中间代理、中间者
这里有个疑问,定义的proxy对象,在dealloc释放时,还存在吗?
proxy
对象会正常释放,因为vc正常释放了
,所以可以释放其持有者,即timer和proxy
,timer的释放也打破了runLoop对proxy的强持有。完美的达到了两层释放,即 vc -×-> proxy <-×- runloop
,解释如下:
-
vc释放,导致了proxy的释放
-
dealloc方法中,timer进行了释放,所以runloop强引用也释放了