feature: dyld && LD 链接器

This commit is contained in:
杭城小刘
2024-06-29 16:00:34 +08:00
parent 1a8659e143
commit 13f7457be9
367 changed files with 12893 additions and 3049 deletions

View File

@@ -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 和 VCVC 和 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强引用 timertimer 强引用 blockblock 弱引用 self3者没有形成环。
### 采用系统 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 执行周期为1sRunLoop 前面任务繁重,第一次走完一个完整的 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_;
![](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 增加分类
@@ -476,6 +564,8 @@ __strong __typeof(&*weakSelf)self = weakSelf;
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
## 检测
根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)