美文网首页
iOS内存管理2

iOS内存管理2

作者: f8d1cf28626a | 来源:发表于2022-06-24 17:43 被阅读0次

    内存管理2

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

    强引用(强持有)

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

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

    其主要原因是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的描述

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

    iOS-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方法,why?

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

    加上weakSelf之后,变成了这样

    weakSelf 与 self

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

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

    • 2、weakSelf 和 self 的指针地址相同吗,是指向同一片内存吗?

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

    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
    
    __weak typeof(self) weakSelf = self;
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
    

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

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

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

    结果如下

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

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

    • 从上面打印可以看出,此时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(临时变量的指针地址),可以通过地址拿到指针

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

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

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

    解决 timer强引用(强持有)

    以下几种方法的思路均是:依赖中介者模式,打破强持有,其中推荐思路四

    思路一: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,其步骤如下

    自定义timerWapper

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

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

    • 如果target不存在,已经释放了,则释放当前的timerWrapper,即打破了RunLoop对timeWrapper的强持有 (timeWrapper <-×- RunLoop)

    自定义roc_invalidate方法中释放timer。这个方法在vc的dealloc方法中调用,即vc释放,从而导致timerWapper释放,打破了vc对timeWrapper的的强持有( vc -×-> timeWrapper)

    //*********** .h文件 ***********
    
    @interface RCTimerWapper : NSObject
    
    - (instancetype)rc_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    - (void)rc_invalidate;
    
    @end
    
    //*********** .m文件 ***********
    #import "RCTimerWapper.h"
    #import <objc/message.h>
    
    @interface RCTimerWapper ()
    
    @property(nonatomic, weak) id target;
    @property(nonatomic, assign) SEL aSelector;
    @property(nonatomic, strong) NSTimer *timer;
    
    @end
    
    @implementation RCTimerWapper
    
    - (instancetype)rc_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(RCTimerWapper *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)rc_invalidate{
        [self.timer invalidate];
        self.timer = nil;
    }
    
    - (void)dealloc
    {
        NSLog(@"%s",__func__);
    }
    
    @end
    
    
    • timerWapper的使用
    //定义
    self.timerWapper = [[RCTimerWapper alloc] rc_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
    
    //释放
    - (void)dealloc{
         [self.timerWapper rc_invalidate];
    }
    

    运行pop之后,完美释放

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

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

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

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

    • 首先定义一个继承自NSProxy的子类
    //************NSProxy子类************
    @interface RCProxy : NSProxy
    + (instancetype)proxyWithTransformObject:(id)object;
    @end
    
    @interface RCProxy()
    @property (nonatomic, weak) id object;
    @end
    
    @implementation RCProxy
    
    + (instancetype)proxyWithTransformObject:(id)object{
    
        RCProxy *proxy = [RCProxy alloc];
        proxy.object = object;
        return proxy;
    }
    -(id)forwardingTargetForSelector:(SEL)aSelector {
        return self.object;
    }
    
    • 将timer中的target传入NSProxy子类对象,即timer持有NSProxy子类对象
    //************解决timer强持有问题************
    
    // 可以建议使用这种方式,缓存命中的时间不会被拉长,因为NSProxy 与 NSObject 平级,但很单纯
    
    self.proxy = [RCProxy 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和proxy,timer的释放也打破了runLoop对proxy的强持有。完美的达到了两层释放,即 vc -×-> proxy <-×- runloop,解释如下:
    
    1.vc释放,导致了proxy的释放
    2.dealloc方法中,timer进行了释放,所以runloop强引用也释放了
    

    补充

    __weak 只是一个标记,或称标识符,标记之后会走objc_initWeak

    ->storeWeak<DontHaveOld,DoHaveNew,DoCreashIfDeallocation>(location,(objc_object*)newObj)结构体

    
    // c++ 的模版参数
    
    // 声明形式
    
    template <DontHaveOld havaOld,DoHaveNew haveNew,enum CreashIFDeallocation creashIfDeallocation>
    static id storeWeak(id *location,objc_object *newObj){}
    
    // 调用形式
    
    (void)storeWeak<DontHaveOld,DoHaveNew,DoCreashIfDeallocation>(location,(objc_object*)newObj)
    

    retainCount -> 表示有多少指针指向同一个对象的计数

    weakObject = Object 是各自独立的,weakObject完成了一次拷贝动作且引用计数+1.

    理解部分:
    1.由于创建Object的时候引用计数是1,weakObject指向Object时Object的引用计数则为1

    1. weakObject将Object指向的内存拷贝到弱引用表,此时的weakObject指向弱引用表开辟出来的地址包含Object的内容,_objc_rootRetainCount(weakObject) ==1
      3.之后weakObject会对指向的内存空间的引用+1,所以_objc_rootRetainCount(weakObject) == 2.

    坑点:weakObject = Object

    1. Object 指向的内存不存在,则无法通过Object找到对应的sideTable,然而weakObject指针指向的是sideTable里面的弱引用表的内存空间, sideTable都找不到了自然就会发生crash
      2.解决方案:weakObject释放之前,Object对象不能被释放。

    总结 __weak

    • 强引用 在强引用表 管理自己的内存 一旦创建会被添加到sideTable(小对象除外)

    • 弱引用 在弱引用表 管理自己的内存 会通过强对象找到对应的sideTable,拷贝对象到对应的弱引用表

    相关文章

      网友评论

          本文标题:iOS内存管理2

          本文链接:https://www.haomeiwen.com/subject/otgivrtx.html