iOS自从引入ARC机制后,一般的内存管理就可以不用我们来负责了,但是一些操作如果不注意,还是会引起内存泄漏,从而浪费手机的性能。
一、概述
- 内存泄漏原理
内存泄漏的在百度上的解释就是“程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果”。
在我的理解里就是,公司给一个入职的员工分配了一个工位,但是这个员工离职后,这个工位却不能分配给下一位入职的员工使用,造成了大量的资源浪费。
- 常规的检测方法
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];
}
首先当执行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