mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
feature: dyld && LD 链接器
This commit is contained in:
@@ -1,67 +1,280 @@
|
||||
# NSTimer 中的内存泄露
|
||||
# NSTimer、CSDisplayLink 中的内存泄露
|
||||
|
||||
NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
|
||||
|
||||
|
||||
## CADisplayLink 内存泄漏
|
||||
|
||||
<img src="./../assets/CSDisplayLinkMemoryLeak.png" style="zoom:30%" />
|
||||
|
||||
可以看到 CADisplayLink 和 VC,VC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。
|
||||
|
||||
|
||||
|
||||
## NSTimer 内存泄漏
|
||||
|
||||
### 对比实验
|
||||
|
||||
NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
|
||||
|
||||
Demo 如下:
|
||||
|
||||
<img src="./../assets/NSTimerMemoeryLeakDemo.png" style="zoom:30%" />
|
||||
|
||||
但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么?
|
||||
|
||||
<img src="./../assets/NSTimerMemoeryNotLeakWhenRepeatNO.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
### 源码分析
|
||||
|
||||
查看 gnu 源码发现
|
||||
|
||||
```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];
|
||||
|
||||
// NSTimer.m
|
||||
+ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
|
||||
target: (id)object
|
||||
selector: (SEL)selector
|
||||
userInfo: (id)info
|
||||
repeats: (BOOL)f
|
||||
{
|
||||
return AUTORELEASE([[self alloc] initWithFireDate: nil
|
||||
interval: ti
|
||||
target: object
|
||||
selector: selector
|
||||
userInfo: info
|
||||
repeats: f]);
|
||||
}
|
||||
|
||||
- (void)p_doSomeThing {
|
||||
// doSomeThing
|
||||
}
|
||||
|
||||
- (void)p_stopDoSomeThing {
|
||||
[self.timer invalidate];
|
||||
self.timer = nil;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.timer invalidate];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
内部调用下面的函数
|
||||
|
||||
```objective-c
|
||||
- (id) initWithFireDate: (NSDate*)fd
|
||||
interval: (NSTimeInterval)ti
|
||||
target: (id)object
|
||||
selector: (SEL)selector
|
||||
userInfo: (id)info
|
||||
repeats: (BOOL)f
|
||||
{
|
||||
if (ti <= 0.0)
|
||||
{
|
||||
ti = 0.0001;
|
||||
}
|
||||
if (fd == nil)
|
||||
{
|
||||
_date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
|
||||
initWithTimeIntervalSinceNow: ti];
|
||||
}
|
||||
else
|
||||
{
|
||||
_date = [fd copyWithZone: NSDefaultMallocZone()];
|
||||
}
|
||||
_target = RETAIN(object);
|
||||
_selector = selector;
|
||||
_info = RETAIN(info);
|
||||
if (f == YES)
|
||||
{
|
||||
_repeats = YES;
|
||||
_interval = ti;
|
||||
}
|
||||
else
|
||||
{
|
||||
_repeats = NO;
|
||||
_interval = 0.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
```
|
||||
|
||||
外面的 repeat 根据传递的布尔值,内部赋值给 _repeats 参数。
|
||||
|
||||
内部会自动调用 fire
|
||||
|
||||
```objective-c
|
||||
- (void) fire
|
||||
{
|
||||
/* We check that we have not been invalidated before we fire.
|
||||
*/
|
||||
if (NO == _invalidated) {
|
||||
if ((id)_block != nil) {
|
||||
CALL_BLOCK(_block, self);
|
||||
} else {
|
||||
id target;
|
||||
|
||||
/* We retain the target so it won't be deallocated while we are using
|
||||
* it (if this timer gets invalidated while we are firing).
|
||||
*/
|
||||
target = RETAIN(_target);
|
||||
|
||||
if (_selector == 0) {
|
||||
NS_DURING {
|
||||
[(NSInvocation*)target invoke];
|
||||
}
|
||||
NS_HANDLER {
|
||||
NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
|
||||
@"raised during posting of timer with target %s(%s) "
|
||||
@"and selector '%@'",
|
||||
[localException name], [localException reason],
|
||||
GSClassNameFromObject(target),
|
||||
GSObjCIsInstance(target) ? "instance" : "class",
|
||||
NSStringFromSelector([target selector]));
|
||||
}
|
||||
NS_ENDHANDLER
|
||||
} else {
|
||||
NS_DURING {
|
||||
[target performSelector: _selector withObject: self];
|
||||
}
|
||||
NS_HANDLER {
|
||||
NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
|
||||
@"raised during posting of timer with target %p and "
|
||||
@"selector '%@'",
|
||||
[localException name], [localException reason], target,
|
||||
NSStringFromSelector(_selector));
|
||||
}
|
||||
NS_ENDHANDLER
|
||||
}
|
||||
RELEASE(target);
|
||||
}
|
||||
|
||||
if (_repeats == NO) {
|
||||
[self invalidate];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到如果 repeat 为 NO ,则会执行 `[target performSelector: _selector withObject: self];` 调用1次方法,然后会执行 `invalidate` 函数,`invalidate` 实现如下
|
||||
|
||||
```objective-c
|
||||
- (void) invalidate
|
||||
{
|
||||
/* OPENSTEP allows this method to be called multiple times. */
|
||||
_invalidated = YES;
|
||||
if (_target != nil)
|
||||
{
|
||||
DESTROY(_target);
|
||||
}
|
||||
if (_info != nil)
|
||||
{
|
||||
DESTROY(_info);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到当 target 和 info 存在的时候,都会在 `invalidate` 方法中被 destory,也就是释放。
|
||||
|
||||
```
|
||||
#define DESTROY(object) ({ \
|
||||
void *__o = (void*)object; \
|
||||
object = nil; \
|
||||
[(id)__o release]; \
|
||||
})
|
||||
#endif
|
||||
```
|
||||
|
||||
结论:通过 gnu 可以看到,NSTimer 会对传入的 target、info 对象进行持有强引用,当 repeat 参数为 NO 的时候,则会立马通过 performSelector 的方式执行定时器任务,然后执行 invalidate 方法,对其内部引用的 object、info 进行释放。
|
||||
|
||||
|
||||
|
||||
上面的代码主要是利用定时器重复执行 p_doSomeThing 方法,在合适的时候调用 p_stopDoSomeThing 方法使定时器失效。
|
||||
|
||||
能看出问题吗?在开始讨论上面代码问题之前,需要对 NSTimer 做一点说明。NSTimer 的 `scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:` 方法的最后一个参数为 YES 时,NSTimer 会保留目标对象,等到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用 invalidate 方法才会失效。
|
||||
|
||||
当前的 VC 和 定时器互相引用,造成循环引用。
|
||||
当前的 VC 和 定时器互相引用,造成循环引用。所以思路如下:
|
||||
|
||||
如果能在合适的时机打破循环引用就不会有问题了
|
||||
|
||||
1. 控制器不再强引用定时器
|
||||
2. 定时器不再保留当前的控制器
|
||||
|
||||
## 解决方案:
|
||||
|
||||
### 替换 NSTimer API
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 改用 block 的方式替换 API,不再持有 target
|
||||
|
||||
<img src="./../assets/NSTimerFixMemoryLeakIssueByBlockAPI.png" style="zoom:30%" />
|
||||
|
||||
该种方式,控制器 (self)强引用 timer,timer 强引用 block,block 弱引用 self,3者没有形成环。
|
||||
|
||||
|
||||
|
||||
### 采用系统 NSProxy 代替自定义的中间类
|
||||
|
||||
<img src="./../assets/NSTimerMemoryLeakFixedByNSProxy.png" style="zoom:30%" />
|
||||
|
||||
注意:继承自 NSProxy 的类,不能 init。
|
||||
|
||||
|
||||
|
||||
QA:自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?看上去反而是自定义的 NSObject 使用更简单呀?
|
||||
|
||||
答:**NSProxy 效率更高**。NSProxy 的主要作用是为消息转发提供一个通用的接口,是一个继承自 NSObject 的对象,虽然看上去 API 更简单,写法简单,但内部运行的时候还是基于 isa 去查找类对象、元类对象的 cache 中查找,找不到再去 class_rw_t 中查找,找不到再从 superclass 找父类的类对象、元类对象...流程,最后还是找不到,则走 runtime 的动态方法解析、消息转发阶段。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
看一段神奇的代码
|
||||
|
||||
<img src="./../assets/NSProxyAndNSObjectMethodImpl.png" style="zoom:30%" />
|
||||
|
||||
为什么打印出 `0 1`?
|
||||
|
||||
分析:
|
||||
|
||||
- p1 是 `TimerProxy` 类,继承于 NSObject 所以就不是 UIViewController 类型。
|
||||
|
||||
- p2 是 `MethodProxy` 类,继承自 NSProxy,当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 self,所以上面的 `[p2 isKindOfClass:[self class]]` 等价于 `[self isKindOfClass:[self.class]]`,所以为 1。
|
||||
|
||||
也就是说继承自 NSProxy 类的对象,调用方法的时候,会自动走消息转发的流程。
|
||||
|
||||
这一点可以查看 GUN 查看下源码印证。`NSProxy.m`
|
||||
|
||||
```objectivec
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
[weakSelf timerTest];
|
||||
}];
|
||||
- (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;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到内部直接调用了消息转发。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### GCD Timer
|
||||
|
||||
CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。
|
||||
**CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。**
|
||||
|
||||
GCD 的定时器会更加准时,底层依赖系统内核。
|
||||
|
||||
|
||||
<img src="./../assets/RunLoop-SourceCode.png" style="zoom:30%" />
|
||||
|
||||
假设一个 NSTimer 被加到 RunLoop 开头,NSTimer 执行周期为1s,RunLoop 前面任务繁重,第一次走完一个完整的 RunLoop 需要0.4s,然后从头检测 NSTimer 有没有到时间,发现还没到继续执行 RunLoop 后续逻辑。后面遇到卡顿任务了,第二次 RunLoop 用了0.5s,然后从头检测 NSTimer 有没有到时间,0.4+0.5还不到时间,继续跑,第三次 RunLoop 比较轻松,耗时0.2s,再判断定时器时间有没有到,则此次已经0.4+0.5+0.2=1.1s了,此时 NSTimer 的事件被执行,此时精确度已经不够了(每次 RunLoop 的执行时间不固定)
|
||||
|
||||
如果 NSTimer 被添加到了一个特定的模式,当滚动视图时, RunLoop 会切换到 `UITrackingRunLoopMode`,如果 NSTimer 没有被添加到这个模式,它就不会触发。
|
||||
|
||||
当 RunLoop 没有事件可处理时,它会进入休眠状态。这意味着即使定时器的时间间隔到了,但 `RunLoop` 可能还在休眠中,因此定时器不会立即触发。
|
||||
|
||||
|
||||
|
||||
网上有些针对 FPS 帧率的检测是基于 CADisplayLink 计算的,所以这种方案不准确。具体可以查看文章:[带你打造一套 APM 监控系统](./1.74.md)
|
||||
|
||||
|
||||
|
||||
GCD 的 timer 会更加准时,底层依赖系统内核,不依赖 RunLoop。
|
||||
|
||||
```objectivec
|
||||
@property (nonatomic, strong) dispatch_source_t timer;
|
||||
@@ -86,27 +299,17 @@ self.timer = timerSource;
|
||||
|
||||
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
|
||||
<img src="./../assets/NSTimerMemoryLeakFixedByProxy.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@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];
|
||||
```
|
||||
|
||||
### 高精度定时器封装
|
||||
|
||||
@@ -239,122 +442,7 @@ dispatch_semaphore_t semaphore_;
|
||||
|
||||

|
||||
|
||||
### 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 增加分类
|
||||
|
||||
@@ -476,6 +564,8 @@ __strong __typeof(&*weakSelf)self = weakSelf;
|
||||
|
||||
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
|
||||
|
||||
|
||||
|
||||
## 检测
|
||||
|
||||
根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)
|
||||
Reference in New Issue
Block a user