Files
knowledge-kit/Chapter1 - iOS/1.45.md
2022-06-23 14:59:47 +08:00

481 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NSTimer 中的内存泄露
NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
```objective-c
@interface ViewController()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(p_doSomeThing)
userInfo:nil
repeats:YES];
}
- (void)p_doSomeThing {
// doSomeThing
}
- (void)p_stopDoSomeThing {
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc {
[self.timer invalidate];
}
@end
```
上面的代码主要是利用定时器重复执行 p_doSomeThing 方法,在合适的时候调用 p_stopDoSomeThing 方法使定时器失效。
能看出问题吗?在开始讨论上面代码问题之前,需要对 NSTimer 做一点说明。NSTimer 的 `scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:` 方法的最后一个参数为 YES 时NSTimer 会保留目标对象,等到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用 invalidate 方法才会失效。
当前的 VC 和 定时器互相引用,造成循环引用。
如果能在合适的时机打破循环引用就不会有问题了
1. 控制器不再强引用定时器
2. 定时器不再保留当前的控制器
## 解决方案:
### 替换 NSTimer API
```objectivec
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
```
### GCD Timer
CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。
GCD 的定时器会更加准时,底层依赖系统内核。
```objectivec
@property (nonatomic, strong) dispatch_source_t timer;
// 创建队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建 GCD 定时器
dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
uint64_t start = 2.0;
uint64_t interval = 1.0;
// 设置定时器周期
dispatch_source_set_timer(timerSource, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
// 设置定时器任务
dispatch_source_set_event_handler(timerSource, ^{
NSLog(@"tick tock");
});
// 启动定时器
dispatch_resume(timerSource);
self.timer = timerSource;
```
为什么 GCD timer 会更准确?因为普通定时器运行依赖 RunLoopRunLoop 一个运行周期内的任务繁忙程度是不确定的。当某次任务繁重,那么定时器调度就不准时。
GCD timer 不依赖 RunLoop系统底层驱动所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。
### 打破循环引用NSTimer target 自定义
```objectivec
@interface LBPProxy : NSObject
+ (instancetype)proxyWithObject:(id)target;
@property (nonatomic, weak) id target;
@end
@implementation LBPProxy
+ (instancetype)proxyWithObject:(id)target{
LBPProxy *proxy = [[LBPProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
@end
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
```
### 高精度定时器封装
项目中经常使用定时器,普通定时器存在精度丢失的问题、循环引用的问题,为了使用方法我们封装一个定时器
```objectivec
#import <Foundation/Foundation.h>
@interface PreciousTimer : NSObject
+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (void)cancelTask:(NSString *)name;
@end
#import "PreciousTimer.h"
@implementation PreciousTimer
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
// 队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重复的任务
[self cancelTask:name];
}
});
// 启动定时器
dispatch_resume(timer);
return name;
}
+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!target || !selector) return nil;
return [self execTask:^{
if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeats:repeats async:async];
}
+ (void)cancelTask:(NSString *)name
{
if (name.length == 0) return;
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}
dispatch_semaphore_signal(semaphore_);
}
@end
```
使用 Demo
```objectivec
- (void)viewDidLoad{
[super viewDidLoad];
NSLog(@"now");
self.timerId = [PreciousTimer execTask:^{
NSLog(@"tick tock %@", [NSThread currentThread]);
} start:2 interval:1 repeats:YES async:YES];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[PreciousTimer cancelTask:self.timerId];
}
```
说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ignoreXcodewarning.png)
### NSProxy
```objectivec
#import "LBPProxy.h"
@implementation LBPProxy
+ (instancetype)proxyWithObject:(id)target{
LBPProxy *proxy = [LBPProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
// 方法1
invocation.target = self.target;
[invocation invoke];
// 方法2
[invocation invokeWithTarget:self.target];
}
@end
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
```
QA: 自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?
NSProxy 效率更高。继承自 NSObject 的代理内部运行的时候还是存在方法查找isa、superclass、cache、methods流程。
看一段神奇的代码
`LBPProxy`
```objectivec
@interface LBPProxy : NSObject
+ (instancetype)proxyWithObject:(id)target;
@property (nonatomic, weak) id target;
@end
@implementation LBPProxy
+ (instancetype)proxyWithObject:(id)target{
LBPProxy *proxy = [LBPProxy alloc];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
@end
```
`LBPProxy2`
```objectivec
@interface LBPProxy2 : NSProxy
+ (instancetype)proxyWithObject:(id)target;
@property (nonatomic, weak) id target;
@end
@implementation LBPProxy2
+ (instancetype)proxyWithObject:(id)target{
LBPProxy2 *proxy = [LBPProxy2 alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
// 方法1
invocation.target = self.target;
[invocation invoke];
// 方法2
[invocation invokeWithTarget:self.target];
}
@end
```
main.m
```objectivec
ViewController *vc = [[ViewController alloc] init];
LBPProxy *p1 = [LBPProxy proxyWithObject:vc];
LBPProxy2 *p2 = [LBPProxy2 proxyWithObject:vc];
NSLog(@"%d %d",
[p1 isKindOfClass:[UIViewController class]],
[p2 isKindOfClass:[UIViewController class]]);
appDelegateClassName = NSStringFromClass([AppDelegate class]);
// 0 1
```
为什么打印出 `0 1`。
分析:
- p1 是 LBPProxy 类,继承于 NSObject 所以就不是 UIViewController 类型。
- p2 是 LBPProxy2 类,继承自 NSProxy当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 VC所以为 1。
这一点可以查看 GUN 查看下源码印证。`NSProxy.m`
```objectivec
- (BOOL) isKindOfClass: (Class)aClass
{
NSMethodSignature *sig;
NSInvocation *inv;
BOOL ret;
sig = [self methodSignatureForSelector: _cmd];
inv = [NSInvocation invocationWithMethodSignature: sig];
[inv setSelector: _cmd];
[inv setArgument: &aClass atIndex: 2];
[self forwardInvocation: inv];
[inv getReturnValue: &ret];
return ret;
}
```
可以看到内部直接调用了消息转发。
### 采用 Block 的形式为 NSTimer 增加分类
```objectivec
//.h文件
#import <Foundation/Foundation.h>
@interface NSTimer (UnRetain)
+ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
repeats:(BOOL)repeats
block:(void(^)(NSTimer *timer))block;
@end
//.m文件
#import "NSTimer+SGLUnRetain.h"
@implementation NSTimer (SGLUnRetain)
+ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(lbp_blcokInvoke:) userInfo:[block copy] repeats:repeats];
}
+ (void)lbp_blcokInvoke:(NSTimer *)timer {
void (^block)(NSTimer *timer) = timer.userInfo;
if (block) {
block(timer);
}
}
@end
//控制器.m
#import "ViewController.h"
#import "NSTimer+UnRetain.h"
//定义了一个__weak的self_weak_变量
#define weakifySelf \
__weak __typeof(&*self)weakSelf = self;
//局域定义了一个__strong的self指针指向self_weak
#define strongifySelf \
__strong __typeof(&*weakSelf)self = weakSelf;
@interface ViewController ()
@property(nonatomic strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger i = 0;
weakifySelf
self.timer = [NSTimer lbp_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
strongifySelf
[self p_doSomething];
NSLog(@"----------------");
if (i++ > 10) {
[timer invalidate];
}
}];
}
- (void)p_doSomething {
}
- (void)dealloc {
// 务必在当前线程调用invalidate方法使得Runloop释放对timer的强引用(具体请参阅官方文档)
[self.timer invalidate];
}
@end
```
上面的方法之所以能解决内存泄漏的问题,关键在于把保留转移到了定时器的类对象身上,这样就避免了实例对象被保留。
当我们谈到循环引用时,其实是指实例对象间的引用关系。类对象在 App 杀死时才会释放,在实际开发中几乎不用关注类对象的内存管理。下面的代码摘自苹果开源的 NSObject.mm 文件,从中可以看出,对于类对象,并不需要像实例对象那样进行内存管理。
```objective-c
+ (id)retain {
return (id)self;
}
// Replaced by ObjectAlloc
- (id)retain {
return ((id)self)->rootRetain();
}
+ (oneway void)release {
}
// Replaced by ObjectAlloc
- (oneway void)release {
((id)self)->rootRelease();
}
+ (id)autorelease {
return (id)self;
}
// Replaced by ObjectAlloc
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
+ (NSUInteger)retainCount {
return ULONG_MAX;
}
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
```
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
## 检测
根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)