7

iOS开发-RunLoop详解

 2 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzUyMDAxMjQ3Ng%3D%3D&%3Bmid=2247492019&%3Bidx=1&%3Bsn=786b8540d0ad7fd83033fd14b54fadc1
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

RunLoop概述

RunLoop是什么?为什么要有RunLoop?一般来说,一个线程只能执行一个任务,执行完就退出。如果我们需要一种机制,让线程不退出,随时能处理事件,那么我们就用到了RunLoop。那么,RunLoop是什么?RunLoop又叫运行循环,内部就是一个do-while循环,在这个循环内部不断处理各种任务,保证程序持续运行。RunLoop存在的目的就是当线程中有任务的时候,保证线程干活,当线程没有任务的时候,让线程睡眠,提高程序性能,节省资源,该做事的时候做事,该休息的时候休息(想看一下你在房间里一直转圈抗饿还是躺在床上睡觉更抗饿?)。

RunLoop的作用

1、保持程序持续运行。

App一启动就会开启主线程,主线程在开启的时候就会开启主线程对应的RunLoop,RunLoop能保证线程不被销毁,主线程不销毁,程序就会持续运行。

2、处理App中各类事件。

事件响应、手势识别、界面刷新、AutoreleasePool自动释放池、NSTimer等事件处理。

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

如概述所述,当线程中有任务的时候,保证线程干活,当线程没有任务的时候,让线程睡眠,提高程序性能,节省资源,该做事的时候做事,该休息的时候休息。

RunLoop的原理

想要更好的理解RunLoop,阅读源码是一个不错的选择。老司机说,有了源码,RunLoop也就没那么神秘了。首先我们平时所讲的RunLoop有两种, 一种是NSRunLoop,一种CFRunLoop,NSRunLoop是存在于Foundation框架中,CFRunLoop是存在于CoreFoundation框架中的。平时我们使用的是NSRunLoop,其实NSRunLoop是基于CFRunLoop的一层简单的OC封装,CFRunLoop本质是一个结构体,NSRunLoop是一个NSObject对象。NSRunLoop不是线程安全的,CFRunLoop时候线程安全的。那么我们通过源码了解一下CFRunLoop。首先看下基本数据结构

1、首先看下CFRunLoop的定义,这里我把需要关注的参数都做了注释

EjEje2e.jpg!web

由此可以看出,CFRunLoop是一个结构体,里面含有很多属性。看一下这个结构体里面我们需要关注的几个参数。每一个RunLoop都有自己的模式(Mode),而且不止一个模式。模式(Mode)里面存储的是RunLoop要处理的事件源,事件源有三种,Source、Timer、Observer这三种,下面会有详细介绍。RunLoop有很多模式,但是某一个时刻只能有一个确定的Mode,就是_currentMode,下面第二条讲述的就是RunLoop 的Mode,在RunLoop结构体里面几个与模式(Mode)相关的参数 :

_currentMode,,表示该RunLoop当前所处的模式;

_modes表示该RunLoop中所有的模式;

另外RunLoop里面有一个Mode是NSRunLoopCommonModes,这个Mode并没有什么含义,它只是对几个模式(Mode)进行标记的一个集合;

_commonModes 表示NSRunLoopCommonModes这个模式下保存的Mode,我们也可以将自定义的Mode添加到这个set里面;

_commonModeItem 表示添加到NSRunLoopCommonModes里面的Source/Timer等;

2、上面提到的RunLoop里面有很多模式(Mode),来了解一下模式CFRunLoopMode,下面是CFRunLoop的源码。

muAbE3i.jpg!web

其实与CFRunLoop相关的几个定义都是结构体,CFRunLoopMode也是结构体,看代码了解下CFRunLoopMode的几个相关参数,主要是上面标记出来的四个参数

上面说RunLoop是用来处理事件,它处理的事件主要有三种,Source、Timer、Observer,那么Source还可以分为两种,Source0和Source1,CFRunLoopMode的定义里面有四个集合,分别表示存储这四种事件源的集合,如上标注。

RunLoop中的Mode主要有以下几种:

1)KCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行。

2)UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动的时候不受其他Mode影响。

3)KCFRunLoopCommonMode:这是一个占位用的Mode,作为标记KCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode。

4)UIInitializationRunLoopMode:在刚启动App的时候进入的第一个Mode,启动完成后不再使用。

5)CSEventReceiveRunLoopMode:接受系统事件内部Mode,通常用不到

RunLoop启动的时候只能选择其中一个Mode,作为currentMode,如果需要切换Mode,只能退出当前Mode,再重新选择一个Mode进入。

到这里,基于以上CFRunLoop和CFRunLoopMode的理解,RunLoop中保存的是RunLoopMode,而RunLoopMode中保存的才是实际执行的任务。

3、RunLoopMode里面存储的是RunLoop要处理的事件源,事件源有三种,Soure、Timer和Observer。

1)CFRunLoopSourceRef,就是Soure事件源,看一下它的定义。

3yqq22V.jpg!web

CFRunLoopSourceRef是RunLoop要处理的事件源之一,version0、version1 是根据对不同事件的处理区分出来的source0、source1。

2)CFRunLoopTimerRef 。RunLoop的相关Timer事件,定时器,定时执行一个任务,也是在RunLoop中处理的。

VZZf63U.jpg!web

