iOS-RunLoop,为手机省电,节省CPU资源,程序离不开的机制

转载:iOS-RunLoop,为手机省电,节省CPU资源,程序离不开的机制

RunLoop是什么?基本操作是什么?

1、RunLoop的作用

RunLoop可以:

  • 保持程序的持续运行

  • 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)

  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息

学到这里,你就知道了RUnLoop的作用了吧。看看程序里的例子:

程序中的main函数里面:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

在UIApplicationMain里面就开启了一个RunLoop,这个默认启动的RunLoop是跟主线程相关联的。它就可以处理我们上面说的那些事情,说白了就是让CUP有时间休息,没事的时候帮我们省电。

下面我们看看怎么访问它:

2、iOS中有2套API来访问和使用RunLoop
  • 1.Foundation
    NSRunLoop

  • 2.Core Foundation
    CFRunLoopRef

2.1、两者的关系:

NSRunLoop和CFRunLoopRef都代表着RunLoop对象

NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)

2.2、如何获得RunLoop对象

Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

3、RunLoop和线程的关系

每条线程都有唯一的一个与之对应的RunLoop对象

主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建

RunLoop在第一次获取时创建,在线程结束时销毁

4、RunLoop的结构

如图所示:

102337335674367.png

一个RunLoop包含若干个Mode,

而每个Mode又包含若干个Source、Timer、Observer

对应的类是:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

每个RunLoop启动时,只能指定一种Model,并且切换Mode时,只能先退出RunLoop,这样是为了分隔开不同组的Source、Timer、Observer。

RunLoop有5种Mode:

系统默认注册了5个Mode:

NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行,可以把这个理解为一个”过滤器“,我们可以只对自己关心的事件进行监视。
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到
NSRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode

5、RunLoop的内部类

每个Mode又包含若干个Source、Timer、Observer,他们对应的类如下:

5.1、CFRunLoopTimerRef

  • CFRunLoopTimerRef是基于时间的触发器

  • CFRunLoopTimerRef基本上说的就是NSTimer,它受RunLoop的Mode影响

  • GCD的定时器不受RunLoop的Mode影响

5.2、CFRunLoopSourceRef

  • CFRunLoopSourceRef是事件源(输入源)

按照官方文档,Source的分类

  • Port-Based Sources
  • Custom Input Sources
  • Cocoa Perform Selector Sources

按照函数调用栈,Source的分类

  • Source0:非基于Port的, 用于用户主动触发事件

  • Source1:基于Port的,通过内核和其他线程相互发送消息
    5.3、CFRunLoopObserverRef

  • CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

可以监听的时间点有以下几个

102351015205266.png

添加Observer

// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});

// 添加观察者:监听RunLoop的状态

CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// 释放Observer

CFRelease(observer);

RunLoop的使用

下来是Run Loop的使用场合:

1.使用port或是自定义的input source来和其他线程进行通信
2.在线程(非主线程)中使用timer
3.使用 performSelector…系列(如performSelectorOnThread, …)
4.使用线程执行周期性工作

  • run loop不需要创建,在线程中只需要调用[NSRunLoop currentRunLoop]就可以得到

  • 假设我们想要等待某个异步方法的回调。比如connection。如果我们的线程中没有启动run loop,是不会有效果的(因为线程已经运行完毕,正常退出了)。

  • 你不需要在任何情况下都去启动一个线程的 run loop。比 如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动 run loop。Run loop 在你要和线程有更多的交互时才需要,比如以下情况:

 使用端口或自定义输入源来和其他线程通信

 使用线程的定时器

 Cocoa 中使用任何 performSelector...的方法

 使线程周期性工作

如果你决定在程序中使用 run loop,那么它的配置和启动都很简单。和所有线程 编程一样,你需要计划好在辅助线程退出线程的情形。让线程自然退出往往比强制关闭它更好。关于更多介绍如何配置和退出一个 run loop,参阅”使用 Run Loop 对象” 的介绍。

终于学好了关于RunLoop的基本概念,

我们知道了,RunLoop接收到两种事件就会去调用相应的方法处理
事件,两种事件分别是输入源(input source)和定时源 (timer source),换句话说,RunLoop就是所有要监视的输入源和定时源以及要通知的 run loop 注册观察 者的集合。

所以,我们要知道

Run loop 入口

Run loop 何时处理一个定时器

Run loop 何时处理一个输入源

Run loop 何时进入睡眠状态

Run loop 何时被唤醒,但在唤醒之前要处理的事件

Run loop 终止

例子

** 给子线程添加RunLoop**

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

- (void)show
{
    [NSRunLoop currentRunLoop]; // 只要调用currentRunLoop方法, 系统就会自动创建一个RunLoop, 添加到当前线程中
}

常驻线程

有这么一个需求,我们要在子线程中没接收一个事件就调用一次方法。但是子线程在完成任务后就销毁,全局变量强引用?试试

