docs: 内容

This commit is contained in:
杭城小刘
2020-11-08 15:51:47 +08:00
parent a744b950b0
commit e89fe0ca1c
27 changed files with 11290 additions and 10341 deletions

View File

@@ -200,7 +200,7 @@
**__CFRunLoopModeIsEmpty**
此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
```objective-c
@@ -768,3 +768,341 @@
}
```
![image-20200812221231993](/Users/lbp/Desktop/resume/2020-08-12-RunLoopStructure1.png)
- performSelector 是在 source0 上实现的
- RunLoopTimer 外部接口设置的精度精度大于0则使用 dispatch_source_set_timer精度小于0则使用 mk_timer_arm
- timer_source 使用 dispatch_timer_STRICT 创建,则系统会尽最大努力遵守设置的 leeway 值
- NSTimer 不准的原因:底层 RunLoop Timer 底层使用的 timer 的精度不高mk_timer与 RunLoop 底层的调用机制有关系
- 那么为什么存在 RunLoopTimer意义是什么应用场景
![image-20200812125534225](/Users/lbp/Desktop/Github/knowledge-kit/assets/2020-08-12-RunLoopStructure.png)
```c++
// Data structure to hold TSD data, cleanup functions for each
typedef struct __CFTSDTable {
uint32_t destructorCount;
uintptr_t data[CF_TSD_MAX_SLOTS];
tsdDestructor destructors[CF_TSD_MAX_SLOTS];
} __CFTSDTable;
```
```c++
// 主线程 RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
// 局部静态变量
static CFRunLoopRef __main = NULL; // no retain needed
// 创建主线程对应的 RunLoop
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
// 子线程 RunLoop
// 先从 __CFTSDTable 获取 RunLoop如果有则 return没有则调用 _CFRunLoopGet0_CFRunLoopGet0 内部调用 __CFRunLoopCreate 创建 CFRunLoop然后写入 __CFTSDTable
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
// __CFTSDTable
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
// 没有则创建
return _CFRunLoopGet0(pthread_self());
}
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(_CFThreadRef t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
}
CFRunLoopRef newLoop = NULL;
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
newLoop = __CFRunLoopCreate(t);
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFUnlock(&loopsLock);
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
if (newLoop) { CFRelease(newLoop); }
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
#if _POSIX_THREADS
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
#else
_CFSetTSD(__CFTSDKeyRunLoopCntr, 0, &__CFFinalizeRunLoop);
#endif
}
}
return loop;
}
```
- 为什么只有当 RunLoop 中存在 Timer Sourcrs、Input Sources 时,才能保证 RunLoop 不退出?
RunLoop 本质就是一个有条件的 do...while 循环。__CFRunLoopModeIsEmpty 里面去判断 source0、source1、timers 不存在则 while 循环条件不满足RunLoop 退出
```c++
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
// expects rl and rlm locked
static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) {
// ...
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
// ...
return true;
}
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// ...
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
// ...
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// ...
return result;
}
```
## 线程保活技术:
```objective-c
- (void)viewDidLoad {
[super viewDidLoad];
[self testKeepAliveThread];
}
- (void)testKeepAliveThread
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.thread = (ThreadKeepLive *)[[NSThread alloc] initWithTarget:self selector:@selector(threadTask) object:nil];
[self.thread setName:@"LongLiveThread"];
[self.thread setQualityOfService:[NSThread mainThread].qualityOfService];
[self.thread start];
});
}
- (void)threadTask
{
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"come");
}];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (true) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
```
- come 会打印几次1次因为 RunLoop 跑起来之后,后面的代码不会执行,内部不断的 do...while
- NSRunLoop 常驻线程可以运行在 NSRunLoopCommonModes 下吗?
不可以。因为在跑的时候如果 modeName 等于 kCFRunLoopCommonModes 则直接 kCFRunLoopRunFinished则 RunLoop 的 while 循环条件失败
```objective-c
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CFLog(kCFLogLevelError, CFSTR("invalid mode '%@' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution."), modeName);
_CFRunLoopError_RunCalledWithInvalidMode();
});
return kCFRunLoopRunFinished;
}
// ...
return result;
}
```
### Mach Port 跨线程通信
1. Mach IPC 基于 Mach 内核实现进程间通讯。
2. Mach IPC 被抽象为3种操作messages、ports、and port sets
3. Mach port 跨线程通信
线程 B 有个 port 在等待消息,具有消息接收权限,等待消息到来的时候会阻塞当前线程
线程 A 有个 port 在发送消息具有发送消息权限要把发送的消息包装成消息通过消息队列传递message 包括header目的地 port、size、data。
线程 B 收到消息后,解除 block线程继续向下运行
![image-20200813024843611](/Users/lbp/Library/Application Support/typora-user-images/image-20200813024843611.png)
4. Mach port 如何进行跨线程通信?
线程开启一个 port然后给 port 申请接收、发送的权限。mach_msg 是通信函数,在等待消息的时候不加 timeout 则会一直阻塞,等到消息到来
![image-20200813093820911](/Users/lbp/Desktop/Github/knowledge-kit/assets/2020-08-13-MachPort.png)
5. 在测试工作中 main.m 文件中打印当前的 RunLoop
```objective-c
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
NSLog(@"%@", NSRunLoop.currentRunLoop);
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
//
2020-08-13 09:49:41.326621+0800 ***[52423:2383402] <CFRunLoop 0x6000015f0000 [0x7fff8062d750]>{wakeup port = 0x1103, stopped = false, ignoreWakeUps = true,
current mode = (none),
common modes = <CFBasicHash 0x6000027f1320 [0x7fff8062d750]>{type = mutable set, count = 1,
entries =>
2 : <CFString 0x7fff80640a20 [0x7fff8062d750]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = (null),
modes = <CFBasicHash 0x6000027f1290 [0x7fff8062d750]>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopMode 0x6000012fc0d0 [0x7fff8062d750]>{name = kCFRunLoopDefaultMode, port set = 0x1003, queue = 0x6000007f0100, source = 0x6000007f0280 (not fired), timer port = 0xe03,
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
currently 618976181 (257342842892563) / soft deadline in: 1.84464867e+10 sec (@ -1) / hard deadline in: 1.84464867e+10 sec (@ -1)
},
}
}
```
![Mach Port Test Demo](./../assets/2020-08-13-MachPortTest.png)
可以看到 main.m 中,还没有 return 的时候当前 RunLoop 的内部结构中存在一个 **wakeup port** 的端口。查看 RunLoop 源代码 **wakeup port** 就是 mach port 的一种。
```c++
typedef mach_port_t __CFPort;
struct __CFRunLoop {
CFRuntimeBase _base;
_CFRecursiveMutex _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
_CFThreadRef _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
_Atomic(uint8_t) _fromTSD;
CFLock_t _timerTSRLock;
};
```
```c++
void CFRunLoopWakeUp(CFRunLoopRef rl) {
// ...
kern_return_t ret;
/* We unconditionally try to send the message, since we don't want
* to lose a wakeup, but the send may fail if there is already a
* wakeup pending, since the queue length is 1. */
ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
// ...
int ret;
do {
ret = eventfd_write(rl->_wakeUpPort, 1);
} while (ret == -1 && errno == EINTR);
// ...
SetEvent(rl->_wakeUpPort);
// ...
}
```
`CFRunLoopWakeUp(CFRunLoopGetCurrent());` 可以唤醒 RunLoop函数的底层实现如上。核心实现就是 `__CFSendTrivialMachMessage` 函数。在iOS 中,除了 source1 可以自己唤醒 RunLoop 之外,其他的事件都需要用户手动唤醒 RunLoop 才可以。RunLoop 提供了专门的方法来实现这个功能。其核心部分就是调用 mach_msg 来向指定的 **_wakeUpPort** 端口发送消息,从而唤醒线程继续工作。
为什么 Source1 可以唤醒 RunLoop因为 Source1 本质上就是针对 Mach Port 的封装
6. 做个实验检测 _wakeUpPort 端口
```objective-c
- (void)viewDidLoad {
[super viewDidLoad];
[self listenWakeUpPort];
}
- (void)listenWakeUpPort
{
NSArray<NSString *> *array = [NSRunLoop.currentRunLoop.description componentsSeparatedByString:@"wakeup port = "];
NSString *wakeupPort = [array.lastObject substringToIndex:[array.lastObject rangeOfString:@","].location];
dispatch_queue_t queue = dispatch_queue_create("com.test.wake_up_port_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, [self numberWithHexString:wakeupPort], 0, queue);
dispatch_source_set_event_handler(source, ^{
mach_port_t port = (mach_port_t)dispatch_source_get_handle(source);
NSLog(@"%u--wakeUp", port);
});
dispatch_activate(source);
}
- (NSInteger)numberWithHexString:(NSString *)hexString
{
const char *hexChar = [hexString cStringUsingEncoding:NSUTF8StringEncoding];
int hexNumber;
sscanf(hexChar, "%x", &hexNumber);
return (NSInteger)hexNumber;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}
```
可以看到每次在点击屏幕时调用 `CFRunLoopWakeUp` 尝试唤醒 RunLoop然后监听 RunLoop 的 _wakeUpPort都可以在回调中获取到消息。
7. RunLoop lifecycle
![RunLoop lifecycle](./../assets/2020-08-13-RunLoopLifeCycle.png)
休眠时 RunLoop 被 mach_msg 阻塞,等到消息到来,可以是手动给 RunLoop 的 wakeUpPort 发消息 mach_msg或者是 timer、Source1。然后继续走到 RunLoop run不断循环
Timer 不准确的原因1. RunLoop 底层对 Timer 实现的精度不高,所以需要指定 Timer 的精度2. RunLoop 在 DoSource0、DoSource1 的时候耗时较多,则导致 Timer 下次执行会不准确