前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS常见的内存问题——循环引用

iOS常见的内存问题——循环引用

作者头像
用户5521279
发布2019-12-10 16:02:34
1.6K0
发布2019-12-10 16:02:34
举报
文章被收录于专栏:搜狗测试搜狗测试搜狗测试

前言

小编在这段儿时间测试过程中发现了好多内存问题,其中较大部分都是由于循环引用造成的内存泄漏,这里小编就借此类问题来给大家分享一下循环引用引发的原因及常见解决方案。

引用计数

介绍循环引用问题前,首先我们要简单的介绍一下iOS的内存管理方式引用计数。引用计数是一个简单而有效的管理对象生命周期的方式:

  • 当我们创建一个新对象时,它的引用计数为1
  • 当有一个新的指针指向这个对象时,我们将引用计数加1
  • 当某个指针不再指向这个对象时,我们将引用计数减1
  • 当对象的引用计数为0时,说明这个对象不再被任何指针指向了,就可以将对象销毁,回收内存

循环引用

引用计数这种管理内存的方式虽然简单,但是有一个比较大的瑕疵,它不能很好的解决循环引用问题。

对象A和对象B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1,这就导致了A的销毁依赖于B的销毁,同样B的销毁依赖于A的销毁,这样就造成了循环引用问题。

不仅仅只在两个对象中存在循环引用问题,多个对象依次持有对方,形成一个环状,也会造成循环引用问题。

常见内存情况

1. Delegate

代理协议是一个最典型的场景,需要你使用弱引用来避免循环引用。ARC时代,需要将代理声明为 weak 是一个即好又安全的做法:

@property (nonatomic, weak) id <MyCustomDelegate> delegate;

2. block

Block 的循环引用,主要是发生在 ViewController 中持有了 block,比如:

@property (nonatomic, copy)LFCallbackBlock callbackBlock;

同时在对 callbackBlock 进行赋值的时候又调用了 ViewController 的方法,比如:

self.callbackBlock = ^{        
  [self doSomething];    
}];

就会发生循环引用,因为:ViewController-> 强引用了 callback -> 强引用了ViewController,解决方法也很简单:

__weak __typeof(self) weakSelf= self;    
self.callbackBlock = ^{      
  [weakSelf doSomething];    
}];

使用 MRC 管理内存时,Block 的内存管理需要区分是 Global(全局)、Stack(栈)还是 Heap(堆),而在使用了 ARC 之后,苹果自动会将所有原本应该放在栈中的 Block 全部放到堆中。全局的 Block 比较简单,凡是没有引用到 Block 作用域外面的参数的 Block 都会放到全局内存块中,在全局内存块的 Block 不用考虑内存管理问题。(放在全局内存块是为了在之后再次调用该 Block 时能快速反应,当然没有调用外部参数的 Block 根本不会出现内存管理问题)。

所以 Block 的内存管理出现问题的,绝大部分都是在堆内存中的 Block 出现了问题。默认情况下,Block 初始化都是在栈上的,但可能随时被收回,通过将 Block 类型声明为 copy 类型,这样对 Block 赋值的时候,会进行 copy 操作,copy 到堆上,如果里面有对 self 的引用,则会有一个强引用的指针指向 self,就会发生循环引用,如果采用 weakSelf,内部不会有强类型的指针,所以可以解决循环引用问题。

3. NSTimer

NSTimer 我们开发中会用到很多,比如下面一段代码:

    - (void)viewDidLoad {        
      [super viewDidLoad];        
      self.myTimer = [NSTimerscheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomeThing)userInfo:nil repeats:YES];    
    }    

    - (void)doSomeThing {    
    }    

    - (void)dealloc {         
      [self.timer invalidate];         
      self.timer = nil;    
    }

这是典型的循环引用,因为 timer 会强引用 self,而 self 又持有了timer,所有就造成了循环引用。那有人可能会说,我使用一个 weak 指针,比如:

__weak typeof(self) weakSelf = self;    
self.myTimer = [NSTimerscheduledTimerWithTimeInterval:1 target:weakSelfselector:@selector(doSomeThing) userInfo:nil repeats:YES];

但是其实并没有用,因为不管是 weakSelf 还是 strongSelf,最终在 NSTimer 内部都会重新生成一个新的指针指向 self,这是一个强引用的指针,结果就会导致循环引用。那怎么解决呢?主要有如下三种方式:

a. 使用中间类

创建一个继承 NSObject 的子类MyTimerTarget,并创建开启计时器的方法。

// MyTimerTarget.h   
#import <Foundation/Foundation.h>   
@interface MyTimerTarget : NSObject   
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)intervaltarget:(id)target selector:(SEL)selector userInfo:(id)userInforepeats:(BOOL)repeats;   
@end   

// MyTimerTarget.m   
#import "MyTimerTarget.h"   
@interface MyTimerTarget ()   
@property (assign, nonatomic) SEL outSelector;   
@property (weak, nonatomic) id outTarget;   
@end   
@implementation MyTimerTarget
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)intervaltarget:(id)target selector:(SEL)selector userInfo:(id)userInforepeats:(BOOL)repeats {       
  MyTimerTarget *timerTarget = [[MyTimerTarget alloc] init];       
  timerTarget.outTarget = target;       
  timerTarget.outSelector = selector;       
  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:intervaltarget:timerTarget selector:@selector(timerSelector:) userInfo:userInforepeats:repeats];       
  return timer;   
}
   
