美文网首页
iOS - 常见的几种内存泄漏与查找

iOS - 常见的几种内存泄漏与查找

作者: ShIwEn9 | 来源:发表于2020-02-26 18:22 被阅读0次

iOS自从引入ARC机制后,一般的内存管理就可以不用我们来负责了,但是一些操作如果不注意,还是会引起内存泄漏,从而浪费手机的性能。

一、概述
  1. 内存泄漏原理

内存泄漏的在百度上的解释就是“程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果”。

在我的理解里就是,公司给一个入职的员工分配了一个工位,但是这个员工离职后,这个工位却不能分配给下一位入职的员工使用,造成了大量的资源浪费。

  1. 常规的检测方法

2.1、Analyze静态分析 (command + shift + b)。

2.2、动态分析方法(Instrument工具库里的Leaks),product->profile ->leaks 打开可以工具主窗口,具体使用方法可以参考这篇文章:https://www.jianshu.com/p/9fc2132d09c7

二、常见的内存泄漏情况:

1. 对象之间的循环引用问题:
循环引用的实质:多个对象相互之间有强引用,不能施放让系统回收。

如: 对象A强引用对象B,对象B也强引用对象A,那么这样就会出现循环引用使得两者都不能释放内存。

解决循环引用的解决办法一般是将 strong 引用改为 weak 引用,这样就可以打破对象之间的相互强引用
如上面的例子:将B弱引用A,那么就打破了这种循环

  • 1.1. 父类和子类之间的循环引用:

如:在使用UITableView 的时候,将 UITableView 给 Cell 使用,cell 中的 strong 引用会造成循环引用。

// controller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TestTableViewCell *cell =[tableView dequeueReusableCellWithIdentifier:@"UITableViewCellId" forIndexPath:indexPath];
    cell.tableView = tableView;
    return cell;
}

// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, strong) UITableView *tableView; // strong 造成循环引用
@end

解决:strong 改为 weak

// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, weak) UITableView *tableView; // strong 改为 weak
@end
  • 1.2. block的循环引用

block的循环引用是最常见的循环引用情况之一
block在copy时都会对block内部用到的对象进行强引用的。

typedef void(^block)();
 
@property (copy, nonatomic) block myBlock;  // 2
@property (copy, nonatomic) NSString *blockString;
 
 
- (void)testBlock {
    self.myBlock = ^() {
        //其实注释中的代码,同样会造成循环引用
        NSString *localString = self.blockString; // 1
          //NSString *localString = _blockString;
          //[self doSomething];
    };
}

解决方法:使用__weak打破循环的方法只在ARC下才有效,在MRC下应该使用__block

__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
   NSString *localString = weakSelf.blockString;
};

或者在block执行完后,将block置nil,这样也可以打破循环引用,这样做的缺点是,block只会执行一次,因为block被置nil了,要再次使用的话,需要重新赋值。

这里会发现如果我们在使用系统自带的一些block的时候,如 UIView动画、GCD等、都没有用 weak self ,那为什么没有产生循环引用的问题呢?
上文已经解释了循环引用的原理:所以当 block 本身不被 self 持有,而被别的对象持有,同时不产生循环引用的时候,就不需要使用 weak self 了.,所以当block 不是对象的属性 / 变量,而是方法的参数 / 临时变量的时候也不需要去管理循环引用的问题。

  • 1.2. delegate的循环引用
    delegate是委托模式.委托模式是将一件属于委托者做的事情,交给另外一个被委托者来处理
    在这里我们可能会出现委托者和被委托人之间的相互强引用问题
    解决办法:在声明 delegate 属性的时候 用weak 进行若引用
@protocol  MyUIViewDelegate <NSObject>
- (void)func;
@end


@interface  MyUIView: UIView
@property(nonatomic, weak) id<MyUIViewDelegate> delegate;
  • 1.3. NSTime的循环引用
    NSTimer 的 target 对传入的参数都是强引用(即使是 weak 对象)
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)timerRun {
    NSLog(@"%s", __func__);
}

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

这里的每一个箭头代表着一个强指针,当回退上一个界面时,NavigationController指向TimerViewController的强引用被销毁,但是TimerViewController和timer之间互相强引用,内存泄漏。

  • 解决办法:
    在这里加入了一个中间代理对象LJProxy,TimerViewController不直接持有timer,而是持有LJProxy实例,让LJProxy实例来弱引用TimerViewController,timer强引用LJProxy实例
