先说说RunLoop 是什么?
Runloop是通过内部维护一个事件循环来对事件、消息进行管理的一个对象。是的,它是一个对象。 大家用C语言过main函数的都知道,main函数运行完成后程序就结束退出了。但是为什么iOS的App的main函数运行完之后APP还能一直运行呢?这就是Runloop的功劳。 这也是Runloop最基本的应用。 参考下面iOS的main函数:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
复制代码
Runloop是个对象,怎么获取呢?
- Foundation [NSRunloop currentRunLoop];获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop];获得主线程的Runloop对象
- Core Foundation CFRunLoopGetCurrent();获得当前线程的RunLoop对象 CFRunLoopGetMain();获得主线程的Runloop对象
RunLoop的实现机制是什么?
为了方便Runloop机制的理解,下面写一段伪代码来表示一下RunLoop循环。
function runloop() {
initialize();
do {
var message = get_next_message();//从队列获取消息
process_message(message);//处理消息
} while (message != quit);//当触发quit条件时,Runloop退出
}
复制代码
从代码代码可以看出,Runloop的处理机制是 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息)。
RunLoop的核心是什么?
就是它如何在没有消息处理时休眠,在有消息时又能唤醒。这样可以提高CPU资源使用效率 当然RunLoop它不是简单的while循环,不是用sleep来休眠,毕竟sleep这方法也是会占用cpu资源的。那它是如何实现真正的休眠的呢?
那就是:没有消息需要处理时,就会从用户态切换到内核态,用户态进入内核态后,把当前线程控制器交给内核态,这样的休眠线程是被挂起的,不会再占用cpu资源。
这里要注意用户态和内核态 这两个概念,还有mach_msg()方法。 内核态 这个机制是依靠系统内核来完成的(苹果操作系统核心组件 Darwin 中的 Mach )。
唤醒Runloop的事件
- 收到基于 port 的 Source1 的事件
- Timer到时间执行
- RunLoop自身的超时时间到了
- 被其他调用者手动唤醒
为什么只有主线程的Runloop是自动开启的?
mian()函数中调用UIApplicationMain,这里会创建一个主线程,用于UI处理,为了让程序可以一直运行并接收事件,所以在主线程中开启一个runloop,让主线程常驻.
PerformSelector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?
不起作用,子线程默认没有 Runloop。 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。可以使用 GCD的dispatch_after来实现afterDelay这样的需求。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效,。
UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动TableView的Cell时,label是否刷新?为什么?
不刷新了。 因为NSTimer对象是以NSDefaultRunLoopMode添加到主运行循环中的时候, TableView(ScrollView)滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。如果希望在滚动时,定时器也能运行,那就应该使用common mode。 通过 CFRunloopAddTimer(runloop,timer ,commonMode) 实现。就是同步把事件源timer用同一个mode.
2.为什么只在主线程刷新UI
我们所有用到的UI都是来自于UIKit这个基础库.因为objc不是一门线程安全的语言所以存在多线程读写不同步的问题,如果使用加锁的方式操作系统开销很大,会耗费大量的系统资源(内存、时间片轮转、cpu处理速度…),加上上面讲到的系统事件的接收处理都在主线程,如果UI异步线程的话 还会存在 同步处理事件的问题,所以多点触摸手势等一些事件要保持和UI在同一个线程相对是最优解.
另一方面是 屏幕的渲染是 60帧(60Hz/秒), 也就是1秒钟回调60次的频率,(iPad Pro 是120Hz/秒),我们的runloop 理想状态下也会按照时钟周期 回调60次(iPad Pro 120次), 这么高频率的调用是为了 屏幕图像显示能够垂直同步 不卡顿.在异步线程的话是很难保证这个处理过程的同步更新. 即便能保证的话 相对主线程而言 系统资源开销 线程调度等等将会占据大部分资源和在同一个线程只专门干一件事有点得不偿失.
PerformSelector和runloop的关系
当调用NSObect的 performSelector:相关的时候,内部会创建一个timer定时器添加到当前线程的runloop中,如果当前线程没有启动runloop,则该方法不会被调用.
开发中遇到最多的问题就是这个performSelector: 导致对象的延迟释放,这里开发过程中注意一下,可以用单次的NSTimer替代.
如何使线程保活?
想要线程保活的话就开启该线程的runloop即可,注意:在NSThread执行的方法中添加while(true){},这样是模拟runloop的运行原理,结合GCD的信号量,在{}代码块中处理任务.
但是注意 开启runloop的方法要正确
//测试开启线程
- (void)memoryTest {
for (int i = 0; i < 100000; ++i) {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
[self performSelector:@selector(stopThread) onThread:thread withObject:nil waitUntilDone:YES];
}
}
//线程停止
- (void)stopThread {
CFRunLoopStop(CFRunLoopGetCurrent());
NSThread *thread = [NSThread currentThread];
[thread cancel];
}
//运行线程的runloop 注意 意添加的那个空port,否则会出现内存泄露
- (void)run {
@autoreleasepool {
NSLog(@"current thread = %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
if (!self.emptyPort) {
self.emptyPort = [NSMachPort port];
}
[runLoop addPort:self.emptyPort forMode:NSDefaultRunLoopMode];
[runLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];
}
}
//下列代码用于模拟线程内部做的一些耗时任务
- (void)printSomething {
NSLog(@"current thread = %@", [NSThread currentThread]);
[self performSelector:@selector(printSomething) withObject:nil afterDelay:1];
}
//模拟手动点击按钮 让 runloop停掉
- (void)stopButtonDidClicked:(id)sender {
[self performSelector:@selector(stopRunloop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)stopRunloop {
CFRunLoopStop(CFRunLoopGetCurrent());
}
复制代码
AFNetworking 中如何运用 Runloop?
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
复制代码
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
复制代码
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
解释一下Runloop在 NSTimer中的的作用
NSTimer 其实就是 CFRunLoopTimerRef,这两个类之间,是可以交换使用的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,在发生阻塞状态并不会准时回调给Timer。某个时间点被错过了,不会在延期时间后给你执行。比如等公交,如果10:10 有一趟公交,我没赶上,那我只能等 10:20 这一趟。10:10分那趟不会再回来的。
Runloop 和线程的关系?
Runloop 和是一对一的关系,一个线程对应一个 Runloop。主线程的默认就有了 Runloop。 可以通过数据结构看出来,创建线程时,线程默认是没有runloop的,需要手工创建线程的runloop。
有了线程,你觉得为什么还要有runloop?
Runloop最主要的作用 就是它如何在没有消息处理时休眠,在有消息时又能唤醒。这样可以提高CPU资源使用效率 。runloop 另外一个作用是消息处理。只有线程,是做不到这点的。
GCD 在Runloop中的使用?
GCD由子线程返回到主线程,只有在这种情况下才会触发 RunLoop。会触发 RunLoop 的 Source 1 事件。
CADispalyTimer和Timer哪个更精确
当然是CADisplayLink 更精确。
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
看上面Runloop在 NSTimer中的使用的问题,就知道NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。可见 NSTimer的定时是很不靠谱的。
CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。
Runloop和线程是什么关系?
每条线程都有唯一的一个与之对应的RunLoop对象;主线程的RunLoop已经自动创建,子线程的RunLoop需要主动创建;RunLoop在第一次获取时创建,在线程结束时销毁
Runloop的mode作用是什么?
指定事件在运行循环中的优先级的,线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。(比如可以优化tableview的时候可以设置UITrackingRunLoopMode下不进行一些操作,比如设置图片等。)
以+scheduledTimerWithTimeInterval:的方式触发的timer,在滑动页面上的列表时,timer会暂停回调, 为什么?
滑动scrollView时,主线程的RunLoop
会切换到UITrackingRunLoopMode
这个Mode,执行的也是UITrackingRunLoopMode
下的任务(Mode中的item),而timer是添加在NSDefaultRunLoopMode
下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode
的任务执行完毕,runloop切换到NSDefaultRunLoopMode
后,才会继续执行timer。
如何解决在滑动页面上的列表时,timer会暂停回调?
将
Timer
放到NSRunLoopCommonModes
中执行即可