- (void)timerSelector:(NSTimer *)timer {       
  if (self.outTarget && [self.outTargetrespondsToSelector:self.outSelector]) {            
    [self.outTargetperformSelector:self.outSelector withObject:timer.userInfo];       
  } else {            
    [timer invalidate];       
  }   
}   
@end   

// 调用方   
@property (strong, nonatomic) NSTimer *myTimer;   
- (void)viewDidLoad {       
  [super viewDidLoad];       
  self.myTimer = [MyTimerTarget scheduledTimerWithTimeInterval:1target:self selector:@selector(doSomething) userInfo:nil repeats:YES];    
}   

- (void)doSomeThing {   
}   

- (void)dealloc {       
  NSLog(@"MyViewController dealloc");
}

VC 强引用 timer,因为 timer 的 target 是MyTimerTarget 实例,所以 timer 强引用MyTimerTarget 实例,而 MyTimerTarget 实例弱引用 VC,解除循环引用。这种方案 VC 在退出时都不用管 timer,因为自己释放后自然会触发 timerSelector:中的[timer invalidate]逻辑,timer 也会被释放。

b. 使用类方法

我们还可以对 NSTimer 做一个category,通过 block 将 timer 的 target 和 selector 绑定到一个类方法上,来实现解除循环引用。

// NSTimer+MyUtil.h
#import <Foundation/Foundation.h>
@interface NSTimer (MyUtil)
+ (NSTimer*)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)intervalblock:(void(^)())block repeats:(BOOL)repeats;
@end
// NSTimer+MyUtil.m
#import "NSTimer+MyUtil.h"
@implementation NSTimer (MyUtil)
+ (NSTimer *)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)intervalblock:(void(^)())block repeats:(BOOL)repeats {
  return [self scheduledTimerWithTimeInterval:interval target:selfselector:@selector(MyUtil_blockInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)MyUtil_blockInvoke:(NSTimer *)timer {
  void (^block)() = timer.userInfo;
  if (block) {
     block();
  }
}
@end

// 调用方
@property (strong, nonatomic) NSTimer *myTimer;
- (void)viewDidLoad {
  [super viewDidLoad];
  self.myTimer = [NSTimer MyUtil_scheduledTimerWithTimeInterval:1 block:^{
    NSLog(@"doSomething");
  } repeats:YES];
}

- (void)dealloc {
  if (_myTimer) {
    [_myTimer invalidate];
  }
  NSLog(@"MyViewController dealloc");
}

这种方案下,VC 强引用 timer,但是不会被 timer 强引用,但有个问题是 VC 退出被释放时,如果要停掉 timer 需要自己调用一下 timer 的 invalidate 方法。

c. 使用 weakProxy

创建一个继承 NSProxy 的子类 MyProxy,并实现消息转发的相关方法。NSProxy 是 iOS 开发中一个消息转发的基类,它不继承自 NSObject。因为他也是 Foundation 框架中的基类, 通常用来实现消息转发, 我们可以用它来包装 NSTimer 的 target, 达到弱引用的效果。

// MyProxy.h
#import <Foundation/Foundation.h>
@interface MyProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
// MyProxy.m
#import "MyProxy.h"
@interface MyProxy ()
@property (weak, readonly, nonatomic) idweakTarget;
@end
@implementation MyProxy
+ (instancetype)proxyWithTarget:(id)target{
    return [[MyProxy alloc]initWithTarget:target];
}
- (instancetype)initWithTarget:(id)target {
    _weakTarget = target;
    return self;
}
- (void)forwardInvocation:(NSInvocation*)invocation {
    SEL sel = [invocation selector];
    if (_weakTarget &&[self.weakTarget respondsToSelector:sel]) {
        [invocationinvokeWithTarget:self.weakTarget];
    }
}
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
    return [self.weakTargetmethodSignatureForSelector:sel];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
    return [self.weakTargetrespondsToSelector:aSelector];
}
@end
// 调用方
@property (strong, nonatomic) NSTimer*myTimer;
- (void)viewDidLoad {
    [super viewDidLoad];
    self.myTimer = [NSTimerscheduledTimerWithTimeInterval:1 target:[MyProxy proxyWithTarget:self]selector:@selector(doSomething) userInfo:nil repeats:YES];
}
- (void)dealloc {
    if (_myTimer) {
        [_myTimer invalidate];
    }
    NSLog(@"MyViewControllerdealloc");
}

上面的代码中,了解一下消息转发的过程就可以知道-forwardInvocation: 是会有一个 NSInvocation 对象,这个 NSInvocation 对象保存了这个方法调用的所有信息,包括 Selector 名,参数和返回值类型,最重要的是有所有参数值,可以从这个 NSInvocation 对象里拿到调用的所有参数值。这时候我们把转发过来的消息和weakTarget 的 selector 信息做对比,然后转发过去即可。

这里需要注意的是,在调用方的 dealloc 中一定要调用 timer 的 invalidate 方法,因为如果这里不清理 timer,这个调用方 dealloc 被释放后,消息转发就找不到接收方了,就会 crash。

总结

在App开发中的内存问题往往是最难发现而且最难排查解决的问题,因此我们需要在开发之初就要对代码进行审查,针对上面提出的几个问题要多加关注,与此同时,我们还需要利用评测工具来助力,Instruments,FBRetainCycleDetector和MLeaksFinder,这三款工具均可检测内存问题。


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 搜狗测试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档