iOS-底层原理 33:内存管理(二)强引用分析

简介: iOS-底层原理 33:内存管理(二)强引用分析

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


强应用(强持有)


假设此时有两个界面A、B,从A push 到B界面,在B界面中有如下定时器代码。当从B pop回到A界面[图片上传中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]

时,发现定时器没有停止,其方法仍然在执行,为什么?

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

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


解决方式一



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


解决方式二


  • 定义timer时,采用闭包的形式,因此不需要指定target
- (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的描述


image.png

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


iOS-底层原理 30: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虽然打破了 self -> timer -> self之前的循环引用,即引用链变成了self -> timer -> weakSelf -> self。但是在这里我们的分析并不全面,此时还有一个Runloop对timer的强持有,因为Runloop生命周期B界面更长,所以导致了timer无法释放,同时也导致了B界面的self也无法释放。所以,最初引用链应该是这样的

image.png

weakSelf 与 self


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


  • 1、weakSelf会对引用计数进行+1操作吗?
  • 2、weakSelfself 的指针地址相同吗,是指向同一片内存吗?
  • 带着疑问,我们在weakSelf前后打印self的引用计数
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

运行结果如下,发现前后self的引用计数都是8

image.png

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


  • 继续打印weakSelfself对象,以及指针地址
po weakSelf
po self
po &weakSelf
po &self

结果如下

image.png

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


image.png

  • 从上面打印可以看出,此时timer捕获的是<LGTimerViewController: 0x7f890741f5b0>,是一个对象,所以无法通过weakSelf来解决强持有。即引用链关系为:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7f890741f5b0>)。所以RunLoop对整个 对象的空间有强持有,runloop没停,timer 和 weakSelf是无法释放的
  • 而我们在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方法
- (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);
}

运行结果如下

image.png


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

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

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

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

image.png


思路三:自定义封装timer


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


  • 自定义timerWapper


  • 在初始化方法中,定义一个timer,其target是自己。即timerWapper中的timer,一直监听自己,判断selector,此时的selector已交给了传入的target(即vc对象),此时有一个方法fireHomeWapper,在方法中,判断target是否存在
  • 如果target存在,则需要让vc知道,即向传入的target发送selector消息,并将此时的timer参数也一并传入,所以vc就可以得知fireHome方法,就这事这种方式定时器方法能够执行的原因
  • 如果target不存在,已经释放了,则释放当前的timerWrapper,即打破了RunLoop对timeWrapper的强持有 (timeWrapper <-×- RunLoop
  • 自定义cjl_invalidate方法中释放timer。这个方法在vc的dealloc方法中调用,即vc释放,从而导致timerWapper释放,打破了vctimeWrapper的的强持有( vc -×-> timeWrapper
//*********** .h文件 ***********
@interface CJLTimerWapper : NSObject
- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)cjl_invalidate;
@end
//*********** .m文件 ***********
#import "CJLTimerWapper.h"
#import <objc/message.h>
@interface CJLTimerWapper ()
@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation CJLTimerWapper
- (instancetype)cjl_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(CJLTimerWapper *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)cjl_invalidate{
    [self.timer invalidate];
    self.timer = nil;
}
- (void)dealloc
{
    NSLog(@"%s",__func__);
}
@end
  • timerWapper的使用
//定义
self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
//释放
- (void)dealloc{
     [self.timerWapper cjl_invalidate];
}

运行结果如下

image.png

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


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


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

可以通过NSProxy虚基类,可以交给其子类实现,NSProxy的介绍在iOS-底层原理 30:Block底层原理已经介绍过了,这里不再重复


  • 首先定义一个继承自NSProxy的子类
//************NSProxy子类************
@interface CJLProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface CJLProxy()
@property (nonatomic, weak) id object;
@end
@implementation CJLProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    CJLProxy *proxy = [CJLProxy alloc];
    proxy.object = object;
    return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}
  • timer中的target传入NSProxy子类对象,即timer持有NSProxy子类对象
//************解决timer强持有问题************
self.proxy = [CJLProxy 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;
}

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


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


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


  • vc释放,导致了proxy的释放
  • dealloc方法中,timer进行了释放,所以runloop强引用也释放了


相关文章
|
12月前
|
存储 弹性计算 缓存
阿里云服务器ECS经济型、通用算力、计算型、通用和内存型选购指南及使用场景分析
本文详细解析阿里云ECS服务器的经济型、通用算力型、计算型、通用型和内存型实例的区别及适用场景,涵盖性能特点、配置比例与实际应用,助你根据业务需求精准选型,提升资源利用率并降低成本。
714 3
|
8月前
|
设计模式 缓存 Java
【JUC】(4)从JMM内存模型的角度来分析CAS并发性问题
本篇文章将从JMM内存模型的角度来分析CAS并发性问题; 内容包含:介绍JMM、CAS、balking犹豫模式、二次检查锁、指令重排问题
208 1
|
11月前
|
存储 人工智能 自然语言处理
AI代理内存消耗过大?9种优化策略对比分析
在AI代理系统中,多代理协作虽能提升整体准确性,但真正决定性能的关键因素之一是**内存管理**。随着对话深度和长度的增加,内存消耗呈指数级增长,主要源于历史上下文、工具调用记录、数据库查询结果等组件的持续积累。本文深入探讨了从基础到高级的九种内存优化技术,涵盖顺序存储、滑动窗口、摘要型内存、基于检索的系统、内存增强变换器、分层优化、图形化记忆网络、压缩整合策略以及类操作系统内存管理。通过统一框架下的代码实现与性能评估,分析了每种技术的适用场景与局限性,为构建高效、可扩展的AI代理系统提供了系统性的优化路径和技术参考。
755 4
AI代理内存消耗过大?9种优化策略对比分析
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
724 159
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
147 4
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
279 2
|
Java 开发工具 Android开发
安卓与iOS开发环境对比分析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自占据半壁江山。本文深入探讨了这两个平台的开发环境,从编程语言、开发工具到用户界面设计等多个角度进行比较。通过实际案例分析和代码示例,我们旨在为开发者提供一个清晰的指南,帮助他们根据项目需求和个人偏好做出明智的选择。无论你是初涉移动开发领域的新手,还是寻求跨平台解决方案的资深开发者,这篇文章都将为你提供宝贵的信息和启示。
287 8
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
637 1
|
iOS开发
iOS内存管理学习笔记二
继续学习内存管理相关知识。 1、小小知识点 1>有关OC内存管理的方法并不包括在该语言中,而是包含在cocoa框架中,用于OS X,iOS的开发。 2>cocoa框架中foundation框架类库中的NSObject类负责内存管理的职责。
966 0
|
iOS开发 编译器 开发者
iOS内存管理学习笔记一
1、引用计数 引用计数就是当前的对象被多少个其他对象所引用。这是我自己的理解。 MRC:手动引用计数,开发者自己来计算每一个对象被引用了多少次,自己进行内存的释放。
1020 0