# Runloop与线程

## RunLoop笔记

runloop是用来管理 事件/消息，在线程没有处理消息时，休眠避免资源占用，有消息到来时立刻被唤醒

runloop实际上就是一个对象，这个对象管理了其要处理的事件和消息。并提供了一个入口函数来

- 主线程的RunLoop在应用启动的时候就会自动创建
- 其他线程则需要在该线程下自己启动
- 不能自己创建RunLoop
- RunLoop并不是线程安全的，所以需要避免在其他线程上调用当前线程的RunLoop
- RunLoop负责管理autorelease pools 负责处理消息事件，即输入源事件和计时器事件

**子线程的runloop默认是不开启循环，比如子线程的的NSTimer会失效（可以手动开启RunLoop循环）**

可以将timer加在子线程中并让runloop循环（但runloop一直在循环），可以自己写个循环来控制runloop的停止，主线程被杀死后，子线程还可以继续运行

runloop用于渲染UI，循环非常非常快

- kCFRunLoopEntry -- 进入runloop循环
- kCFRunLoopBeforeTimers -- 处理定时调用前回调
- kCFRunLoopBeforeSources -- 处理input sources的事件
- kCFRunLoopBeforeWaiting -- runloop睡眠前调用
- kCFRunLoopAfterWaiting -- runloop唤醒后调用
- kCFRunLoopExit -- 退出runloop

### 程序启动过程
从程序启动开始到view显示：
start -> (加载framework，动态静态链接库，启动图片，Info.plist等) -> main函数 -> UIApplicationMain函数：
  - 初始化UIApplication单例对象
  - 初始化AppDelegate对象，并设为UIApplication对象的代理
  - 检查Info.plist设置的xib文件是否有效，如果有则解冻Nib文件并设置outlets，创建显示key window、rootViewController、与rootViewController关联的根view（没有关联则看rootViewController同名的xib），否则launch之后由程序员手动加载。
  - 建立一个主事件循环，其中包含UIApplication的Runloop来开始处理事件。

**UIApplication：**
1. 通过window管理视图；
2. 发送Runloop封装好的control消息给target；
3. 处理URL，应用图标警告，联网状态，状态栏，远程事件等。

**AppDelegate：**
管理UIApplication生命周期和应用的五种状态(notRunning/inactive/active/background/suspend)。

**Key Window：**
1. 显示view；
2. 管理rootViewcontroller生命周期；
3. 发送UIApplication传来的事件消息给view。

**rootViewController：**
1. 管理view（view生命周期；view的数据源/代理；view与superView之间事件响应nextResponder的“备胎”）;
2. 界面跳转与传值；
3. 状态栏，屏幕旋转。

**view：**
1. 通过作为CALayer的代理，管理layer的渲染（顺序大概是先更新约束，再layout再display）和动画（默认layer的属性可动画，view默认禁止，在UIView的block分类方法里才打开动画）。layer是RGBA纹理，通过和mask位图（含alpha属性）关联将合成后的layer纹理填充在像素点内，GPU每1/60秒将计算出的纹理display在像素点中。
2. 布局子控件（屏幕旋转或者子视图布局变动时，view会重新布局）。
3. 事件响应：event和guesture。控制器生命周期


### 如何将线程置于休眠状态，以避免空闲时浪费 CPU 资源
以避免浪费 CPU 资源，通常需要使用运行循环（RunLoop）或 GCD（Grand Central Dispatch）等机制。这样可以让线程在没有任务时进入休眠状态，当有任务需要处理时再唤醒线程。以下是一些常见的方法：

1. 使用 RunLoop：

使用运行循环是在 iOS/macOS 开发中控制线程休眠和唤醒的一种常见方法。RunLoop允许线程在没有任务时进入休眠状态，当有任务需要处理时自动唤醒。

```
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
```

2. 使用 GCD 队列：

GCD 提供了异步任务的调度机制，你可以创建一个串行队列，将任务提交到队列中，然后使用 dispatch_async 或 dispatch_sync 函数来执行任务。当队列为空时，线程会休眠，直到有任务到达队列。

```
dispatch_queue_t myQueue = dispatch_queue_create("com.example.myqueue", NULL);
dispatch_async(myQueue, ^{
    // 执行任务
});
```


### runloop退出条件
- app退出；线程关闭；设置最大时间到期；modeItem为空；
- 同一时间一个runloop只能在一个mode，切换mode只能退出runloop，再重进指定mode（隔离modeItems使之互不干扰）；
- 一个item可以加到不同mode；一个mode被标记到commonModes里（这样runloop不用切换mode）

### runloop如何优化
优化运行循环（RunLoop）的主要目标是减少资源消耗、提高性能和响应性。下面是一些可以优化RunLoop的方法：

- 避免空闲循环： 不要让RunLoop进入无限循环，尤其是在主线程上。空闲循环会浪费CPU资源。可以使用合适的运行模式，让RunLoop在没有任务时休眠。

- 合理使用运行模式： 理解RunLoop的运行模式，根据需要选择合适的模式。使用合适的模式可以确保只在需要的时候唤醒RunLoop。

- 限制Observer的使用： 观察者（Observer）是用于监听RunLoop事件的，但过多的观察者可能会导致性能下降。只使用必要的观察者，或者在不需要的时候暂时禁用观察者

- 处理输入源： 如果你的应用依赖于输入源（如点击、触摸、网络数据等），及时处理这些输入源以减少事件积压。

- 将任务移到后台线程： 长时间运行的任务应该在后台线程中执行，以免阻塞主线程的RunLoop。使用GCD或操作队列来管理后台任务。

- 避免UI更新： 避免在常驻RunLoop的线程上执行与UI更新相关的操作，这可能导致性能下降和界面不响应。UI更新应该在主线程中执行。

- 合理使用定时器： 使用定时器时要小心，不要创建过多的定时器。确保及时销毁不再需要的定时器。

- 使用RunLoop源代码优化工具： Xcode提供了工具来分析和优化RunLoop的性能。你可以使用"Instruments"来检测和解决性能问题。

- 避免长时间运行的同步任务： 避免在主线程上运行长时间运行的同步任务，这会导致RunLoop阻塞。如果需要同步任务，可以将其移到后台线程。

- 优化事件处理： 将事件处理逻辑尽量简化，减少不必要的计算和内存分配。确保事件处理尽可能快速完成。

## runloop检测卡顿
### 卡顿的原因
卡顿通常是由于主线程长时间被阻塞，无法响应用户输入或渲染界面而导致的现象。卡顿的原理涉及到主线程的运行机制和事件处理，主要原因包括：

- 死锁：主线程拿到锁 A，需要获得锁 B，而同时某个子线程拿了锁 B，需要锁 A，这样相互等待就死锁了。
- 抢锁：主线程需要访问 DB，而此时某个子线程往 DB 插入大量数据。通常抢锁的体验是偶尔卡一阵子，过会就恢复了。
- 主线程大量 IO：主线程为了方便直接写入大量数据，会导致界面卡顿。
- 主线程大量计算：算法不合理，导致主线程某个函数占用大量 CPU。
- 大量的 UI 绘制：复杂的 UI、图文混排等，带来大量的 UI 绘制。

**如何怎么定位问题**
- 死锁一般会伴随 crash，可以通过 crash report 来分析。
- 抢锁不好办，将锁等待时间打出来用处不大，我们还需要知道是谁占了锁。
- 大量 IO 可以在函数开始结束打点，将占用时间打到日志中。
- 大量计算同理可以将耗时打到日志中。
- 大量 UI 绘制一般是必现，还好办；如果是偶现的话，想加日志点都没地方，因为是慢在系统函数里面。

### 如何判定卡顿
一般来说，用户感受得到的卡顿大概有三个特征：

1. FPS 降低
2. CPU 占用率很高
3. 主线程 Runloop 执行了很久

#### 如何确定卡顿参数
 FPS 不好衡量，抖动比较大。而对于抢锁或大量 IO 的情况，光有 CPU 是不行的。我们用下面两个判断卡顿了

1. CPU 占用超过了100%
2. 主线程 Runloop 执行了超过2秒

### 监测卡顿
- 使用子线程定时向主线程中仍入空任务，超过指定时间任务未被执行则判定为卡顿。(缺点: 会不停的唤醒主线程runloop，有一定损耗)
- 基于runloop检测，使用子线程实时检测runloop状态，执行超过指定时间则判定为卡顿。(缺点: 捕获到的卡顿堆栈，不一定是最耗时的任务。不过最耗时任务有较大的概率被捕获到)

![](media/16994250704782.jpg)
**Runloop 的起始最开始和结束最末尾位置添加 Observer，获取主线程的开始和结束状态。卡顿监控起一个子线程定时检查主线程的状态，当主线程的状态运行超过一定阈值则认为主线程卡顿，从而标记为一个卡顿**

![](media/16993537207607.jpg)

开辟一个子线程定时检查主线程的状态，并记录下主线程在各个运行状态的时间点，当主线程的运行状态超过一定的时间阈值后，则认为主线程卡顿。记录这时的堆栈信息，并进行相应的处理。

![](media/16994250528546.jpg)


**降低检测带来的性能损耗**
- 内存 dump：每1秒检查一次，如果检查到主线程卡顿，就将所有线程的函数调用堆栈 dump 到内存中。
- 文件 dump：如果内存 dump 的堆栈跟上次捕捉到的不一样，则 dump 到文件中；否则按照斐波那契数列将检查时间递增（1，1，2，3，5，8…）直到没有遇到卡顿或卡顿堆栈不一样。这样能够避免同一个卡顿写入多个文件的情况，也能避免检测线程围着同一个卡顿空转的情况。
  