@interface LJProxy : NSObject
+ (instancetype) proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LJProxy
+ (instancetype) proxyWithTarget:(id)target
{
    LJProxy *proxy = [[LJProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end

Controller里只修改了下面一句代码

- (void)viewDidLoad {
    [super viewDidLoad];
    // 这里的target发生了变化
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

image.png

首先当执行pop的时候,1号指针被销毁,现在就没有强指针再指向TimerViewController了,TimerViewController可以被正常销毁。
TimerViewController销毁,会走dealloc方法,在dealloc里调用了[self.timer invalidate],那么timer将从RunLoop中移除,3号指针会被销毁。
当TimerViewController销毁了,对应它强引用的指针也会被销毁,那么2号指针也会被销毁。
上面走完,timer已经没有被别的对象强引用,timer会销毁,LJProxy实例也就自动销毁了。

这里需要注意的有两个地方:
1.- (id)forwardingTargetForSelector:(SEL)aSelector是什么?
了解iOS消息转发的朋友肯定知道这个东西,不了解的可以去这个博客看看
(https://www.jianshu.com/p/eac6ed137e06)。
简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
本文中由于LJProxy没有实现timerRun方法(当然也不需要它实现),让系统去找target实例的方法实现,也就是去找TimerViewController中的方法实现。
2.timer的invalidate方法的具体作用参考苹果官方,这个方法会停止timer并将其从RunLoop中移除。
This method is the only way to remove a timer from an [NSRunLoop]object. The NSRunLoop object removes its strong reference to the timer, either just before the [invalidate] method returns or at some later point.

  • 1.4. 通知的循环引用

iOS9 以后,一般的通知,都不再需要手动移除观察者,系统会自动在dealloc 的时候调用 [[NSNotificationCenter defaultCenter]removeObserver:self]。iOS9 以前的需要手动进行移除。

原因是:iOS9 以前观察者注册时,通知中心并不会对观察者对象做 retain 操作,而是进行了 unsafe_unretained 引用,所以在观察者被回收的时候,如果不对通知进行手动移除,那么指针指向被回收的内存区域就会成为野指针,这时再发送通知,便会造成程序崩溃。

从 iOS9 开始通知中心会对观察者进行 weak 弱引用,这时即使不对通知进行手动移除,指针也会在观察者被回收后自动置空,这时再发送通知,向空指针发送消息是不会有问题的。

但是最好加上移除通知的操作:

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self.observer name:@"name" object:nil];
  NSLog(@"hi,我 dealloc 了啊");
}
  • 1.5. WKWebView 造成的内存泄漏
    总的来说,WKWebView 不管是性能还是功能,都要比 UIWebView 强大很多,本身也不存在内存泄漏问题,但是,如果开发者使用不当,还是会造成内存泄漏。请看下面这段代码:
@property (nonatomic, strong) WKWebView *wkWebView;
- (void)webviewMemoryLeak {
// 9.2 WKWebView
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = [[WKUserContentController alloc] init];
[config.userContentController addScriptMessageHandler:self name:@"WKWebViewHandler"];
_wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
_wkWebView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:_wkWebView];
NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
[_wkWebView loadRequest:requset];

这样看起来没有问题,但是其实 “addScriptMessageHandler” 这个操作,导致了 wkWebView 对 self 进行了强引用,然后 “addSubview”这个操作,也让 self 对 wkWebView 进行了强引用,这就造成了循环引用。

解决方法就是在合适的机会里对 “MessageHandler” 进行移除操作:

- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[_wkWebView.configuration.userContentController removeScriptMessageHandlerForName:@"WKWebViewHandler"];
}

2. 内存泄漏的查询
2.1. Analyze静态分析 (command + shift + b)

主要分析以下四种问题:

1、逻辑错误:访问空指针或未初始化的变量等;

2、内存管理错误:如内存泄漏等;

3、声明错误:从未使用过的变量;

4、Api调用错误:未包含使用的库和框架。

静态分析结果会有警告提示


2.2. Instruments中的Leak动态分析内存泄漏
product->profile ->leaks 打开工具主窗口
具体的参考 文章:
ios内存泄漏检查-leaks使用

简单的内存泄漏就介绍到这里。如果觉得文章不错的话欢迎大家点赞,如有错误欢迎指正! ⛽️
参考文章:
iOS之__block、__weak、Block循环引用、__weak typeof(self) weakSelf = self
iOS开发系列之内存泄漏分析(上)
iOS开发系列之内存泄漏分析(下)
如何正确的使用NSTimer

相关文章

  • iOS - 常见的几种内存泄漏与查找

    iOS自从引入ARC机制后,一般的内存管理就可以不用我们来负责了,但是一些操作如果不注意,还是会引起内存泄漏,从而...

  • Xcode调试工具

    一.静态内存分析工具 编译阶段查找内存泄漏等问题 1.常见内存泄漏问题 常见的内存泄漏除了循环引用,CoreFou...

  • Android性能分析的几种方法

    Android性能分析的几种方法 通过Memory Monitor 查找内存泄漏Android Profiler中...

  • android性能优化总结

    1,UI优化:这篇文章总结的不错 2,内存泄漏优化 常见的几种形式: 资源对象没关闭造成的内存泄漏: 资源对象没关...

  • iOS 内存泄漏三两事

    iOS 内存泄漏三两事 iOS 内存泄漏三两事

  • Android 内存泄漏

    内存泄漏的原因 常见的内存泄漏与解决方法 检测内存泄漏 认识内存泄漏 根本原因就是当一个对象理应被回收的时候,因为...

  • iOS 内存泄漏的查找

    更多内容请挪步我的博客 前言 最近为[伯乐在线]翻译一篇文章(该文章尚未发布),作者 Russ Bishop 谈到...

  • [iOS] 线上内存泄漏检测方案与结果

    [iOS] 线上内存泄漏检测方案与结果

  • Android内存泄漏原因及解决的总结

    分三步说明Android内存泄漏的原因及解决,“内存泄漏与内存溢出的区别”,“引用方式”,“常见引发原因与解决方案...

  • iOS 常见内存泄漏

    一、基本概念 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。一次内存泄露危害可以忽略,...

网友评论

      本文标题:iOS - 常见的几种内存泄漏与查找

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