mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
feature: dyld && LD 链接器
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# RunLoop 探究
|
||||
|
||||
> 为什么 main 函数可以保持一直运行而不退出?
|
||||
>
|
||||
> 卡顿如何监控
|
||||
|
||||
|
||||
|
||||
## RunLoop 是什么
|
||||
|
||||
- 运行循环
|
||||
@@ -11,7 +17,7 @@
|
||||
|
||||
- 处理App中的各种事件(比如触摸事件、定时器事件等)
|
||||
|
||||
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
|
||||
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息。休息和工作是从用户态切换到内核态,内核态切换到用户态的不断切换
|
||||
|
||||
- ......
|
||||
|
||||
@@ -29,13 +35,17 @@
|
||||
|
||||
先附上一张总结的非常棒的RunLoop图
|
||||
|
||||

|
||||
<img src="./../assets/2019-05-09-RunLoop-review.png" style="zoom:30%" />
|
||||
|
||||
和
|
||||
|
||||

|
||||
|
||||
## RunLoop API
|
||||
一言以蔽之,什么是 RunLoop?为什么 main 函数可以保持一直运行而不退出?
|
||||
|
||||
iOS 侧 main 函数中,调用 UIApplicationMain 方法,内部启动主线程的 RunLoop,RunLoop 是一个事件循环的维护机制。有事情做的时候做事情(Source0、Source1),没有事做的时,从用户态到内核态的切换,去实现线程休眠。避免资源浪费。
|
||||
|
||||
|
||||
|
||||
## RunLoop 几个重要角色
|
||||
|
||||
### 获取 RunLoop
|
||||
|
||||
@@ -65,6 +75,8 @@ NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的
|
||||
|
||||
- RunLoop 在第一次获取时创建,在线程结束时消失
|
||||
|
||||
|
||||
|
||||
### RunLoop 相关的5个类
|
||||
|
||||
- CFRunLoopRef
|
||||
@@ -103,6 +115,8 @@ struct __CFRunLoopMode {
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
### CFRunLoopModeRef 代表 RunLoop 的运行模式
|
||||
|
||||
- 一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个 Source/Timer/Observer
|
||||
@@ -110,9 +124,7 @@ struct __CFRunLoopMode {
|
||||
- 如果需要切换 Mode,只能退出 RunLoop,则以一个 Mode 进入
|
||||
- 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出
|
||||
|
||||
QA:为什么一个 RunLoop 需要创建这么多 Mode?
|
||||
|
||||
这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响(性能、功能)。
|
||||
|
||||
系统默认注册了5个Mode
|
||||
|
||||
@@ -122,6 +134,26 @@ QA:为什么一个 RunLoop 需要创建这么多 Mode?
|
||||
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
|
||||
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
|
||||
|
||||
|
||||
|
||||
Demo:
|
||||
|
||||
<img src="./../assets/NSRunloopRoundWithCFRunloop.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
结论:NSRunLoop 是对 CFRunLoop 的一层包装。
|
||||
|
||||
|
||||
|
||||
QA:为什么一个 RunLoop 需要创建这么多 Mode?
|
||||
|
||||
这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响(性能、功能)。
|
||||
|
||||
在UITableView场景下,不同的 RunLoop Mode 主要是与 UITableView 的滑动优化和事件处理相关的。当 UITableView 滑动时,RunLoop的运行模式会从默认的 CFRunLoopDefaultMode 切换到 CFRunLoopTrackingMode,此 Mode 下 RunLoop 主要关注于处理与滑动相关的触摸事件和动画效果,而忽略其他类型的事件,如定时器事件。这是因为如果同时处理所有类型的事件,可能会导致滑动不流畅,影响用户体验。之前添加到 CFRunLoopDefaultMode 上的事件通知(如定时器事件)可能无法被及时处理,这就是为什么在UITableView 滑动时,添加到主线程的 NSTimer 可能会停止执行的原因。
|
||||
|
||||
|
||||
|
||||
### Source0、Source1、Timer、Observers 是什么
|
||||
|
||||
```c
|
||||
@@ -138,12 +170,18 @@ RunLoop 在各个 Mode 下做事情,其实就是在处理某个 Mode 中的 So
|
||||
|
||||
Source0:
|
||||
|
||||
- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档Input Source 中的 Custom 和 `performSelector:onThread` 事件源。
|
||||
- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档 Input Source 中的 Custom 和 `performSelector:onThread` 事件源。
|
||||
|
||||
- `performSelector:onThread:`
|
||||
|
||||
- 数组
|
||||
|
||||
Demo:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。
|
||||
|
||||
<img src="./../assets/TouchActionByRunLoopSource0.png" style="zoom:25%" />
|
||||
|
||||
|
||||
|
||||
Source1:
|
||||
|
||||
- 基于 Port 的线程间通信,可以主动唤醒 RunLoop
|
||||
@@ -156,7 +194,7 @@ Timers:
|
||||
|
||||
- NSTimer
|
||||
|
||||
- `performSelector:withObject:afterDelay:`
|
||||
- `performSelector:withObject:afterDelay:`,底层也是 Timer
|
||||
|
||||
Observers:
|
||||
|
||||
@@ -179,6 +217,101 @@ CFRunLoopSourceRef 事件源(输入源)
|
||||
- Source0:非基于 port 的,用户主动触发的事件
|
||||
- Source1: 基于 port的,通过内核在线程间相互发送消息
|
||||
|
||||
|
||||
|
||||
### 一对多的关系
|
||||
|
||||
<img src="./../assets/ThreadRunLoopModeStructure.png" style="zoom:70%" />
|
||||
|
||||
|
||||
|
||||
#### RunLoopTimer 的封装
|
||||
|
||||
- `+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;`
|
||||
- `+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;`
|
||||
- `- (void)performSelector:(SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds inModes: (NSArray*)modes;`
|
||||
- `+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;`
|
||||
- `- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;`
|
||||
|
||||
|
||||
|
||||
#### CFRunLoopSource
|
||||
|
||||
Source 是 RunLoop 的数据源抽象类(protocol)
|
||||
|
||||
RunLoop 定义了2个 Version 的 Source:
|
||||
|
||||
- Source0:处理 App 内部事件、App 自己负责管理(触发),如 UIEvent、CGSocket
|
||||
- Source1:由 RunLoop 和内核管理,Mach port 驱动,如 CFMachPort、CFMessagePort。
|
||||
|
||||
定义如下:
|
||||
|
||||
```c++
|
||||
struct __CFRunLoopSource {
|
||||
CFRuntimeBase _base;
|
||||
uint32_t _bits;
|
||||
pthread_mutex_t _lock;
|
||||
CFIndex _order; /* immutable */
|
||||
CFMutableBagRef _runLoops;
|
||||
union {
|
||||
CFRunLoopSourceContext version0; /* immutable, except invalidation */
|
||||
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
|
||||
} _context;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
CFIndex version;
|
||||
void * info;
|
||||
const void *(*retain)(const void *info);
|
||||
void (*release)(const void *info);
|
||||
CFStringRef (*copyDescription)(const void *info);
|
||||
Boolean (*equal)(const void *info1, const void *info2);
|
||||
CFHashCode (*hash)(const void *info);
|
||||
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
|
||||
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
|
||||
void (*perform)(void *info);
|
||||
} CFRunLoopSourceContext;
|
||||
|
||||
typedef struct {
|
||||
CFIndex version;
|
||||
void * info;
|
||||
const void *(*retain)(const void *info);
|
||||
void (*release)(const void *info);
|
||||
CFStringRef (*copyDescription)(const void *info);
|
||||
Boolean (*equal)(const void *info1, const void *info2);
|
||||
CFHashCode (*hash)(const void *info);
|
||||
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
|
||||
mach_port_t (*getPort)(void *info);
|
||||
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
|
||||
#else
|
||||
void * (*getPort)(void *info);
|
||||
void (*perform)(void *info);
|
||||
#endif
|
||||
} CFRunLoopSourceContext1;
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### CFRunLoopObserver
|
||||
|
||||
向外部报告 RunLoop 当前状态的更改。框架中很多机制都是由 RunLoopObserver 触发,比如 CAAnimation、AutoReleasePool。
|
||||
|
||||
系统或者开发者很多都是 RunLoop 的业务方。
|
||||
|
||||
|
||||
|
||||
#### CFRunLoopMode
|
||||
|
||||
Mode 是 iOS App 滑动流畅的关键。
|
||||
|
||||
不同任务被添加到不同 Mode 中去。
|
||||
|
||||
UITrackingMode 模式下,核心关注滚动时 UI 流畅相关逻辑。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### CFRunLoopObserverRef 监听 RunLoop 状态变化
|
||||
|
||||
```objective-c
|
||||
@@ -212,7 +345,7 @@ CFRelease(obersver);
|
||||
|
||||
```objective-c
|
||||
//给 RunLoop 添加监听者
|
||||
- (void)testRunLoopObserver{
|
||||
- (void) {
|
||||
|
||||
//创建监听者
|
||||
// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
|
||||
@@ -301,7 +434,7 @@ CFRelease(obersver);
|
||||
*/
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟
|
||||
|
||||
@@ -371,20 +504,22 @@ CFRelease(obersver);
|
||||
*/
|
||||
```
|
||||
|
||||
## RunLoop 内部运行原理
|
||||
|
||||

|
||||
|
||||
## RunLoop 运行原理
|
||||
|
||||
### 运行原概要
|
||||
|
||||

|
||||
|
||||
- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。
|
||||
- Source0:非基于 port 的,用户主动触发的事件。
|
||||
- Source1:基于 port,通过内核和其它线程互相发送消息
|
||||
- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop)
|
||||
|
||||
## RunLoopMode 的概念
|
||||
|
||||

|
||||
|
||||
## 底层实现
|
||||
### 源码探究
|
||||
|
||||
内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer)
|
||||
|
||||
@@ -392,7 +527,7 @@ CFRelease(obersver);
|
||||
|
||||
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
|
||||
|
||||

|
||||

|
||||
|
||||
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
|
||||
|
||||
@@ -409,6 +544,8 @@ SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterva
|
||||
}
|
||||
```
|
||||
|
||||
`CFRunLoopRunSpecific` 方法就是系统启动 RunLoop 的入口。内部先通知 Runloop 的观察者进入 Runloop 了,然后 调用 `__CFRunLoopRun` 执行核心逻辑(处理 timers、source 事件、block),最后告诉观察者退出 Runloop。
|
||||
|
||||
我们继续看看 `__CFRunLoopRun` 。源码很多很乱,对无关代码进行裁剪,便于理解流程逻辑
|
||||
|
||||
```c
|
||||
@@ -472,10 +609,10 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
|
||||
} while (1);
|
||||
// 通知 Observers:结束休眠
|
||||
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
|
||||
|
||||
handle_msg:;
|
||||
|
||||
handle_msg:; // 判断是怎么唤醒的 Runloop
|
||||
__CFRunLoopSetIgnoreWakeUps(rl);
|
||||
|
||||
//
|
||||
if (MACH_PORT_NULL == livePort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_NOTHING();
|
||||
// handle nothing
|
||||
@@ -490,7 +627,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
|
||||
__CFArmNextTimerInMode(rlm, rl);
|
||||
}
|
||||
}
|
||||
|
||||
// 被 Timer 唤醒,执行代码。
|
||||
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_TIMER();
|
||||
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
|
||||
@@ -532,8 +669,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
|
||||
|
||||
voucher_mach_msg_revert(voucherState);
|
||||
os_release(voucherCopy);
|
||||
|
||||
} while (0 == retVal);
|
||||
} while (0 == retVal); // 当 retVal == 0 的时候结束 Runloop
|
||||
return retVal;
|
||||
}
|
||||
```
|
||||
@@ -931,9 +1067,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
|
||||
}
|
||||
```
|
||||
|
||||
__CFRunLoopModeIsEmpty
|
||||
|
||||
此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
|
||||
`__CFRunLoopModeIsEmpty`函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
|
||||
|
||||
```objective-c
|
||||
// expects rl and rlm locked
|
||||
@@ -993,9 +1127,51 @@ __CFRunLoopModeIsEmpty
|
||||
}
|
||||
```
|
||||
|
||||
## RunLoop 休眠原理
|
||||
|
||||
本质上就是函数 `__CFRunLoopServiceMachPort` 来控制实现休眠。查看 CFRunLoop.c 源码可以发现,是由 `mach_msg` 实现的。不是平时的在应用层 API 上 sleep 休眠的。比如 while 循环。`mach_msg` 休眠是从用户态切换到内核态,内核 api 控制休眠,做到真正节省影响的作用,等到由新消息来到,继续切换到用户态。
|
||||
|
||||
RunLoop 内部几个核心的动作:`__CFRunLoopDoObservers`、`__CFRunLoopServiceMachPort` 、`__CFRunLoopDoTimers` 方法内部实现调用的还是 `__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__`、`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__` 等方法,均以 `__CFRUNLOOP_IS_CALLING_OUT_TO_` 方法名作为开头。可以在堆栈上得以体现。
|
||||
|
||||
|
||||
|
||||
### 运行流程
|
||||
|
||||
上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。
|
||||
|
||||
<img src="./../assets/RunLoop-SourceCode.png" style="zoom:30%" />
|
||||
|
||||
Demo:
|
||||
|
||||
1. 上面第4步的 blocks 是指可以给 RunLoop 添加 Block 任务。
|
||||
|
||||
```objective-c
|
||||
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^{
|
||||
NSLog(@"runloop block task");
|
||||
});
|
||||
```
|
||||
|
||||
2. 上面8>2,Runloop 处理 GCD Async To Main Quque
|
||||
|
||||
<img src="./../assets/RunLoopPerformTaskFromGCDMainQueue.png" style="zoom:25%" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### RunLoop 休眠原理
|
||||
|
||||
> Runloop 在处理完 timer、source、block 后会检查有没有 source1 事件,没有则休眠。这个休眠是 while 循环死等吗?怎么实现的?
|
||||
|
||||
可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用
|
||||
|
||||
<img src="./../assets/RunLoopSleepSystemCall.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
本质上就是函数 `__CFRunLoopServiceMachPort` 来控制实现休眠。查看 CFRunLoop.c 源码可以发现,是由 `mach_msg` 实现的。不是平时的在应用层 API 上 sleep 休眠的。比如 while 循环。
|
||||
|
||||
**`mach_msg` 休眠是从用户态切换到内核态,内核 api 控制休眠,做到真正节省资源的作用,等到由新消息来到,继续切换到用户态**。能力更底层,效果更好,从而更加省电
|
||||
|
||||
```c
|
||||
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
|
||||
@@ -1050,6 +1226,16 @@ static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### RunLoop 是如何响应用户操作的?
|
||||
|
||||
用户交互事件首先在 IOHID 层生成 HIDEvent,然后向事件处理线程的 Source1 的 mach port 发送 HIDEvent 消息,Source1 的回调函数将事件转化为 UIEvent 并筛选需要处理的事件推入待处理事件队列,向主线程的事件处理 Source0 发送信号,并唤醒主线程,主线程检查到事件处理 Source0 有待处理信号后,触发 Source0 的回调函数,从待处理事件队列中提取 UIEvent,最后进入 hit-test 等 UIEvent 事件响应流程
|
||||
|
||||
等待梳理完善。
|
||||
|
||||
|
||||
|
||||
## CFRunLoopTimerRef 是基于时间的触发器
|
||||
|
||||
- 基本上说就是 NSTimer,它会收到 RunLoopMode 的影响
|
||||
@@ -1170,6 +1356,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
|
||||
// ...
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Mach Port 跨线程通信
|
||||
|
||||
1. Mach IPC 基于 Mach 内核实现进程间通讯。
|
||||
@@ -1310,6 +1498,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
|
||||
|
||||
可以看到每次在点击屏幕时调用 `CFRunLoopWakeUp` 尝试唤醒 RunLoop,然后监听 RunLoop 的 _wakeUpPort,都可以在回调中获取到消息。
|
||||
|
||||
|
||||
|
||||
## RunLoop 应用场景
|
||||
|
||||
- 控制线程生命周期(线程保活)
|
||||
@@ -1320,6 +1510,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
|
||||
|
||||
- 性能优化
|
||||
|
||||
|
||||
|
||||
### NSTimer 经常会不准确,原因是什么?
|
||||
|
||||
NSTimer 在创建的时候经常会指派到特定的 NSRunLoopMode 中去,举个例子,默认创建的NSTimer 是被添加到 NSRunLoopDefaultMode 中去,当你的页面上有 UIScrollView 或者子类的时如果被拖动了,当前 RunLoop 的 NSRunloopMode 会从 NSDefaultRunLoopMode 转变为 UITrackingRunLoopMode 。遇到这种情况你需要精确的 NSTimer 的话,在创建好 NSTimer 之后,设置 RunLoopMod 为 NSRunLoopCommonModes。
|
||||
@@ -1388,23 +1580,33 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### ImageView显示(PerformSelector)
|
||||
|
||||
UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,**FPS** 达不到60。
|
||||
|
||||
利用 RunLoop 可以实现这个效果,就是给下载并显示图片的方法指定 **NSRunLoopMode**。
|
||||
UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,FPS 达不到60。
|
||||
|
||||
```objectivec
|
||||
- (void)downloadAndShowImage{
|
||||
self.imageview.image = [UIImage imageNamed:@"test"];
|
||||
}
|
||||
|
||||
- (IBAction)clickLoadIMage:(id)sender {
|
||||
//[self.imageview performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"test"] afterDelay:2];
|
||||
[self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
|
||||
}
|
||||
|
||||
- (void)downloadAndShowImage{
|
||||
self.imageview.image = [UIImage imageNamed:@"test"];
|
||||
}
|
||||
```
|
||||
|
||||
知道 RunLoop 的工作原理,就清楚 UITableView(任何 UIScrollView 子类)在滚动的时候,RunLoop 会处于 `UITrackingRunLoopMode`,那么可以将图片下载或者解码显示的逻辑放到 `NSDefaultRunLoopMode` 中
|
||||
|
||||
```objective-c
|
||||
[self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 自动释放池
|
||||
|
||||
自动释放池什么时候创建和释放?
|
||||
@@ -1421,6 +1623,8 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
|
||||
|
||||
总结版:在主线程执行的代码,通常是写在事件回调、Timer 回调内的,这些回调都会被 RunLoop 自身状态相关的 AutoreleasePool 所包裹,所以会自动管理内存,开发者不需要手动创建 AutoreleasePool。
|
||||
|
||||
|
||||
|
||||
### 事件响应
|
||||
|
||||
系统注册了 Source1(基于 Mach port)用来接收系统事件,其回调函数为 `__IOHIDEventSystemClientQueueCallback`
|
||||
@@ -1429,18 +1633,24 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
|
||||
|
||||
`_UIApplicationHandleEventQueue` 会把 `IOHIDEvent` 处理并包装成 UIEvent 进行处理和分发(其中包括 UIGesture、屏幕旋转等)。
|
||||
|
||||
|
||||
|
||||
### 手势识别
|
||||
|
||||
`_UIApplicationHandleEventQueue` 识别到一个手势时,首先会调用 cancel 将当前的 touchBegin/End/Move 系统回调打断,然后系统会将对应的 `UIGestureRecognizer ` 标记为待处理。
|
||||
|
||||
苹果注册了一个 Observer 监控 RunLoop 的 `kCFRunLoopBeforeWaiting`(将要休眠)状态,回调为 `_UIGestureRecognizerUpdateObserver`,其内部会获取所有刚被标记为待处理的 UIGestureRecognizer,并执行对应的回调。
|
||||
|
||||
|
||||
|
||||
### UI 刷新
|
||||
|
||||
当界面的 Frame 改变,或者更改 UIView、CALayer 的层次时,或者调用了 UIView、CALayer 的 setNeedsLayout、setNeedsDisplay 方法后,这个 UIView、CALayer 会被标记为待处理(类比前端的 Virtual Dom Diff,标记为 dirty),并被提交到一个全局容器中。
|
||||
|
||||
苹果设计 UI 更新也是 RunLoop 的业务方,所以会注册一个 Obserger 监控 `kCFRunLoopBeforeWaiting`(将要休眠)和 `kCFRunLoopExit` (即将退出 RunLoop)状态,然后会执行 `_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()` 回调。内部会遍历所有待处理的 UIView、CALayer 以执行实际的绘制和渲染,更新 UI
|
||||
|
||||
|
||||
|
||||
### RunLoop 空闲时做一些任务
|
||||
|
||||
```objectivec
|
||||
@@ -1465,6 +1675,8 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Crash 防护
|
||||
|
||||
利用监控手段,比如 C/OC crash、Signal、Mach 异常,当监控到异常之后,正常来说会发生闪退等,体验较差。某些场景下希望 App 从异常中恢复,重新启动,这个可以利用 RunLoop 实现。
|
||||
@@ -1494,37 +1706,162 @@ CFRelease(allModes);
|
||||
|
||||
### 线程保活
|
||||
|
||||
为什么线程做完事情就会退出?
|
||||
|
||||
NSThread 的一个工作流程如下:
|
||||
|
||||
`start() -> 创建 pthread -> main() -> [target performSelector:selector] -> exit`
|
||||
|
||||
NSThread 需要保活。为什么会死掉?看看 gnu 源码
|
||||
|
||||
```c++
|
||||
- (void) start
|
||||
{
|
||||
pthread_attr_t attr;
|
||||
|
||||
if (_active == YES)
|
||||
{
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"[%@-%@] called on active thread",
|
||||
NSStringFromClass([self class]),
|
||||
NSStringFromSelector(_cmd)];
|
||||
}
|
||||
if (_cancelled == YES)
|
||||
{
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"[%@-%@] called on cancelled thread",
|
||||
NSStringFromClass([self class]),
|
||||
NSStringFromSelector(_cmd)];
|
||||
}
|
||||
if (_finished == YES)
|
||||
{
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"[%@-%@] called on finished thread",
|
||||
NSStringFromClass([self class]),
|
||||
NSStringFromSelector(_cmd)];
|
||||
}
|
||||
|
||||
/* Make sure the notification is posted BEFORE the new thread starts.
|
||||
*/
|
||||
gnustep_base_thread_callback();
|
||||
|
||||
/* The thread must persist until it finishes executing.
|
||||
*/
|
||||
RETAIN(self);
|
||||
|
||||
/* Mark the thread as active while it's running.
|
||||
*/
|
||||
_active = YES;
|
||||
|
||||
errno = 0;
|
||||
pthread_attr_init(&attr);
|
||||
/* Create this thread detached, because we never use the return state from
|
||||
* threads.
|
||||
*/
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
/* Set the stack size when the thread is created. Unlike the old setrlimit
|
||||
* code, this actually works.
|
||||
*/
|
||||
if (_stackSize > 0)
|
||||
{
|
||||
pthread_attr_setstacksize(&attr, _stackSize);
|
||||
}
|
||||
// 设置回调函数
|
||||
if (pthread_create(&pthreadID, &attr, nsthreadLauncher, self))
|
||||
{
|
||||
DESTROY(self);
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"Unable to detach thread (last error %@)",
|
||||
[NSError _last]];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
看看 pthread 创建后的回调函数
|
||||
|
||||
```c++
|
||||
static void *
|
||||
nsthreadLauncher(void *thread)
|
||||
{
|
||||
NSThread *t = (NSThread*)thread;
|
||||
|
||||
setThreadForCurrentThread(t);
|
||||
|
||||
/*
|
||||
* Let observers know a new thread is starting.
|
||||
*/
|
||||
if (nc == nil)
|
||||
{
|
||||
nc = RETAIN([NSNotificationCenter defaultCenter]);
|
||||
}
|
||||
// 发送通知
|
||||
[nc postNotificationName: NSThreadDidStartNotification
|
||||
object: t
|
||||
userInfo: nil];
|
||||
// 设置线程名
|
||||
[t _setName: [t name]];
|
||||
// 调用 main 方法
|
||||
[t main];
|
||||
// 线程退出
|
||||
[NSThread exit];
|
||||
// Not reached
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
看了源码,会发现 NSThread 调用 start 内部就会调用 `[NSThread exit]` 所以会退出。要想常驻,就需要在 main 方法做 runloop 保活。
|
||||
|
||||
```c++
|
||||
- (void) main
|
||||
{
|
||||
if (_active == NO)
|
||||
{
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"[%@-%@] called on inactive thread",
|
||||
NSStringFromClass([self class]),
|
||||
NSStringFromSelector(_cmd)];
|
||||
}
|
||||
|
||||
[_target performSelector: _selector withObject: _arg];
|
||||
}
|
||||
```
|
||||
|
||||
main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始化方法中,传入的 selector 中进行 runloop 保活逻辑。
|
||||
|
||||
|
||||
|
||||
应用场景:经常在子线程中处理某些逻辑的场景。如果销毁再创建再销毁再创建效率很低,这个情况下就需要线程保活。
|
||||
|
||||
```objectivec
|
||||
@interface LBPThread : NSThread
|
||||
```objective-c
|
||||
@interface LifeThread : NSThread
|
||||
@end
|
||||
@implementation LBPThread
|
||||
@implementation LifeThread
|
||||
- (void)dealloc{
|
||||
NSLog(@"%s", __func__);
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ViewController ()
|
||||
@property (nonatomic, strong) LBPThread *task;
|
||||
@property (nonatomic, strong) LifeThread *thread;
|
||||
@end
|
||||
|
||||
@implementation ViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.task = [[LBPThread alloc] initWithTarget:self selector:@selector(run) object:nil];
|
||||
[self.task start];
|
||||
self.thread = [[LifeThread alloc] initWithTarget:self selector:@selector(run) object:nil];
|
||||
[self.thread start];
|
||||
}
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
[self performSelector:@selector(test) onThread:self.task withObject:nil waitUntilDone:NO];
|
||||
}
|
||||
- (void)test{
|
||||
NSLog(@"沿用保活的线程,做事情:%@", [NSThread currentThread]);
|
||||
NSLog(@"沿用保活的线程,处理任务:%@", [NSThread currentThread]);
|
||||
}
|
||||
// 该方法仅用于线程保活
|
||||
- (void)run{
|
||||
NSLog(@"%s %@", __func__, [NSThread currentThread]);
|
||||
// 给 RunLoop 的某个 Mode 里添加 Source/Timer/Observer
|
||||
// 给 RunLoop 的某个 Mode 里添加 Source/Timer/Observer。其中 addPort 就是 Source1
|
||||
[[NSRunLoop currentRunLoop] addPort:[[NSMachPort alloc] init] forMode:NSDefaultRunLoopMode];
|
||||
[[NSRunLoop currentRunLoop] run];
|
||||
NSLog(@"task finished");
|
||||
@@ -1534,7 +1871,7 @@ CFRelease(allModes);
|
||||
|
||||
默认创建的 NSThread 会在 NSDefaultRunLoopMode 模式下运行,当 UI 滑动则进入 UITrackingMode 模式,所以 NSThread 的方法会停止。
|
||||
|
||||
线程保活就是给方法内部添加 RunLoop,但是新创建的 RunLoop 运行肯定是基于某个 Mode,RunLoop 持续运行(保活)的前提是必须有 Timer、Sources、Observer。所以添加了 NSMachPort。
|
||||
线程保活就是给方法内部添加 RunLoop,但是新创建的 RunLoop 运行肯定是基于某个 Mode,RunLoop 持续运行(保活)的前提是必须有 Timer、Sources、Observer。所以在 Demo 中我们添加了 `NSMachPort`。
|
||||
|
||||
上面的代码存在问题:
|
||||
|
||||
@@ -1542,67 +1879,64 @@ CFRelease(allModes);
|
||||
|
||||
2. LBPThread 线程不会死亡,假如我们需要在某个时机让保活线程销毁,现在是办不到的
|
||||
|
||||
3. RunLoop 不会停止
|
||||
|
||||
改进:
|
||||
|
||||
1. Thread 换种 api `-(instancetype)initWithBlock:(void (^)(void))block`,线程不持有 self
|
||||
|
||||
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop
|
||||
|
||||

|
||||
<img src="./../assets/RunLoop-RunIssue.png" style="zoom:45%" />
|
||||
|
||||
改进代码如下
|
||||
|
||||
```objectivec
|
||||
@interface ViewController ()
|
||||
@property (strong, nonatomic) LBPThread *task;
|
||||
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
|
||||
@end
|
||||
```objective-c
|
||||
__weak ViewController *weakself = self;
|
||||
self.thread = [[LifeThread alloc] initWithBlock:^{
|
||||
NSLog(@"RunLoop Start");
|
||||
[[NSRunLoop currentRunLoop] addPort:[[NSMachPort alloc] init] forMode:NSDefaultRunLoopMode];
|
||||
while (weakself && !weakself.needStopThread) {
|
||||
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
|
||||
}
|
||||
NSLog(@"RunLoop Stop");
|
||||
}];
|
||||
[self.thread start];
|
||||
|
||||
@implementation ViewController
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.stopped = NO;
|
||||
self.task = [[LBPThread alloc] initWithBlock:^{
|
||||
NSLog(@"%@----begin----", [NSThread currentThread]);
|
||||
// 往RunLoop里面添加Source\Timer\Observer
|
||||
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
|
||||
while (weakSelf && !weakSelf.isStoped) {
|
||||
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
|
||||
}
|
||||
NSLog(@"%@----end----", [NSThread currentThread]);
|
||||
}];
|
||||
[self.task start];
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
if(!self.thread) return;
|
||||
[self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:YES];
|
||||
}
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
||||
{
|
||||
if (!self.task) return;
|
||||
[self performSelector:@selector(test) onThread:self.task withObject:nil waitUntilDone:NO];
|
||||
#pragma mark - 线程相关
|
||||
- (void)threadTask {
|
||||
NSLog(@"线程任务 %@", [NSThread currentThread]);
|
||||
}
|
||||
// 子线程需要执行的任务
|
||||
- (void)test {
|
||||
NSLog(@"沿用保活的线程,做事情:%@", [NSThread currentThread]);
|
||||
|
||||
- (void)stopThread {
|
||||
if(!self.thread) return;
|
||||
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
|
||||
}
|
||||
- (IBAction)stop {
|
||||
if (!self.task) return;
|
||||
// 在子线程调用stop
|
||||
[self performSelector:@selector(stopThread) onThread:self.task withObject:nil waitUntilDone:YES];
|
||||
}
|
||||
// 用于停止子线程的RunLoop
|
||||
- (void)stopThread{
|
||||
// 设置标记为NO
|
||||
self.stopped = YES;
|
||||
// 停止RunLoop
|
||||
|
||||
- (void)stop {
|
||||
self.needStopThread = YES;
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
NSLog(@"%s %@", __func__, [NSThread currentThread]);
|
||||
self.task = nil;
|
||||
self.thread = nil;
|
||||
}
|
||||
- (void)dealloc{
|
||||
|
||||
- (void)dealloc {
|
||||
NSLog(@"%s", __func__);
|
||||
[self stopThread];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
效果如下:
|
||||
|
||||
<img src="./../assets/RunLoopThreadKeepLive.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
注意:
|
||||
|
||||
- 如果 `stop` 方法内部的 `waitUntilDone` 为 NO,则会出现 Crash。因为该参数代表后续代表会不会等该 selector 执行完毕。因为为 NO,所以 ViewController 执行 dealloc 了,所以 `dealloc` 方法和 Thread 内部的 block 同时进行,不能确保在 block 内部执行的时候 dealloc 有没有执行完,访问 weakSelf.isStoped 可能会 crash
|
||||
@@ -1629,16 +1963,9 @@ CFRelease(allModes);
|
||||
}
|
||||
```
|
||||
|
||||
线程保活后如何暂停?
|
||||
线程保活的目的是保证线程处于激活状态,而不是使用强指针让线程不要释放。为让其处于激活状态就需要使用 RunLoop。
|
||||
|
||||
```
|
||||
[thread cancel];
|
||||
thread = nil;
|
||||
// 指针 nil,还是被 RunLoop 持有。
|
||||
// 也不行。CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
|
||||
```
|
||||
|
||||
主线程为什么
|
||||
|
||||
### 线程封装
|
||||
|
||||
@@ -1751,3 +2078,124 @@ typedef void (^LBPPermenantThreadTask)(void);
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 卡顿监控
|
||||
|
||||
RunLoop 监控卡顿,可以查看 [带你打造一套 APM 系统](./1.4.md) 文章
|
||||
|
||||
|
||||
|
||||
### AsyncDisplayKit
|
||||
|
||||
卡顿主要原因是 CPU/GPU 高负荷工作(mask/cornerRadius/drawrect/opaque 带来的 offscreen rendering/blending 等),或者任务在时间分配下不均衡。
|
||||
|
||||
Autolayout 布局性能瓶颈。约束的计算会随着 View 数量和层级的增长呈指数级增长,且必须在主线程执行。
|
||||
|
||||
并行效率低。大多数情况下,主线程繁忙,其他子线程空余。所以思路是把主线程的任务转移一部分给其他线程进行异步处理,主线程带来性能提升
|
||||
|
||||
AsyncDisplayKit 主要针对:
|
||||
|
||||
- 渲染:对于大量文字、图片混合在一起时,而文字区域的大小和布局,恰恰依赖着渲染结果。ASDK 尽可能走后台线程进行渲染,完成后再同步回到主线程相应的 UIView
|
||||
- 布局。ASDK 抛弃了 Autolayout,实现了自己的布局和缓存
|
||||
- 系统对象的创建和销毁。UIKit 封装了 CALayer 以支持出没灯显示以外的操作。耗时也增加了,这些操作也需要在主线程进行。ASDK 基于 Node 的设计,突破了 UIKit 线程的限制。
|
||||
|
||||
|
||||
|
||||
ASDK 创建了 ASDisplayNode 对象,内部封装了 UIView/CALayer,具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor,这些属性都可以在子线程更改,这样可以实现将排版和绘制放到后台线程,但最终都需要将 View 的更改同步到主线程的 UIView/CALayer 中去。
|
||||
|
||||
这个同步时机,就是利用 RunLoop 实现的。系统的 UIKit/QuartzCore 也是 RunLoop 的业务方,同样,我们可以模仿系统行为,将针对 View 的改动,在主线程 RunLoop 添加一个 Observer,监听 `kCFRunLoopBeforeWaiting` 、`kCFRunLoopExit` 状态,当收到回调时,遍历所有之前加入到队列中待处理的事务,然后一一执行。
|
||||
|
||||
```objective-c
|
||||
+ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup
|
||||
{
|
||||
ASDisplayNodeAssertMainThread();
|
||||
static CFRunLoopObserverRef observer;
|
||||
ASDisplayNodeAssert(observer == NULL, @"A _ASAsyncTransactionGroup should not be registered on the main runloop twice");
|
||||
// defer the commit of the transaction so we can add more during the current runloop iteration
|
||||
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
|
||||
CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping
|
||||
kCFRunLoopExit); // before exiting a runloop run
|
||||
CFRunLoopObserverContext context = {
|
||||
0, // version
|
||||
(__bridge void *)transactionGroup, // info
|
||||
&CFRetain, // retain
|
||||
&CFRelease, // release
|
||||
NULL // copyDescription
|
||||
};
|
||||
|
||||
observer = CFRunLoopObserverCreate(NULL, // allocator
|
||||
activities, // activities
|
||||
YES, // repeats
|
||||
INT_MAX, // order after CA transaction commits
|
||||
&_transactionGroupRunLoopObserverCallback, // callback
|
||||
&context); // context
|
||||
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
|
||||
CFRelease(observer);
|
||||
}
|
||||
|
||||
static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
|
||||
{
|
||||
ASDisplayNodeCAssertMainThread();
|
||||
_ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info;
|
||||
[group commit];
|
||||
}
|
||||
|
||||
- (void)commit
|
||||
{
|
||||
ASDisplayNodeAssertMainThread();
|
||||
|
||||
if ([_containers count]) {
|
||||
NSHashTable *containersToCommit = _containers;
|
||||
_containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
|
||||
|
||||
for (id<ASAsyncTransactionContainer> container in containersToCommit) {
|
||||
// Note that the act of committing a transaction may open a new transaction,
|
||||
// so we must nil out the transaction we're committing first.
|
||||
_ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction;
|
||||
container.asyncdisplaykit_currentAsyncTransaction = nil;
|
||||
[transaction commit];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user