3)CFRunLoopObserverRef 。CFRunLoopObserverRef 是RunLoop的监听者,能够监听RunLoop的状态改变。

3UZrquq.jpg!web

RunLoop在运行过程中有以下几个状态:

u2EZzi3.jpg!web

可以给一个RunLoop添加观察。通过监测RunLoop的状态判断是否出现卡顿。创建一个Observer观察者,将创建好的观察者添加到主线程RunLoop的CommonMode模式下观察,创建一个持续的子线程专门用来监控主线程的RunLoop状态,一旦发现进入睡眠前的KCFRunLoopBeforeSource状态,或者唤醒后的状态KCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判断为卡顿,dump出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间长。

4、了解了RunLoop的基本数据结构,下面看一下RunLoop是如何运行的。

首先,如何创建一个RunLoop,其实RunLoop并不需要我们手动创建。任何一个RunLoop都与一个线程关联着,先有线程,再有RunLoop。苹果提供了两个API,让我们来获取RunLoop,CFRunLoopGetMain() 和CFRunLoopGetCurrent() ,这两个方法分别获取MainRunLoop 和当前线程的RunLoop。

ZFrIbiz.jpg!web

从上面两个函数可以看出,RunLoop是通过_CFRunLoopGet0()这个函数来获取的,并且以线程作为参数,这个函数的作用与通过key从NSDictionary获取Value极为相似。接下来,看一下_CFRunLoopGet0()的实现。

获取某个线程的RunLoop,首先以该线程作为key,从全局字典查找,如果没有找到,就新建一个,并以线程为key,RunLoop为Value存到全局字典中(如果全局字典不存在,就先初始化全局字典,并新建一个MainRunLoop保存到全局字典中)。下面是源码,我都添加了注释。

a6JzQrI.jpg!web

上面这个是获取当前RunLoop的原理,那么RunLoop内部又是如何执行任务的。这里有一个图解。

IbEfUbR.jpg!web

CFRunLoopRun 和CFRunLoopRunInMode 内部都调用了CFRunLoopRunSpecific。而CFRunLoopRunSpecific内部又调用了__CFRunLoopRun,CFRunLoopRunSpecific和__CFRunLoopRun 合起来就是RunLoop的完整实现了。看下下面一段伪代码解读,这个就是RunLoop的内部逻辑:

R7Rb22m.jpg!web

RunLoop和线程之间的关系

1、RunLoop保存在一个全局的Dictionary里面,线程为key,RunLoop为Value。

2、线程刚创建的时候是没有RunLoop对象的,RunLoop会在第一次获取它的时候创建。

3、RunLoop会在线程结束的时候销毁。

4、主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。

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

6、先有线程,再有RunLoop。

我们在开发中常见RunLoop使用

1、控制线程生命周期(线程保活、线程永驻)。原理:如果Mode里面没有任何的Soure0/Source1/Timer/Observer,RunLoop会立马退出,所以为了不让它退出,可以在RunLoop里面添加一个Soure1,AF2.x中用到常驻线程,就是这个原理。

E7veUfE.jpg!web

2、TableView延迟加载图片。把setImage放到NSDefaultRunLoopMode去做,也就是在滑动的时候并不会去调用赋值图片的方法,而是会等到滑动完毕切换到NSDefaultRunLoopMode下面才会调用。

[self.img performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:[NSDefaultRunLoopMode]];


3、解决NSTimer在滑动时停止工作的问题(将Timer添加到CommonMode里面即可)。

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

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

Timer默认是处在NSDefaultRunLoopMode模式,当我们滑动页面的时候RunLoop会切换到UITrackingRunLoopMode模式,这样我们的timer就停止工作了,就像商城的倒计时,滑动页面的时候倒计时就停止了,为了解决这个问题,需要让timer在UITrackingRunLoopMode下也能工作,而NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和UITrackingRunLoopMode的结合。 所以给timer指定NSRunLoopCommonModes模式,这样 timer可以在NSDefaultRunLoopMode、UITrackingRunLoopMode模式下都运行。

4、另外可以通过监测RunLoop的状态监测应用卡顿。

RunLoop在进入睡眠之前和唤醒后的两个loop状态定义的值,分别是KCFRunLoopBeforeSources和KCFRunLoopAfterWaiting,也就是要触发Source0回调和接收mach_port消息两个状态。创建一个Observer观察者,将创建好的观察者添加到主线程RunLoop的CommonMode模式下观察,创建一个持续的子线程专门用来监控主线程的RunLoop状态,一旦发现进入睡眠前的KCFRunLoopBeforeSource状态,或者唤醒后的状态KCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判断为卡顿,dump出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间长。

以上是开发中常用的与RunLoop相关的应用。

对于RunLoop的这篇总结,是在阅读前人博客和自己对源码的解读之后编写的。基于对RunLoop的理解,我们可以清晰的看到,RunLoop就是一个do-while循环,在这个循环内部,有事情则处理事情,没事情就休息,这样做的好处就是提高程序性能,节省资源。另外RunLoop中保存的是RunLoopMode,而RunLoopMode中保存的才是实际执行的任务。对RunLoop理解到这里,我们可以去网上搜罗一下RunLoop的案例,检阅一下自己啦,加油!

参考文献

iOS线下分享《RunLoop》by孙源@sunnyxx

深入理解RunLoop

老司机出品—源码解析之RunLoop详解

关于RunLoop部分源码的注释


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK