iOS 底层 day18 RunLoop 执行流程 NSTimer 线程保活

一、RunLoop 的运行逻辑

1. Source0Source1TimersObservers 的作用?
大概了解一下,有个印象就行
2. RunLoop 的运行逻辑
RunLoop 的运行逻辑图(了解即可)
3. 一句话概括上面的流程图?
  • RunLoop 就是进入某一种循环,然后把 Source0Source1TimersObservers 拿出来执行以下,然后进入休眠,等待新的消息唤醒它
4. RunLoop 休眠的实现原理?它和代码 while(1); 这种死循环有什么区别?
  • RunLoop 的休眠是从用户态内核态 (Linux 的计算机知识)
  • RunLoop 的唤醒是从内核态用户态
  • while(1); 是让线程一直执行 ; 代码,是让程序一直在跑,并没有进行休眠,很浪费资源。

二、RunLoop 和 NSTimer

1. 为什么默认情况下 NSTimer ,在用户拖拽滚动的时候会停止调用?
  • NSTimer 是由 RunLoop 在 NSDefaultRunLoopMode 模式下调度执行的
  • RunLoop 默认情况会在 NSDefaultRunLoopMode 模式下运行,所以一般情况下 NSTimer 能正常运行
  • 当用户拖拽滚动的时候,RunLoop 会进入 UITrackingRunLoopMode 模式,所以 NSTimer 不会运行
2. 如何解决默认情况下 NSTimer ,在用户拖拽滚动的时候会停止调用?
  • 将 NSTimer 设定成 NSRunLoopCommonModes
  • 严格来讲 NSRunLoopCommonModes 并不是 RunLoop 的一种模式
RunLoop结构体
  • _commonModes 里面装着 NSDefaultRunLoopModeUITrackingRunLoopMode
  • NSRunLoopCommonModes 就是 _commonModes 里面模式都能得到执行

二、RunLoop 用于线程保活

1. 什么是线程保活?
  • 默认情况下,一个线程执行完需要执行的代码就会挂掉
  • 线程保活就是由程序猿自己控制线程的死活
2. 如何用NSThread创建一个一般线程(创建→执行代码→执行完成销毁)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    NSThread *thread = [[YYThread alloc] initWithTarget:self selector:@selector(threadStart) object:nil];
    [thread start];
}

- (void)threadStart {
    NSLog(@"%s", __func__);
}
  • YYThread 继承自 NSThread 主要重写 -dealloc 方法 用于观察线程的释放
3. 如果我们希望上面的线程一直存活要怎么办?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    NSThread *thread = [[YYThread alloc] initWithTarget:self selector:@selector(threadStart) object:nil];
    [thread start];
}

- (void)threadStart {
    NSLog(@"%s", __func__);
    while (1);
}
  • 上述代码就可以实现线程一直不死,但是不是我们想要的,我们想要它有事做事,无事休眠
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    NSThread *thread = [[YYThread alloc] initWithTarget:self selector:@selector(threadStart) object:nil];
    [thread start];
}

- (void)threadStart {
    NSLog(@"---------start-------- %s", __func__);
    // 调用获取 currentRunLoop 就会让线程自动创建 RunLoop
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"---------end-------- %s", __func__);
}
  • 思考上述代码,可以让线程不死吗?
  • 线程依然会死掉,虽然我们用获取方法创建了 RunLoop,但是前面学过 如果启动RunLoop时 Mode 里面没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出
  • 所以我们考虑往 RunLoop 中添加一个任务
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    NSThread *thread = [[YYThread alloc] initWithTarget:self selector:@selector(threadStart) object:nil];
    [thread start];
}

- (void)threadStart {
    NSLog(@"---------start-------- %s", __func__);
    // 调用获取 currentRunLoop 就会让线程自动创建 RunLoop
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"---------end-------- %s", __func__);
}
  • 这样我们的 RunLoop 就算创建并且真正运行了。
  • 思考我们能看到 NSLog(@"---------end-------- %s", __func__);的打印信息吗?
  • 不能,因为 RunLoop 就是一个运行循环,它会卡住当前线程,让线程一直不死,所以在 RunLoop 挂掉之前,NSLog(@"---------end-------- %s", __func__);都不会被执行
