iOS多线程之NSRunLoop

本文主要兑现 上篇说到的和大家分享多线程之NSRunLoop,讲真我对RunLoop的理解还是小白阶段。接下来我会以下几点来分享下 个人认为不深 但是实用,有不对的地方大家指正。

注:本文部分例子摘选网上成熟的例子,方便大家理解。如有侵权方便告知。

一、什么是NSRunLoop?

二、RunLoop作用是什么?

三、RunLoop的应用场景有哪些?

四、RunLoop和线程的关系

五、RunLoop的常见问题以及解答

一、什么是NSRunLoop

先来看看官网对NSRunLoop怎么解释的
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events. Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop. Your application does not need to create these objects explicitly; each thread, including the application’s main thread, has an associated run loop object. Only secondary threads need to run their run loop explicitly, however. The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process

翻译如下:
运行循环是与线程相关的基础架构的一部分。 运行循环是一个事件处理循环,用于调度工作并协调传入事件的接收。 运行循环的目的是在有工作时保持线程忙,并在没有线程时让线程进入休眠状态。

运行循环管理不是完全自动的。 您仍然必须设计线程的代码以在适当的时间启动运行循环并响应传入的事件。 Cocoa和Core Foundation都提供了运行循环对象来帮助您配置和管理线程的运行循环。 您的应用程序不需要显式创建这些对象; 每个线程(包括应用程序的主线程)都有一个关联的运行循环对象。 但是,只有辅助线程需要显式运行其运行循环。 作为应用程序启动过程的一部分,应用程序框架会自动在主线程上设置并运行运行循环。


官网翻译的是不是还是有点绕

RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。

二、RunLoop作用是什么?

1、保持程序持续运行

程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行。

2、处理App中的各种事件

比如:触摸事件,定时器事件,Selector事件等。

3、节省CPU资源,提高程序性能

程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情。

三、RunLoop的应用场景有哪些?

苹果的api应用:

1、自动释放池
这里的举例分析的是 APP 启动的时候,与主线程同时生成的自动释放池。

在打印 [NSRunLoop currentRunLoop] 的结果中我们可以看到与自动释放池相关的:

{activities = 0x1, callout =
_wrapRunLoopWithAutoreleasePoolHandler}

{activities = 0xa0, callout =
_wrapRunLoopWithAutoreleasePoolHandler}

APP 启动之后,苹果在主线程对应的 RunLoop 里面注册了两个 Observer, 其回调都是

_wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

与主线程的 RunLoop 运行逻辑类似,在程序中自定义的自动释放池,也是在即将退出 RunLoop 的时候,释放创建的自动释放池。

2、NSTimer (定时器)

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。

上面有讲到 RunLoop 本身就是一个。更进一步说:不断地围着圈跑

这个特性,很像城市里环城巴士

一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。

例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。

Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

3、PerformSelecter...

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

4、事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。

SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。

随后苹果注册的那个 Source1 就会触发回调,并调用_UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

5、手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其将当前的 touchesBegin/Move/End 系列回调打断。

随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

6、UI更新

在当前 RunLoop 的打印结果我们还可以看到

{activities = 0xa0,callout = 
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}

准备进入睡眠即将退出 loop 两个时间点,会调用函数更新 UI 界面.

当在操作 UI 时,某个需要变化的 UIView/CALayer 就被标记为待处理,然后被提交到一个全局的容器去,再在上面的回调执行时才会被取出来进行绘制和调整。

所以如果在一次运行循环中想用如下方法设置一个 view 的两条移动路径是行不通的。因为它会把视图的属性变化汇总起来,直接让 myView 从起点移动到终点了:

CGRect frame = self.myView.frame;
// 先向下移动
frame.origin.y += 200;
[UIView animateWithDuration:1 animations:^{
self.myView.frame = frame;
[self.myView setNeedsDisplay];
}];

// 再向右移动
frame.origin.x += 200;
[UIView animateWithDuration:1 animations:^{
    self.myView.frame = frame;
    [self.myView setNeedsDisplay];
}];

在仔细分析一下,上面代码的逻辑。

第一个动画是想要做到用1秒的时间,Y 值增加200。第二个动画想要实现的是用1秒的时间,X 值增加200.

想要实现的先下后右。

但是这样是无法实现的。因为,UI的绘制是拿到所有之后,在统一绘制的。

7、GCD

RunLoop 底层会用到 GCD 的东西,GCD 的某些 API 也用到了 RunLoop。如当调用了 dispatch_async(dispatch_get_main_queue(), block)时,主队列会把该 block 放到对应的线程(恰好是主线程)中,主线程的 RunLoop 会被唤醒,从消息中取得这个 block,回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 来执行这个 block:

image

开发日常的应用

1、UIImageView 延迟加载图片
假设我们有一个UITableView,UITableView上面有很多UITableViewCell,UITableViewCell上面有一个UIImageView(你可以想象QQ的聊天页面)。这时候一般我们的需求都是那个UIImageView的图片需要你从网络上下载,并且异步,下载成功之后更新到UIImageView上!!!

实际上这个时候我们就会碰到问题,因为我们的UITableView是可以任意拖动的,所以如果不更改NSURLConnection的运行模式,那么在拖动过程中就会冻结掉NSURLConnection的RunLoop。这时候就会产生2个不好的想象:

UIImageView迟迟得不到图片数据,从而导致迟迟无法设置image
NSURLConnection是有timeout的,所以如果被冻结时间过长,可能会导致结果被抛弃
解决此问题的代码如下:

NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];
NSURLConnection *connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]autorelease];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];

2、UIScrollerView 与 NSTimer 冲突
 首先说一下NSTimer,一个NSRunloop可以创建多个Timer。因为定时器只会运行在指定的Mode下 ,一旦Runloop进入其他模式, 定时器就不会工作了。

NSTimer的创建方法

 [NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]

该方法默认添加到当前runloop,并且Mode为kCFRunloopDefaultMode。

1  NSTimer * timer =[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
2     [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 

//手动添加到runloop 可以指定Mode
  这样声明的NSTimer可以解决在滑动scrollView时NSTimer不工作的问题。forMode:NSRunLoopCommonModes的意思为,定时器可以运行在标记为common modes模式下。具体包括两种: kCFRunloopDefaultMode 和 UITrackingRunloopMode。

RunLoop的常见问题以及解答

1、runloop 是怎样做到没事件就休息,有事件就工作的?

其实系统就是发生了一个用户态和内核态的一个相互转换,当我们调用CFRunLoopRun 的时候,系统通过调用mach_msg,来进行相互的转化

2、Runloop和线程是什么关系?

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

3、Runloop的mode作用是什么?

1、指定事件在运行循环中的优先级的,

2、线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。(比如可以优化tableview的时候可以设置UITrackingRunLoopMode下不进行一些操作,比如设置图片等。)

4、如何解决在滑动页面上的列表时,timer会暂停回调?

将Timer放到NSRunLoopCommonModes中执行即可

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];

好了。本期的NSRunLoop 就到这里了。 有问题可以在评论区说下。有空必回。

你可能感兴趣的:(iOS多线程之NSRunLoop)