//
//  ViewController.m
//  NSThreadTest
//
//  Created by 薛银亮 on 14/8/10.
//  Copyright (c) 2014年 薛银亮. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong)NSThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:@"xyl"];
    self.thread = thread;
    [thread start];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:@"xyl" waitUntilDone:YES];
}

-(void)run{
    NSLog(@"runrunrunrun");
}
-(void)test{
    NSLog(@"testtesttest");
}
@end

结果令人感到遗憾:线程只能执行一个函数run,然后就死亡了。就算用全局的变量引用着,这个线程也只是存在于内存中,同样是死亡状态,不能持续的执行。

  • 想在子线程中不断执行任务,必须保证子线不处于死亡状态
  • 但是子线程执行完一次任务就进入死亡状态
  • 那我们可以把线程停留在进入死亡状态之前,这里可以用RunLoop

我们可以在线程初始化的时候执行的方法中给他创建一个运行时RunLoop,这是他就可以不断接收source,也就是这样

-(void)run{
    NSLog(@"runrunrunrun");
    [[NSRunLoop currentRunLoop]addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]run];
}

注意RunLoop:

启动前内部必须要有至少一个item,虽然Obsever也是item的一种,但是只会等待Timer和Source ,Timer是因为有回调,Source是会接收事件,所以当RunLoop里面有Timer或者Source的时候,RunLoop会等待里面的item(除Observer以外)主动给他发消息,然后Observer被动的接收RunLoop发送过来的消息,亦即是说,能主动给RunLoop发消息的item会让RunLoop跑起来并且不退出。

    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

     //1.将NSTimer添加在Default模式, 定时器只会运行在Default Mode下, 当拖拽时Mode切换为Tracking模式所以没反应
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

    // 2.将NSTimer添加在Tracking模式, , 定时器只会运行在Tracking Mode下,当停止时Mode切换为Default模式所以没反应
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

    // 3.将NSTimer添加为被标记为Common的模式, Default和Tracking都被标记为了Common, 所以都有反应
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    // 4.scheduled创建的定时器默认添加在Default模式, 所以不用手动添加, 但是后期也可以修改
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 修改模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

注意:GCD的定时器不受RunLoop的影响,因为RunLoop底层是使用GCD实现timer的

GCD定时器
有这么一个需求,需要这么一个定时器,误差几乎为0的定时器,但是无论是NSTimer还是CGDisplayLink都会有误差,而且误差都比较大,这是我们可以用GCD来实现定时器,实际上,上面已经说了,RunLoop底层也是调用GCD的source来实现NSTimer的,只是NSTimer还受mode的影响,下面来看看怎么用GCD实现

//    获取队列
    dispatch_queue_t queue = dispatch_get_main_queue();
//    创建定时器
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//    设置定时器属性(什么时候开始,间隔多大)
//    定义开始时间
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//    定义时间间隔
    uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
//    设置开始时间和时间间隔
    dispatch_source_set_timer(self.timer, start,interver, 0);
//    设置回调
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"==================") ;
    });
//        dispatch_cancel(self.timer);
//        self.timer = nil;
//    取消定时器
//    启动定时器
    dispatch_resume(self.timer);

线程除了处理输入源,Run Loops也会生成关于Run Loop行为的通知(notification)。Run Loop观察者(Run-Loop Observers)可以收到这些通知,并在线程上面使用他们来作额为的处理,我们可以像下面这样添加一个观察者给RunLoop

添加RunLoop监听

// 创建Observer
    // 第一个参数:用于分配该observer对象的内存
    // 第二个参数:用以设置该observer所要关注的的事件
    // 第三个参数:用于标识该observer是在第一次进入run loop时执行, 还是每次进入run loop处理时均执行
    // 第四个参数:用于设置该observer的优先级
    // 第五个参数: observer监听到事件时的回调block
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch(activity)
        {
            case kCFRunLoopEntry:
                NSLog(@"即将进入loop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理timers");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理sources");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"刚从休眠中唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"即将退出loop");
                break;
            default:
                break;
        }
    });

将上面的监听添加到观察者

    /*
     第一个参数: 给哪个RunLoop添加监听
     第二个参数: 需要添加的Observer对象
     第三个参数: 在哪种模式下监听
     */
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);

    // 释放observer
    CFRelease(observer);

RunLoop面试题

什么是RunLoop?
  • 从字面意思看:运行循环、跑圈

  • 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)

  • 一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop得手动启动(调用run方法)

  • RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop

  • 自动释放池什么时候释放?

  • 通过Observer监听RunLoop的状态

在开发中如何使用RunLoop?什么应用场景?
  • 开启一个常驻线程(让一个子线程不进入消亡状- 态,等待其他线程发来消息,处理其他事件)

  • 在子线程中开启一个定时器

  • 在子线程中进行一些长期监控

  • 可以控制定时器在特定模式下执行

  • 可以让某些事件(行为、任务)在特定模式下执行

  • 可以添加Observer监听RunLoop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)

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

推荐阅读更多精彩内容