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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353