RunLoop

RunLoop 简介

image.png

RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。

  • 运行循环,在程序运行过程中循环做一些事情(如接收消息、处理消息、休眠等待等);
  • RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象;
  • RunLoop不是一个简单的do...while循环,它涉及到用户态和内核态之间的切换。
  • 线程和RunLoop是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
    自己创建的线程默认是没有开启RunLoop的

事件循环

事件循环就是对事件/消息进行管理,事件循环可以达到:

  • 没有消息需要处理时,休眠线程以避免资源占用。从用户态切换到内核态,等待消息;
  • 有消息需要处理时,立刻唤醒线程,回到用户态处理消息;
  • 通过调用mach_msg()函数来转移当前线程的控制权给内核态/用户态。

RunLoop 的基本作用

  • 保持程序的持续运行:
    如果没有RunLoopmain()函数一执行完,程序就会立刻退出。
    而我们的 iOS 程序能保持持续运行的原因就是在main()函数中调用了UIApplicationMain函数,这个函数内部会启动主线程的RunLoop
  • 处理 App 中的的各种事件(比如触摸事件、定时器事件等);
  • 节省 CPU 资源,提高程序性能:该做事时做事,该休息时休息。

各数据结构之间的联系

线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的

image

RunLoop的Mode

关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。

image

当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的

总共是有五种CFRunLoopMode:

  • kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行

  • UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)

  • UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用

  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到

  • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案

RunLoop的实现机制

image

对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。

image

RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。
即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop

Mach消息发送机制
大致逻辑为:
1、通知观察者 RunLoop 即将启动。
2、通知观察者即将要处理Timer事件。
3、通知观察者即将要处理source0事件。
4、处理source0事件。
5、如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
6、通知观察者线程即将进入休眠状态。
7、将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。

  • 一个基于 port 的Source1 的事件(图里应该是source0)。
  • 一个 Timer 到时间了。
  • RunLoop 自身的超时时间到了。
  • 被其他调用者手动唤醒。

8、通知观察者线程将被唤醒。
9、处理唤醒时收到的事件。

  • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
  • 如果输入源启动,传递相应的消息。
  • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

10、通知观察者RunLoop结束。

RunLoop 的应用范畴

  • 定时器(Timer)、PerformSelector
  • GCD:dispatch_async(dispatch_get_main_queue(), ^{ });
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • AutoreleasePool

RunLoop 在实际开发中的应用

  • 使用端口或自定义输入源与其他线程进行通信
  • 在子线程上使用定时器
  • 解决NSTimer在滑动时停止工作的问题
  • 控制线程的生命周期,实现一个常驻线程
  • 在 Cocoa 应用程序中使用任何performSelector...方法
  • 监控应用卡顿
  • 性能优化
  • ......
1、怎么创建一个常驻线程?

创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此可以向当前RunLoop中添加一个基于Port的Source事件维持RunLoop的事件循环

1)创建子线程并开启

  NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
  self.thread = thread;
  [thread start];

2)使用[NSMachPort port]添加一个基于Port的Source事件

 [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

3)添加一个Timer (可选)

 // 添加一个Timer
  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

  // 子线程需要开启RunLoop
  [[NSRunLoop currentRunLoop] run];

4)用常驻线程处理事情

  [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];

详细示例:


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // 创建子线程并开启
  NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
  self.thread = thread;
  [thread start];
}

- (void)show {
  
  // 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。
  
  // 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来
  
  NSLog(@"%s",__func__);
  
  // 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入
  
  // 添加Source [NSMachPort port] 添加一个端口
  [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  
  // 添加一个Timer
  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
  
  [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
  
  // 创建监听者
  CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    
    switch (activity) {
        
      case kCFRunLoopEntry:
        
        NSLog(@"RunLoop进入");
        
        break;
        
      case kCFRunLoopBeforeTimers:
        
        NSLog(@"RunLoop要处理Timers了");
        
        break;
        
      case kCFRunLoopBeforeSources:
        
        NSLog(@"RunLoop要处理Sources了");
        
        break;
        
      case kCFRunLoopBeforeWaiting:
        
        NSLog(@"RunLoop要休息了");
        
        break;
        
      case kCFRunLoopAfterWaiting:
        
        NSLog(@"RunLoop醒来了");
        
        break;
        
      case kCFRunLoopExit:
        
        NSLog(@"RunLoop退出了");
        
        break;
        
      default:
        
        break;
        
    }
  });
  
  // 给RunLoop添加监听者
  CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
  
  // 2.子线程需要开启RunLoop
  [[NSRunLoop currentRunLoop] run];
  
  CFRelease(observer);
}

- (IBAction)btnClick:(id)sender {
  // 用常驻线程处理事情
  [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

-(void)test {
  NSLog(@"%@",[NSThread currentThread]);
}
2、怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?

当我们在子线程请求数据的同时滑动浏览当前页面,如果数据请求成功立即切回主线程更新UI,那么就会影响当前正在滑动的体验。
我们就可以将更新UI事件放在主线程的NSDefaultRunLoopMode上执行即可,这样就会等用户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI

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

推荐阅读更多精彩内容