RunLoop

什么是RunLoop

RunLoop:运行循环,在程序运行过程中循环做一些事情
应用范畴:
- 定时器(Timer)、PerformSelector
- GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool

如果没有RunLoop

执行完第18行代码后,会即将退出程序

如果有了RunLoop

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {

    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
UIApplicationMain中会创建RunLoop对象,RunLoop中的伪代码实现大概是如下的代码:
int retVal = 0;
do{
  //睡眠中等待消息
  int message = sleep_and_wait();
  //处理消息
  retVal = process_message(message);
}while(0 == retVal);
  • 程序并不会马上退出,而是保持运行状态
  • RunLoop的基本作用:
    1.保持程序的基本运行
    2.处理App中的各种事件(比如触摸事件、定时器事件等)
    3.节省CPU资源,提高程序性能:该做事时做事,该休息时休息
    ......
RunLoop对象
  • iOS中有2套API来访问和使用RunLoop
    Foundation:NSRunLoop (OC语言)
    Core Foundation:CFRunLoopRef (C语言)
  • NSRunLoop和CFRunLoopRef都代表着RunLoop对象
    NSRunLoop是基于CFRunLoopRef的一层OC包装
    CFRunLoopRef是开源的:https://opensource.apple.com/tarballs/CF/
RunLoop与线程
  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程销毁时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
获取RunLoop对象
  • Foundation
    [NSRunLoop currentRunLoop];//获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop];//获得主线程的RunLoop对象
  • Core Foundation
    CFRunLoopGetCurrent();//获得当前线程的RunLoop对象
    CFRunLoopGetMain();//获得主线程的RunLoop对象
RunLoop相关的类
  • Core Foundation中关于RunLoop的5个类
    CFRunLoopRef
    CFRunLoopModeRef
    CFRunLoopSourceRef
    CFRunLoopTimerRef
    CFRunLoopObserverRef


    CFRunLoop底层结构的主要代码

    CFRunLoopMode底层结构的主要代码

CFRunLoopModeRef
  • CFRunLoopModeRef代表RunLoop的运行模式
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个Mode,作为currentMode
  • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入:不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响。
  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
  • 常见的2种Mode:
    kCFRunLoopDefaultMode (NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
    UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
RunLoop的运行逻辑
  • Source0
    触摸事件处理
    performSelector: onThread:
  • Source1
    基于Port的线程间通信
    系统事件捕捉
  • Timers
    NSTimer
    performSelector: withObject: afterDelay:
  • Observers
    用于监听RunLoop的状态
    UI刷新(BeforeWaiting)
    Autorelease pool(BeforeWaiting)
创建添加observer监听runloop的所有状态
第一种方法:
- (void)viewDidLoad {
    [super viewDidLoad];
    //创建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
    //kCFRunLoopCommonModes默认包括kCFRunLoopDefaultMode,UITrackingRunLoopMode
    //添加observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    //释放
    CFRelease(observer);
}
void observeRunLoopActicities(CFRunLoopObserverRef observer,CFRunLoopActivity activity,void *info){
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}
第二种方法:
- (void)viewDidLoad {
    [super viewDidLoad];
    ··先创建一个textview,然后滚动,观察打印结果
    //创建observer
    CFRunLoopObserverRef observer =  CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:{
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@",mode);
                CFRelease(mode);
                break;
            }
            case kCFRunLoopExit:{
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@",mode);
                CFRelease(mode);
                break;
            }
            default:
                break;
        }
    });
    //添加observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    //释放
    CFRelease(observer);
}
RunLoop的运行逻辑
RunLoop休眠的实现原理

RunLoop休眠的细节:
RunLoop休眠和while(1)是不一样的,while(1)虽然也不会继续往下进行执行代码,但是一直在执行死循环,属于线程阻塞,CPU是一直没有休息的;而RunLoop休眠是CPU进入休息状态。有一个比较底层的函数mach_msg()直接进入休眠状态,睡觉,属于内核层面的API,不占用CPU的资源。


RunLoop在实际开发中的应用
  • 控制线程生命周期(线程保活,AFNetworking)
    利用RunLoop保持子线程一直活在内存中。
新建一个类继承NSThread,重写dealloc方法,方便监听子线程什么时候销毁
LDThread.h文件
#import <Foundation/Foundation.h>
@interface LDThread : NSThread

@end

LDThread.m文件
#import "LDThread.h"
@implementation LDThread
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end
- (void)viewDidLoad {
    [super viewDidLoad];
    LDThread *thread = [[LDThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start];
}
- (void)run{
    NSLog(@"%s %@",__func__,[NSThread currentThread]);
}
最终打印结果:
-[ViewController run] <LDThread: 0x60000003abc0>{number = 7, name = (null)}  //number=7说明是子线程,如果是主线程,number=1
-[LDThread dealloc]

那么如果有需求,当前的子线程不要挂,要一直存在 ,比如touchBegan方法里,点击一次,就在子线程里走一次run方法,代码如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    LDThread *thread = [[LDThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start];
}
- (void)run{
    NSLog(@"%s %@",__func__,[NSThread currentThread]);
}
打印结果:
-[ViewController run] <LDThread: 0x600001541f00>{number = 8, name = (null)}
-[LDThread dealloc]

-[ViewController run] <LDThread: 0x60000157c080>{number = 9, name = (null)}
-[LDThread dealloc]
每点击一次,创建一个子线程,实现完毕就会被销毁

现在不希望线程挂掉,有没有什么办法呢?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    LDThread *thread = [[LDThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start];
}
- (void)run{
    NSLog(@"%s %@",__func__,[NSThread currentThread]);
     while (1) ;
}
这种方法虽然可以保证子线程不死,但是是已经阻塞了,也没办法做别的事情了,不可取。

那么现在用RunLoop去做,让子线程在有事情做的时候就去做,没事情做的时候就休眠

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[LDThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}
//子线程需要执行的任务
- (void)test{
    NSLog(@"%s %@",__func__,[NSThread currentThread]);
}
//这个方法的目的:线程保活
- (void)run{
    NSLog(@"%s %@",__func__,[NSThread currentThread]);
    //往RunLoop里面添加Source/Timer/Observer,这里如果不添加的话,Runloop会直接退出。因为RunLoop里边是空的
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"%s ---end---",__func__);
}

但是上面的方法会造成ViewController和thread都不会被释放,相互引用造成了强引用,所以这里需要完善一下,代码如下所示:

#import "ViewController.h"
#import "LDThread.h"
#import <objc/runtime.h>
@interface ViewController ()
@property (nonatomic,strong) LDThread *thread;
@property (nonatomic,assign,getter = isStoped)BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.stopped = NO;
    self.thread = [[LDThread alloc] initWithBlock:^{
        NSLog(@"%@ ---begin---",[NSThread currentThread]);
        //往RunLoop里面添加Source/Timer/Observer,这里如果不添加的话,Runloop会直接退出。因为RunLoop里边是空的
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (weakSelf  && !weakSelf.stopped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
//        [[NSRunLoop currentRunLoop] run];//这种开启方法开启了无限次循环,它专门用于开启一个永不销毁的线程,所以调用runloopStop也无法停止。如果想手动停止子线程,就不能调用 [[NSRunLoop currentRunLoop] run];
        NSLog(@"%@ ---end---",[NSThread currentThread]);
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    if(!self.thread) return;
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}
//子线程需要执行的任务
- (void)test{
    NSLog(@"%s %@",__func__,[NSThread currentThread]);
}
//用于停止子线程的RunLoop
- (void)stop{
    //设置标记为YES
    self.stopped = YES;
    //停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@",__func__,[NSThread currentThread]);
    //清空线程
    self.thread = nil;
}
- (void)dealloc{
    NSLog(@"%s",__func__);
    //在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
    [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
@end
  • 解决NSTimer在滑动时停止工作的问题
- (void)viewDidLoad {
    [super viewDidLoad];
    //不用RunLoop设置mode的话,在滑动tableview时,timer被停止,停止滚动之后才会继续工作。
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d",++count);
    }];
//设置如下的mode之后,定时器在拖拽tableview时不受影响
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    //NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
    //NSRunLoopCommonModes并不是一个真的模式,只是一个标记,timer能在_commonModes数组中存放的模式下工作,_commonModes数组中放着两种模式:NSDefaultRunLoopMode和UITrackingRunLoopMode
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
  • 监控应用卡顿
  • 性能优化
线程的封装(创建一条可控制生命周期的线程)
LDPermenantThread.h文件
#import <Foundation/Foundation.h>

typedef void(^LDPermenantThreadTask)(void);

//这里创建的新类是继承自NSObject的,而不是NSThread的,之所以这样做,是不想外界能直接访问NSThread里边的一些方法,只想让它访问我给提供的接口。
@interface LDPermenantThread : NSObject
/*
 开启线程
 */
- (void)run;
/*
 在当前子线程执行一个任务
 */
- (void)executeTask:(LDPermenantThreadTask)task;
/*
 结束线程
 */
- (void)stop;
@end

LDPermenantThread.m文件
#import "LDPermenantThread.h"
/* LDThread 主要就是用来监听线程周期的,正式用的时候就不用这个类了,直接用NSThread就可以了,这个类在调试的时候监听是否能销毁就可以了**/
@interface LDThread : NSThread
@end
@implementation LDThread
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end

/*  LDPermenantThread **/
@interface LDPermenantThread ()
@property (nonatomic,strong) /*LDThread*/NSThread *innerThread;
@property (nonatomic,assign,getter =  isStopped) BOOL stopped;
@end
@implementation LDPermenantThread
#pragma mark - public methods
- (instancetype)init{
    if (self = [super init]) {
        self.stopped = NO;
        __weak typeof(self) weakSelf = self;
        self.innerThread = [[/*LDThread*/NSThread alloc] initWithBlock:^{
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            while (weakSelf && !weakSelf.isStopped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];
    }
    return self;
}

- (void)run{
    if (!self.innerThread) return;
    [self.innerThread start];
}

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

- (void)stop{
    if (!self.innerThread) return;
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
- (void)dealloc{
    NSLog(@"%s",__func__);
    [self stop];
}
#pragma mark - private methods
- (void)__stop{
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}
- (void)__executeTask:(LDPermenantThreadTask)task{
    task();
}
@end
ViewController.m文件
#import "ViewController.h"
#import "LDPermenantThread.h"
@interface ViewController ()
@property (nonatomic,strong) LDPermenantThread *thread;
@property (nonatomic,strong) UIButton *stopButton;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.stopButton];
    self.thread = [[LDPermenantThread alloc] init];
    [self.thread run];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    [self.thread executeTask:^{
        NSLog(@"执行任务 -  %@",[NSThread currentThread]);
    }];
}
//停止按钮的点击事件
- (void)stop{
    [self.thread stop];
}
- (void)dealloc{
    NSLog(@"%s",__func__);
}
- (UIButton *)stopButton{
    if (_stopButton == nil) {
        _stopButton = [UIButton buttonWithType:UIButtonTypeCustom];
        _stopButton.frame = CGRectMake(100, 100, 100, 50);
        [_stopButton setTitle:@"停止" forState:UIControlStateNormal];
        [_stopButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
        [_stopButton addTarget:self action:@selector(stop) forControlEvents:UIControlEventTouchUpInside];
    }
    return _stopButton;
}
@end

上面的是用OC语言,那么用C语言怎么实现呢,init的代码如下:

- (instancetype)init{
    if (self = [super init]) {
        self.stopped = NO;
        __weak typeof(self) weakSelf = self;
        self.innerThread = [[/*LDThread*/NSThread alloc] initWithBlock:^{
            NSLog(@"begin ----");
            //创建上下文(要初始化一下结构体)
            CFRunLoopSourceContext context = {0};
            
            //创建source
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            
            //往RunLoop中添加source
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            
            //销毁source
            CFRelease(source);
            
            //启动
            while (weakSelf && !weakSelf.isStopped) {
                //第三个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
            }
            //或者写成:
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);//这样就不需要stopped这个属性了。更精简了
            NSLog(@"end ----");
        }];
    }
    return self;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容