5. 思考下面的代码,当控制器退出销毁时,线程会被销毁吗?
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[YYThread alloc] initWithBlock:^{
        NSLog(@"---------start-------- %s", __func__);
        // 调用获取 currentRunLoop 就会让线程自动创建 RunLoop
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"---------end-------- %s", __func__);
    }];;
    [self.thread start];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}

- (void)stop {
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:NO];
    NSLog(@"%s", __func__);
}

- (void)stopRunLoop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
    [self stop];
    self.thread = nil;
}
@end
  • 线程不会销毁
  • 因为 [[NSRunLoop currentRunLoop] run]; 这个函数,我们看下官方说明

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.

  • 如果没有任何 sources 或者 timers 被添加到 RunLoop,那么这个 run 方法会离开退出;

  • 它会在 NSDefaultRunLoopMode 模式下重复调用 runMode:beforeDate: 方法

  • 换句话说:这个方法就会无限循环处理来自sourcestimers 的数据

  • CFRunLoopStop(CFRunLoopGetCurrent()); 只能停掉一次 runMode:beforeDate: ,然后[[NSRunLoop currentRunLoop] run];会立刻再调用 runMode:beforeDate:

  • 所以线程无法被销毁

6. 思考下面的代码,touchesBegan:withEvent:被调用时,线程会被销毁吗?
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.stop = NO;
    __weak typeof(self) weakSelf = self;
    self.thread = [[YYThread alloc] initWithBlock:^{
        NSLog(@"---------start-------- %s", __func__);
        // 调用获取 currentRunLoop 就会让线程自动创建 RunLoop
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.isStop) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"---------end-------- %s", __func__);
    }];;
    [self.thread start];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//    [self performSelector:@selector(doSoming) onThread:self.thread withObject:nil waitUntilDone:NO];
    self.stop = YES;
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
    self.thread = nil;
}

