iOS开发经验(18)-Runloop

目录

  1. Runloop
  2. RunLoop 与线程
  3. 个人理解总结
  4. 应用场景
1. 什么是RunLoop

基本作用

  • 保持程序的持续运行(do-while循环,使app不断运行)
  • 处理App中的各种事件(触摸、定时器、Selector)
  • 节省CPU资源、提高程序性能:该做事的时候做事,该休息的时候休息。

RunLoop基本运行流程
运行逻辑总结:一个线程对应一个runLoop,主线程的runloop是程序一启动,默认就创建一个runloop,创建好了之后就会给它添加一些默认的模式,每个模式里面会有很多的 source /timer/observer ,添加好这些模式后,observer就会监听主线程的runloop,进入runloop后,就开始处理事件,先处理timer,再处理source0,source0处理完之后再处理source1,当把这些所有的事件反复的处理完之后,如果没有事件了,那么runloop就会进入睡眠状态,当用户又触发了新的事件,就会唤醒runloop,唤醒runloop后回到第二步,重新处理新的timer,新的source0,新的source1,处理完后就睡眠,一直反复,当我们把程序关闭或者强退,这个时候observer就会监听都runloop退出了。
简单说就是:
先进入 RunLoop,处理系统默认事件,触发事件的时候,RunLoop 醒来处理 timer、source0、source1,处理完再睡觉。

运行循环本质
线程在执行中的休眠和激活就是由RunLoop对象进行管理的
Runloop 轮询用来响应事件,runloop里的任务串行执行,容易受堵塞

main 函数中的 RunLoop
UIApplicationMain函数内部就启动了一个RunLoop,所以UIApplicationMain 函数一直没有返回,保持了程序的持续运行。这个默认启动的 RunLoop 是跟主线程相关联的

2. RunLoop 与线程

一条线程对应一个 RunLoop,主线程的 RunLoop 只要程序已启动就会默认创建并与主线程绑定好,RunLoop 底层的实现是通过字典的形式来将 线程 和 RunLoop 来绑定的,RunLoop 可以理解为懒加载,子线程的 RunLoop 可以调用 currentRunLoop,先从字典里面根据子线程取,如果没有就会去创建并与子线程绑定,保存到字典当中。每个 RunLoop 里面有很多的 Mode,每个 Mode 里面又有很多的source、timer、observer。RunLoop 在同一时刻只能执行一种 Mode,当执行这种 Mode 的时候,只有这种 Mode 中的source、timer、observer 有效,别的 Mode 无效,这样做是为了避免逻辑的混乱。

  • 每条线程都有唯一的一个与之对应的 RunLoop 对象
  • 主线程的 RunLoop 自动创建好了,子线程的 RunLoop 需要主动创建
  • RunLoop 在第一次获取时创建,在线程结束时销毁
3. 获取RunLoop 对象

Foundation

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

Core Foundation

//当前RunLoop
CFRunLoopGetCurrent();
//主线程 RunLoop
CFRunLoopGetMain();

源:分为输入源和定时源。必须将至少其中一个添加到Runloop中,才能保证Runloop不立即退出;当你创建输入源的时候,需要将其分配给 runloop 中的一个或多个模式;模式只会在特定事件影响监听的源。

  • 输入源:
    • 自定义输入源-source0 用户操作触摸事件源。使用回调函数来配置自定义输入源
    • 基于端口的输入源-source1 接受分发系统事件。不需要直接创建输入源。只要简单的创建对象,并使用 NSPort 的方法将该端口添加到 Ruhnloop 中
  • 定时源:产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和 runloop 的特定模式相关。

CoreFoundation中关于RunLoop的5个类

  • CFRunLoopRef:运行循环对象,也就是它自身
  • CFRunLoopModeRef:指定runloop的运行模式。作用:给事件源分组,避免互相影响,逻辑混乱。运行模式1个runLoop可以有很多个Mode,1个Mode可以有很多个Source,Observer,Timer,但是在同一时刻只能同时执行一种Mode
  • CFRunLoopSourceRef:输入源
  • CFRunLoopTimerRef:定时源,定时器;必须加入到runloop
  • CFRunLoopObserverRef(观察者,观察是否有事件)

系统默认注册了 5个Mode:

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个 Mode 下运行的
  • UITrackingRunLoopMode:界面跟踪 Mode,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个Mode,启动完成之后就不再使用。
  • GSEventReceiveRunLoopMode:接收系统时间的内部 Mode,通常用不到。
  • kCFRunLoopCommonModes(比较特殊):这时一个占位用的 Mode,不是一种真正的 Mode。

