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<UITouch *> *)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<UITouch *> *)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<UITouch *> *)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<UITouch *> *)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<UITouch *> *)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<UITouch *> *)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 <Foundation/Foundation.h>

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<UITouch *> *)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 <execinfo.h>
- (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 <Foundation/Foundation.h>

@interface SeMonitorController : NSObject
+ (instancetype) sharedInstance;
- (void) startMonitor;
- (void) endMonitor;
- (void) printLogTrace;
@end
#import "SeMonitorController.h"
#import <libkern/OSAtomic.h>
#import <execinfo.h>

@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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357