- (void)doSoming {
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (void)stopRunLoop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end
  • 会,这就是一个完整的保活线程的例子
  • 不过还存在一个问题,如果在 -[ViewController dealloc] 中如下调用
- (void)dealloc {
    NSLog(@"%s", __func__);
    self.stop = YES;
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
    self.thread = nil;
}
  • 当控制器销毁时,为什么 Thead 并没有被销毁呢?
  • 因为 while (!weakSelf.isStop)中的 weakSelf 被释放了,所以 while (!weakSelf.isStop)这个循环即使设置了 stop = YES,仍然会进入循环,所以我们只需完善逻辑改成 while (weakSelf && !weakSelf.isStop),那线程和控制器都能都在 - (void)dealloc中被释放掉。

三、RunLoop 线程保活 -- 代码封装

我们可以看上,如果上面的线程保活的功能,我们需要在多处调用,那么将会很麻烦,每个要用到的地方都需要添加很多相关代码。基于此,我们可以把它封装成一个迷你工具库

1. 思考,如果我们把上面的线程保活功能封装成一个对象,这个对象应该继承自谁呢?继承自NSThread ? NSThread的分类 ? 继承自NSObject ?
  • 如果使用 NSThread的分类,那么我们添加属性的时候,需要用到 关联对象添加,非常麻烦
  • 如果使用 NSThread的分类继承自NSThread 都存在一些问题:①使用者能拿到 thread 对象,调用其方法进行随意修改 ②使用者使用的时候,包含的方法太多,会让使用者迷茫如何正确使用
  • 所以,我们最终的选择是让它继承自NSObject
2. 封装代码
// 线程保活.h文件代码
#import 

typedef void (^permanentThreadTask)(void);


@interface YYPermanentThread : NSObject
- (void)run;

- (void)stop;

- (void)executeTask:(permanentThreadTask)task;
@end

// 线程保活.m文件代码
#import "YYPermanentThread.h"

/* 这个对象主要用于监测 NSThread 的释放 */
@interface YYThread : NSThread
@end
@implementation YYThread
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end

/* 我们封装的线程保活工具 */

@interface YYPermanentThread ()

@property(nonatomic, strong) NSThread *thread;
@property(nonatomic, assign, getter=isStopped) BOOL stopped;
@end
@implementation YYPermanentThread

- (instancetype)init
{
    self = [super init];
    if (self) {
        __weak typeof(self) weakSelf = self;
        self.thread = [[YYThread alloc] initWithBlock:^{
            NSLog(@"-------RunLoop start-------");
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            while (weakSelf && !weakSelf.isStopped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
            NSLog(@"-------RunLoop end-------");
        }];
    }
    return self;
}

- (void)run {
    if (self.thread == nil) return;
    [self.thread start];
}

- (void)stop {
    if (self.thread == nil) return;
    [self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}


- (void)executeTask:(permanentThreadTask)task {
    if (self.thread == nil || task == nil) return;
    [self performSelector:@selector(__executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}

#pragma mark - private method

- (void) __stopThread {
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
}

- (void) __executeTask:(permanentThreadTask)task {
    task();
}

- (void)dealloc {
    if (self.thread != nil){
        [self stop];
    }
    NSLog(@"%s", __func__);
}

@end

  • 然后我们使用起来就非常容易了
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.permanentThread = [[YYPermanentThread alloc] init];
    [self.permanentThread run];
    
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.permanentThread executeTask:^{
        NSLog(@"我在子线程干活---%@",[NSThread currentThread]);
    }];;
}
@end
3. 我们也可以基于C语言的 RunLoop 来封装,把上面 - (instancetype)init 里面的代码,换成如下代码即可。
- (instancetype)init
{
    self = [super init];
    if (self) {
        __weak typeof(self) weakSelf = self;
        self.thread = [[YYThread alloc] initWithBlock:^{
            NSLog(@"-------RunLoop start-------");
            CFRunLoopSourceContext context = {0};
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            while (weakSelf && !weakSelf.isStopped) {
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10000, NO);
            }
            NSLog(@"-------RunLoop end-------");
        }];
    }
    return self;
}

四、线程卡顿检测

1、如何打印堆栈信息?

  • 借助Linux的内核函数,backtrace() 和 backtrace_symbols()
#import 
- (void)logStack {
    void* callstack[256];
    int frames = backtrace(callstack, 256);
    char **strs = backtrace_symbols(callstack, frames);
    _backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (int i = 0; i < frames; i++) {
        [_backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    NSLog(@"====================堆栈\n %@ \n",_backtrace);
}

2、如何检测卡顿?

  • 核心思想:①创建一个observer,加入到主线程的观察者队列中,用于监测主线程的runloop状态变更 ②创建一个子线程,为kCFRunLoopBeforeSources 、kCFRunLoopAfterWaiting两个计时,如果停留在某个状态过久,就表示卡顿了。
#import 

@interface SeMonitorController : NSObject
+ (instancetype) sharedInstance;
- (void) startMonitor;
- (void) endMonitor;
- (void) printLogTrace;
@end
#import "SeMonitorController.h"
#import 
#import 

@interface SeMonitorController(){
    CFRunLoopObserverRef _observer;
    dispatch_semaphore_t _semaphore;
    CFRunLoopActivity _activity;
    NSInteger _countTime;
    NSMutableArray *_backtrace;
}

@end

@implementation SeMonitorController

+ (instancetype) sharedInstance{
    static dispatch_once_t once;
    static id sharedInstance;
    dispatch_once(&once, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

- (void) startMonitor{
    [self registerObserver];
}

- (void) endMonitor{
    if (!_observer) {
        return;
    }
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    CFRelease(_observer);
    _observer = NULL;
}

- (void) printLogTrace{
    NSLog(@"====================堆栈\n %@ \n",_backtrace);
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    SeMonitorController *instrance = [SeMonitorController sharedInstance];
    instrance->_activity = activity;
    // 发送信号
    dispatch_semaphore_t semaphore = instrance->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver
{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            0,
                                                            &runLoopObserverCallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    
    // 创建信号
    _semaphore = dispatch_semaphore_create(0);
    
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            // 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
            long st = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (_activity==kCFRunLoopBeforeSources || _activity==kCFRunLoopAfterWaiting)
                {
                    if (++_countTime < 5)
                        continue;
                    [self logStack];
                    NSLog(@"something lag");
                }
            }
            _countTime = 0;
        }
    });
}

- (void)logStack{
    void* callstack[128];
    int frames = backtrace(callstack, 128);
    char **strs = backtrace_symbols(callstack, frames);
    int i;
    _backtrace = [NSMutableArray arrayWithCapacity:frames];
    for ( i = 0 ; i < frames ; i++ ){
        [_backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
}

@end

你可能感兴趣的:(iOS 底层 day18 RunLoop 执行流程 NSTimer 线程保活)