RunLoop观察者介绍:

  • CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
  • Observer是监听RunLoop状态的,CoreFunction向线程添加runloop observers来监听事件,意在监听事件发生时来做处理。
  • 线程除了处理输入源,RunLoop也会生成关于Run Loop行为的通知(notification)。RunLoop观察者(Run-Loop Observers)可以收到这些通知,并在线程上面使用他们来作额外的处理;如果RunLoop没有任何源需要监视的话,它会在你启动之际立马退出。

在每次运行开启RunLoop的时候,所在线程的RunLoop会自动处理之前未处理的事件,并且通知相关的观察者。
具体的顺序如下:

  1. 通知观察者RunLoop已经启动
  • 通知观察者即将要开始的定时器
  • 通知观察者任何即将启动的非基于端口的源
  • 启动任何准备好的非基于端口的源
  • 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
  • 通知观察者线程进入休眠状态
  • 将线程置于休眠知道任一下面的事件发生:
  • 某一事件到达基于端口的源
  • 定时器启动
  • RunLoop设置的时间已经超时
  • RunLoop被显示唤醒
  • 通知观察者线程将被唤醒
  • 处理未处理的事件
  • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
  • 如果输入源启动,传递相应的消息
  • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
  • 通知观察者RunLoop结束。(自动释放池

** RunLoop底层实现原理**
RunLoop 底层的实现是通过字典的形式来将 线程 和 RunLoop 来绑定的,RunLoop 可以理解为懒加载,子线程的 RunLoop 可以调用 currentRunLoop,先从字典里面根据子线程取,如果没有就会去创建并与子线程绑定,保存到字典当中。每个 RunLoop 里面有很多的 Mode,每个 Mode 里面又有很多的source、timer、observer。RunLoop 在同一时刻只能执行一种 Mode,当执行这种 Mode 的时候,只有这种 Mode 中的source、timer、observer 有效,别的 Mode 无效,这样做是为了避免逻辑的混乱。

3. 个人理解总结

runloop就是一个do-while循环;
runloop就是用来接受事件源,管理线程,安排线程处理事件。线程是执行任务的。app需要持续运行,如果在主线程里,不开启runloop,就会关闭app。所以程序启动的时候,在创建主线程的时候,runloop也被系统创建了,来保持app持续运行、安排线程处理事件;
它以dic的形式跟线程绑定在一起,key是线程,value是它的runloop。创建方式是懒加载;
子线程开启时,如果没有获取runloop,执行完任务就会销毁,如果你想让线程不自动销毁,可以获取runloop,让runloop安排线程添加源(输入源,计时器源)并执行任务。在添加了 source 以后,你可以给 runloop 添加 observers 来监测 runloop 的不同的执行的状态。注意如果不添加源,runloop会立马退出。

runloop有5个大类:
1. 自身对象;
2. mode:指定事件处理模式 ;在设置 RunLoopMode 以后,你的 RunLoop 就会自动过滤和其他 Mode 相关的事件源,而只监视和当前设置 Mode 相关的源(以及通知相关的观察者)。
3. souce:事件(用户操作,系统事件);
4. timer:计时器
5. Observer:给 RunLoop 注册观察者 Observer,以便监控 RunLoop 的运行过程

mode补充:
主要有三个mode对象:
默认是主线程下的有以下作用:等待唤醒;安排工作优先级顺序; 大多数工作中默认的运行方式。
第二个,使用这个Mode去跟踪来自用户交互的事件,比如UITableView上下滑动,当scroll滑动时,切换为trackmode,让scroll优先级提高,其他事件优先级排后,保证流畅 ;
第三个基于上面两个的混合体:什么时候用,即让scroll流畅运行,也让timer事件得到回调得以运行

Runloop退出
移除runloop的输入源和定时器也可能导致run loop退出

使用方法:
开辟线程,获取runloop
自定义事件源或使用系统端口NSPort,添加到runloop;
指定mode给事件分组;
添加观察者,监听状态;
当事件的模式与消息循环的模式匹配的时候,消息才会运行:让线程即将休眠时,执行任务;接受到用户触摸事件时,切换mode,暂停任务,保证流畅。

4. 应用场景

1. 定时器

  • NSTimer+NSRunLoop:容易受线程堵塞影响(此文主要讲解这个)
  • GCD定时器:GCD 创建的好处,不受 RunLoopMode 的影响。

NSTimer:
就是CFRunLoopTimerRef。
主要用于计时器的工作,当创建完计时器,必须要把它加入runloop中才能进行正常回调,
提问1.那么为什么要将它加入runloop?
回答:这是由于计时器的功能决定的,计时器要不断的运行休眠切换,是持续性的行为。如果不放在runloop中,它无法持续进行,只有runloop才能安排线程什么时候处理这个事件。通过自身的observer类不断监听,来进行回调休眠回调休眠。

提问2.NSTimer有什么需要注意的地方或者说有什么缺点?
回答:需要注意循环引用问题:因为NSTimer强持有target(为什么要强持有target:为了在运行中怕它被销毁,事件的不断持续运行时,所以要强持有),在once的情况下,一般没有问题;但当repeat=YES时,如果我们不主动调用invalid方法,它会在强持有target的情况下无限进行下去,造成内存泄漏。
解决办法:

  1. 手动调用invalid方法并置为nil
  2. 构造一个中间类,提供传入对象和方法的接口,NSTimer对此对象进行强持有。而此对象会自己销毁,进而不会永远被NSTimer所持有造成内存泄漏。

提问3. 在cell上使用NSTimer显示倒计时,如何即保障滑动流畅又保持数据实时更新并显示
回答:创建timer,手动加入到runloop中,指定mode为commonmode模式。

2. ImageView显示
另外还有一个trick是当tableview的cell从网络异步加载图片, 加载完成后在主线程刷新显示图片, 这时滑动tableview会造成卡顿. 通常的思路是tableview滑动的时候延迟加载图片, 等停止滑动时再显示图片. 这里我们可以通过RunLoop来实现.

[self.cellImageView performSelector:@sector(setImage:)
withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

当NSRunLoop为NSDefaultRunLoopMode的时候tableview肯定停止滑动了, why? 因为如果还在滑动中, RunLoop的mode应该是UITrackingRunLoopMode.

3. PerformSelector:

[self performSelector:@selector(download:) withObject:url afterDelay:1.0f];
  • 当调用 NSObject 的 performSelector:afterDelay:后,实际上内部会创建一个 Timer 并添加到当前线程的 RunLoop 中,所以如果当前线程没有 RunLoop,则这个方法会失效。在调用时的当前线程的runloop的default模式中运行。相当于在default中加了个定时器

4. 常驻线程
创建一个线程来处理耗时且频繁的操作,例如即时聊天音频的压缩,或者经常下载,避免频繁开启线程以便提高性能, AFNetWorking就是如此。

[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];

5. 利用Runloop预处理多个cell高度
原理:滑动的时候,主线程中的runloop,会将默认的mode(处理事件)切换为trackmode,也就是说屏蔽了其他源中的事件(一种 mode对应一种事件源),保障滑动流畅。
预处理cell高度:分解成多个runloop source任务,不能在同一个runloop中迭代执行,因为会造成ui卡顿,这时就需要手动向 RunLoop 中添加 Source 任务。可以使用performer的方法,自定义事件源sourceo0任务,通过这个方法加入到指定线程的runloop中,并指定mode,在给定的 Mode 下执行,若指定的 RunLoop 处于休眠状态,则唤醒它处理事件。创建观察者监听runloop的状态。于是,我们用一个可变数组装载当前所有需要“预缓存”的 index path,每个 RunLoopObserver 回调时都把第一个任务拿出来分发。这样,每个任务都被分配到下个“空闲” RunLoop 迭代中执行,其间但凡有滑动事件开始,Mode 切换成 UITrackingRunLoopMode,所有的“预缓存”任务的分发和执行都会自动暂定,最大程度保证滑动流畅。
runloop状态:当用户停止滑动的时候,唤醒runloop,切换到默认mode,让其执行计算事件;当用户滑动的时候,通知runloop,切换trackmode,让其停止计算任务,并休眠。

[self performSelector:@selector(opCellheight:) onThread:sunThread withObject:url waitUntilDone:YES modes:array];

6. 滑动与图片刷新

  • 滑动与图片刷新:当tableView的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程回去加载图片,加载完成后主线程会设置cell的图片,但是会造成卡顿。可以设置图片的任务在CFRunloopDefaultMode下进行,当滚动tableView的时候,Runloop切换到UITrackingRunLoopMode,不去设置图片,而是而是当停止的时候,再去设置图片。
[self performSelector:@selector(download:) withObject:url afterDelay:0 inModes:NSDefaultRunLoopMode];

场景5跟6解决方法思想差不多,但是场景5的方法创建了多个source任务!更高效

你可能感兴趣的:(iOS开发经验(18)-Runloop)