将Runloop的理解都写到这里

项目代码

runloopDemophp

CFCoreFoundation源码git

目录

  • 从主线程runloop啥时候开启
  • runloop对象是怎么存储的
  • runloop怎么跑起来的,又是怎么退出的
  • Runloop do-while作了什么
  • 监听Runloop的状态
  • 常驻线程以及怎么销毁常驻线程
  • runloopperformSelector
  • 网络请求主线程回调,实现同步
  • runloop优化tableview滚动坑点
  • runloop卡顿监测
  • runloopautoreleasepool
  • 界面更新

从主线程runloop啥时候开启

appmain函数中的UIApplicationMain走进去,就一直在里面循环了,NSLog(@"会走吗");是不会被调用的github

这里我就有一个疑惑:那为啥这个main还要return int类型呢?既然都死循环,return不了缓存

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    int i = UIApplicationMain(argc, argv, nil, appDelegateClassName);
    
    NSLog(@"会走吗");
    return i;
}

复制代码

进入UIApplicationMain后,就会接着调用application:didFinishLaunchingWithOptions:方法,在这个方法里就开启runloop,经过监听runloop状态,在***即将进入runloop***回调打上断点,看堆栈便可得知性能优化


runloop对象是怎么存储的

runloop跑起来,得先获取runloop对象,咱们从CFRunloop.c的源码中,找到CFRunLoopGetCurrentbash

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}
复制代码

从这两个方法,获取Runloop的入参是线程对象,能够断定,线程与runloop一一对应的关系,具体,咱们再看下_CFRunLoopGet0的实现markdown

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    // 若是参数为空,那么就默认是主线程
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    
    __CFLock(&loopsLock);
    
    // static CFMutableDictionaryRef __CFRunLoops = NULL;
    // 存放Runloop对象的字典
    // 先判断这个Runloop字典存在不,不存在就建立一个,并加主线程Runloop加进入
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        
        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);
        
        __CFLock(&loopsLock);
    }
    
    // 根据线程去这个字段取Runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    __CFUnlock(&loopsLock);
    
    // 若是不存在,就建立一个Runloop,并加到字典中
    if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFUnlock(&loopsLock); CFRelease(newLoop); } if (pthread_equal(t, pthread_self())) { _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; } 复制代码

大概的代码逻辑就是网络

实现思路
1.先判断这个全局字典存不存在,不存在,建立一个,并将主线程的runloop加进去
2.直接去字典里取这个loop
3.若是loop不存在,就建立一个loop加入到全局字典中
// 伪代码
if(!__CFRunLoops) {
      1.建立全局字典
      2.建立主线程loop,并加入到全局字典中
}
根据线程pthread_t为key,去字典取对应的loop
if(!loop) {
      1.建立loop
      2.加入到字典中
}
return loop
复制代码

因此:app

  • runloop对象和线程是一一对应的关系
  • runloop对象是储存在一个全局字典中的,这个全局字段的key是线程对象,valuerunloop对象

runloop怎么跑起来的,又是怎么退出的

先说下runloop跑一圈是作了什么事情框架

首先runloop有六个状态变化

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};

复制代码

因此,当启动runloop的时候,就是监听输入源(端口port、source0、source1)、定时器、若是有事件,处理事件,没有就休眠

可是实际上并非这样的,而是一直在重复的进入runloop(使用run方法启动runloop的状况)

咱们先从开启runloop的函数入手,从CFRunLoopRun函数,咱们看到了runloop确实是一个do-while操做,那么里面的CFRunLoopRunSpecific每走一次,就算runloop迭代一次,那么这个runloop迭代一次后,会退出runloop,退出runloop后,由于CFRunLoopRun函数有do-while操做,因此又会从新进入runloop

void CFRunLoopRun(void) {	/* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

复制代码

CFRunLoopRunSpecific中作了一些前置判断,好比判断当前Mode为空,直接return,这个也能够说明一点***启动runloop以前,runloop中必定要有输入源或者定时器***

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    ...
    
    //  前置判断,好比判断当前`Mode`为空,直接`return`
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    
    
   ...
   
    // 回调即将进入runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 进入runloop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 即将退出runloop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    ...
    
    return result;
}
复制代码

接下来再看下__CFRunLoopRun函数

// 简化代码,详细直接搜源码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
	do {
		// 监听source、timer
		if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
       // 处理source0
       Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
       
       // 即将进入休眠
       if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

		...
		
		 // 退出runloop的条件
		 if (sourceHandledThisLoop && stopAfterHandle) {
		 	  // 处理完sourcesourceHandledThisLoop会为YES
		 	  // stopAfterHandle,若是是CFRunloop调用的话,是为NO
		 	  // 能够回头看下CFRunLoopRun函数
		 	  // 
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            // 自身超时时间到了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            // 被外部调用CFRunloop中止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            // 被 _CFRunLoopStopMode 中止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // 检查上一个 mode 有没有执行完全部事件源
            retVal = kCFRunLoopRunFinished;
        }
       
	} while(0 = retVal);
}

复制代码

退出runloop有四个条件

  • 入参stopAfterHandle为YES的时候,那么处理完source就会退出runloop
  • 自身超时时间到了
  • 被外部调用CFRunloop中止
  • _CFRunLoopStopMode 中止

CFRunLoopRun指定stopAfterHandleNO,说明使用run方法开启runloop,处理完source后不会退出runloop

若是是使用CFRunLoopRunInMode则能够指定是否须要处理完source后就退出runloop


Runloop do-while作了什么

do-while的过程当中,作了如下操做

  • 监听source(source1是基于port的线程通讯(触摸/锁屏/摇晃等),source0是不基于port的,包括:UIEvent、performSelector),监听到就处理
  • 监听timer的事件,监听到就处理
  • 没有source和timer的时候,就休眠,休眠不是不监听,仍是保持监听的,只是当有事件的时候,才唤醒,继续处理

当咱们触发了事件(触摸/锁屏/摇晃等)后,由IOKit.framework生成一个 IOHIDEvent事件,而IOKit是苹果的硬件驱动框架,由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件,并专门处理用户交互设备,由IOHIDServices和IOHIDDisplays两部分组成,其中IOHIDServices是专门处理用户交互的,它会将事件封装成IOHIDEvents对象,接着用mach port转发给须要的App进程,随后 Source1就会接收IOHIDEvent,以后再回调__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()内触发Source0,Source0 再触发 _UIApplicationHandleEventQueue()。因此触摸事件看到是在 Source0 内的。

总结:触摸事件先经过 mach port 发送,封装为 source1,以后又转换为 source0

1.一个runloop对应一个线程,多个mode,一个mode下对应多个sourceobservertimer

struct __CFRunLoop {
    pthread_t _pthread; // 线程对象
    CFMutableSetRef _commonModes; // 
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
    // 简化
};

struct __CFRunLoopMode {
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ...
    // 简化
};

复制代码

2.常见有五种mode

  • kCFRunLoopDefaultMode: App的默认 Mode,一般主线程是在这个 Mode 下运行的。
  • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其余 Mode 影响。
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就再也不使用。
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,一般用不到。
  • kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际做用。

除了以上5个mode,还有其余mode,可是不多碰见这里

4.子线程不自动开启runloop,手动开启runloop前,必须得有输入源和定时器(输入源就是经过监听端口,能够获取不一样的事件),经过CFRunloop源码中的CFRunLoopRunSpecific函数,其中判断了当modenull或者modeItem为空,直接return

监听Runloop的状态

runloop有六大状态

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};
复制代码

能够经过这就代码监听这六个状态

CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
复制代码

其中的参数分别为

CFRunLoopObserverCreate参数

1.不懂

2.监听runloop变化状态

3.是否重复监听

4.不懂,传0

5.回调的函数指针(须要本身写一个函数)

6.CFRunLoopObserverContext对象

定义函数指针

static void runLoopOserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
   //void *info正是咱们要用来与OC传值的,这边能够转成OC对象,前面咱们传进来的时候是self
    RunloopObserver *target = (__bridge RunloopObserver *)(info);//void *info便是咱们前面传递的self(ViewController)
    
    if (target.callback) {
        target.callback(observer, activity);
    }
}
复制代码

定义CFRunLoopObserverContext对象,其实这个参数是用于通讯的

// 从CFRunLoopObserverRef点进去找
    
    typedef struct {
        CFIndex    version; // 传0,不知道是什么
        void *    info; // 数据传递用的,void *,指就是能够接受全部指针
        const void *(*retain)(const void *info); // 引用
        void    (*release)(const void *info); // 回收
        CFStringRef    (*copyDescription)(const void *info); // 描述,没用到
    } CFRunLoopObserverContext;
复制代码

建立监听

//建立一个监听
static CFRunLoopObserverRef observer;
    
// CFRunLoopObserverCreate参数。1.不懂  2.监听runloop变化状态  3.是否重复监听  4.不懂,传0 5.函数指针  6.CFRunLoopObserverContext对象
observer = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, &runLoopOserverCallBack, &context);
    
//注册监听
CFRunLoopAddObserver(runLoopRef, observer, kCFRunLoopCommonModes);
    
//销毁
CFRelease(observer);
    
复制代码

常驻线程以及怎么销毁常驻线程

先说下performSelector和子线程的,perform...AfterDelayperform..onThread都须要在开启runloop的线程执行

由于其实现原理,都是往runloop添加一个不重复的定时器

- (void)test1
{
    [self.myThread setName:@"StopRunloopThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    
    // performSelector:afterDelay:的原理是往runloop添加不重复执行的定时器
    [self performSelector:@selector(performSelAferDelayClick) withObject:nil afterDelay:1];
    
    [self.myRunloop run];
    
    NSLog(@"我会走吗");
}
复制代码

若是将开启runloop的代码,写到perform前,那么会开启不成功,由于开启runloop须要有输入源或者定时器的状况才能够开启

获取runloop会调用CFRunLoopRunSpecific函数(具体搜下CFRunloop.c

从这个函数中找到如下代码,当currentMode为空的时候,(也就没有输入源或者定时器),直接return kCFRunLoopRunFinished,开启失败

if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
	Boolean did = false;
	if (currentMode) __CFRunLoopModeUnlock(currentMode);
	__CFRunLoopUnlock(rl);
	return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
复制代码

如下代码实现了一个常驻线程

原理就是往当前线程的runloop中添加一个端口,让其监听这个端口(理解为监听某个端口的输入源,好比系统内核端口,监听一些系统事件),由于能够一直监听这个端口,那么runloop就不会退出

其实就是保持runloop不退出,就达到常驻线程的效果了,那么要让runloop不退出,就得有输入源或者重复的定时器让其监听

- (void)test2
{
    [self.myThread setName:@"StopRunloopThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    [self.myRunloop run];
    
    // 由于run以后,这个线程就一直在作do-while操做
    // 至关上面的代码被do-while包围了,那么如下代码不会走
    NSLog(@"我会走吗");
}
复制代码

当开启一个线程,就会对应建立一个runloop对象吗?

不是的,调用获取当前runloop的方法,内部实现:若是当前runloop不存在就建立一个,存在就返回当前runloop

因此走这句代码self.myRunloop = [NSRunLoop currentRunLoop];就生成当前线程对应的runloop


怎么销毁常驻线程?

1.要销毁常驻线程,首先得先把runloop退出?

当没有输入源或者定时器能够监听的时候,退出runloop

若是咱们调用[NSThread exit];,这时候线程是销毁了,可是线程中的代码仍是不会执行,好比NSLog(@"我会走吗");,

说明这时候的runloop并无退出,那么这样会致使一些问题,例如如下代码

- (void)test2
{
    [self.myThread setName:@"TestThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    // 添加监听NSMachPort的端口(这个端口能够理解为输入源,由于能够一直监听这个,因此这时候的runloop不会退出,会一直在作do-while)
    [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    [self.myRunloop run];
    
    // [self.myRunloop run]; 会致使如下代码无法走,由于runloop就是一个do-while的循环,do-while监听源,处理源
    [self.testPtr release];
}
复制代码

由于runloop没有退出,[self.testPtr release];不会执行,那么就会致使testPtr对象无法释放

2.怎么退出runloop呢

若是常驻线程是经过监听端口实现的,那么就调用[self.myRunloop removePort:self.myPort forMode:NSDefaultRunLoopMode];,移除端口,就能够销毁了

其实这时候还不必定能成功销毁,由于可能系统加入了一些其余源的监听

若是是经过添加剧复定时器,实现常驻线程(这种方式不可取,由于比添加监听端口耗性能,须要一次又一次的唤醒runloop)

- (void)test11
{
    [self.myThread setName:@"TestThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    // 首先,须要有一点,当runloop监视输入源或者定时器的时候,才不会退出
    // 开启runloop以前,须要有输入源或者定时器
    // 定时器(若是是添加定时器,不重复,那么监听一次就退出了)
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer执行");
    }];
    
    [self.myRunloop addTimer:timer forMode:NSDefaultRunLoopMode];
    
    
    
    [self.myRunloop run];
    
    // [self.myRunloop run]; 会致使如下代码无法走,由于runloop就是一个do-while的循环,do-while监听源,处理源
    NSLog(@"我会走吗");
    
}
复制代码

若是NSTimerrepeatsNO,那么执行一次timer的事件后,就会退出runloop

以上,若是经过移除端口,结束timer,反正以移除已知的输入源或者定时器来退出runloop都是不太靠谱的,由于系统内部有可能会在当前线程的runloop中添加一些输入源,也就是还有未知的输入源,咱们没有移除

3.使用CFRunLoopStop退出Runloop

- (void)test3
{
    [self.myThread setName:@"TestThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    // 添加监听NSMachPort的端口(这个端口能够理解为输入源,由于能够一直监听这个,因此这时候的runloop不会退出,会一直在作do-while)
    [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
    [self performSelector:@selector(runloopStop) withObject:nil afterDelay:1];

    [self.myRunloop run];
    
    // [self.myRunloop run]; 会致使如下代码无法走,由于runloop就是一个do-while的循环,do-while监听源,处理源
    NSLog(@"我会走吗");
}

- (void)runloopStop
{
    NSLog(@"执行stop");
    CFRunLoopStop(self.myRunloop.getCFRunLoop);
}
复制代码

输出:

2020-05-03 20:10:12.614130+0800 Runloop[60465:2827474] 即将进入Loop,
2020-05-03 20:10:12.614465+0800 Runloop[60465:2827474] 即将处理 Timer,
2020-05-03 20:10:12.615214+0800 Runloop[60465:2827474] 即将处理 Source,
2020-05-03 20:10:12.615634+0800 Runloop[60465:2827474] 即将进入休眠,
2020-05-03 20:10:13.615638+0800 Runloop[60465:2827474] 刚从休眠中唤醒,
2020-05-03 20:10:13.616005+0800 Runloop[60465:2827474] 执行stop
2020-05-03 20:10:13.616194+0800 Runloop[60465:2827474] 即将退出Loop,
2020-05-03 20:10:13.616360+0800 Runloop[60465:2827474] 即将进入Loop,
2020-05-03 20:10:13.616511+0800 Runloop[60465:2827474] 即将处理 Timer,
2020-05-03 20:10:13.616648+0800 Runloop[60465:2827474] 即将处理 Source,
2020-05-03 20:10:13.616765+0800 Runloop[60465:2827474] 即将进入休眠,

复制代码

确实是退出了runloop,可是又立刻进入了

缘由是:

开启线程有三种方式

// 不会退出runloop
- (void)run; 

// 超时时候到退出runloop
- (void)runUntilDate:(NSDate *)limitDate; 

// 处理完source会退出或者时间到也会退出
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

// 上三个方法分别对应CFRunloop

void CFRunLoopRun(void)

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) //  returnAfterSourceHandled为NO

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) //  returnAfterSourceHandled为YES



复制代码

runrunUntilDate:都会重复的调runMode:beforeDate:

具体的解释看NSRunLoop的退出方式

因此刚才stop以后,确实是退出runloop了,可是由于咱们是用run启动的,因此会重复的调用runMode:beforeDate:又启动了

3.用runMode:beforeDate:启动runloop,再用CFRunLoopStop退出runloop试试

将上一段代码[self.myRunloop run];替换成[self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

成功退出runloop而且线程run后的代码也走了,这时候经过打个暂停断点,看堆栈,发现咱们的线程不在了,说明已经被销毁了(runloop退出后,线程没有任务,天然就销毁了)

2020-05-03 20:21:30.330067+0800 Runloop[60593:2834891] 即将进入Loop,
2020-05-03 20:21:30.330303+0800 Runloop[60593:2834891] 即将处理 Timer,
2020-05-03 20:21:30.330639+0800 Runloop[60593:2834891] 即将处理 Source,
2020-05-03 20:21:30.330906+0800 Runloop[60593:2834891] 即将进入休眠,
2020-05-03 20:21:31.330956+0800 Runloop[60593:2834891] 刚从休眠中唤醒,
2020-05-03 20:21:31.331329+0800 Runloop[60593:2834891] 执行stop
2020-05-03 20:21:31.331591+0800 Runloop[60593:2834891] 即将退出Loop,
2020-05-03 20:21:31.331783+0800 Runloop[60593:2834891] 我会走吗

复制代码

虽然使用self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]能够成功退出runloop,可是仍是有问题,当runloop处理完source后,就退出runloop了,并且这时候,也不会想调用run方法那样,从新进入runloop

因此这种方式仍是不行

最后一个最佳方式,既能手动退出runloop,有不会处理完source就退出runloop,再也不进来

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning) {
	// runMode是有返回值的,当启动runloop后,是不会返回的,因此不会一直在调这个方法,runloop退出了,才会再调
	[theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]
}
复制代码

当想退出runloop的时候,将shouldKeepRunning置为NO就能够了


runloopperformSelector

performSelector:withObject:afterDelay:原理,往runloop添加一个不重复的定时器

因此子线程调用这个方法,是须要开启runloop才有效的

顺便看看performSelector:onThread:withObject:waitUntilDone:

// myThread是常驻线程
self.myThread = [[PermanentThread alloc] initWithTarget:self selector:@selector(myThreadStart) object:nil];

[self.myThread start];
    
NSLog(@"1");
[self performSelector:@selector(performWait) onThread:self.myThread withObject:nil waitUntilDone:NO];
NSLog(@"2");

- (void)performWait
{
    NSLog(@"3");
}

复制代码

若是waitUntilDone为NO,那么就是不等待sel执行完,才往下走

输出为一、二、3

若是为YES,那么就是会卡住当前线程,等待sel执行完才走

输出为一、三、2


网络请求主线程回调,实现同步

需求描述:

给你一个接口,这个接口是网络请求,回调是主线程回来的,如今要求调用这个接口后,须要等待回调回来后,后面的代码才能够接着往下走

- (void)netRequestComplete:(void(^)(void))complete
{
    // 模拟网络请求,主线程回调
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            if (complete) {
                complete();
            }
        });
    });
}

复制代码

使用信号量,会致使死锁

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self netRequestComplete:^{
    NSLog(@"3");
    // 由于主线程被卡住,这里不会走了,因此死锁
    dispatch_semaphore_signal(sema);
}];
    
// 卡住主线程
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)));
NSLog(@"2");   
复制代码

正确方式使用CFRunloopRun

[self netRequestComplete:^{
    NSLog(@"3");
    // stop,退出runloop,主线程runloop退出后,又会自动加入,就像前面讲的,开启runloop是使用run的方法
    CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
}];
    
// CFRunLoopRun()至关加了do-while,这时候下面的代码执行不了
CFRunLoopRun();
NSLog(@"2");
    
复制代码

runloop优化tableview滚动坑点

这个点是从这个文章得知的UITableView性能优化-中级篇

作了一个实验

首先,用perform确实能够滑动tableview滚动的时候,不加载图片,达到优化的效果

可是经过这个实验发现,当我中止滚动的时候,前面滑过的indexPath,都会触发logIndexRow:方法

若是这时候是加载图片,那么是多余的了,由于cell都划出界面了,没有必要加载

由这个现象,能够大概的判断,performSelector:inModes,若是是在defaultmode下调用,虽然如今是在滚动,不会触发方法,可是perform就往runloop的defaultMode添加输入源,但滚动结束的时候,切换回defaultMode,这些输入源都会被触发

// 这个selector,能够是loadImg的方法
[self performSelector:@selector(logIndexRow:)
               withObject:indexPath
               afterDelay:0
                  inModes:@[NSDefaultRunLoopMode]];
复制代码
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"123"];
    
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"123"];
    }
    
    // 滑动的时候,不会调用logIndexRow:,由于这时候mode是滑动,可是perform也是属于输入源,这些事件会被积累在NSDefaultRunLoopMode下,当切换到NSDefaultRunLoopMode下后,就会执行这些输入源事件
    [self performSelector:@selector(logIndexRow:)
               withObject:indexPath
               afterDelay:0
                  inModes:@[NSDefaultRunLoopMode]];
    
    cell.textLabel.text = @"123";
    cell.textLabel.textColor = [UIColor redColor];
    
    return cell;
}
复制代码

runloop卡顿监测

引用源码__CFRunLoopRun分析中的说法

从 kCFRunLoopBeforeSources 为起点到 kCFRunLoopBeforeWaiting 休眠前,这其中处理了大量的工做————执行 block,处理 source0,更新界面…作完这些以后 RunLoop 就休眠了,直到 RunLoop 被 timer、source、libdispatch 唤醒,唤醒后会发送休眠结束的 kCFRunLoopAfterWaiting 通知。咱们知道屏幕的刷新率是 60fps,即 1/60s ≈ 16ms,假如一次 RunLoop 超过了这个时间,UI 线程有可能出现了卡顿,BeforeSources 到 AfterWaiting 能够粗略认为是一次 RunLoop 的起止。至于其余状态,譬如 BeforeWaiting,它在更新完界面以后有可能休眠了,此时 APP 已经是不活跃的状态,不太可能形成卡顿;而 kCFRunLoopExit,它在 RunLoop 退出以后触发,主线程的 RunLoop 除了换 mode 又不太可能主动退出,这也不能用做卡顿检测。

监听***即将处理source***,到***结束睡眠***,若是这个过程超过一帧的时间,就可能出现丢帧的状况(丢帧就会致使卡顿)

那么为何这个过程若是一帧的时间,就可能卡顿呢?

首先咱们要理解屏幕显示原理,大概就是CPU计算文本、布局、绘制、图片解码,以后就提交位图到GPU,GPU就进行渲染,渲染完成后,根据V-sync信号,更新缓冲区,同时,视频控制器的指针,也会根据V-sync信号去缓冲区读取一帧的缓存,显示到屏幕上

也就是说从cpu绘制->GPU渲染,要在16ms内完成,才能保证在指定时间内,给视频控制器读取,不然,视频控制器就会读到上一帧的画面,这就致使卡顿了

因此在即将处理source,到结束睡眠这段时间内,若是CPU一直在处理一件任务,若是超过了16ms,那么可能就来不及在16ms内完成一帧画面的渲染

runloopautoreleasepool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 建立自动释放池。其 order 是-2147483647,优先级最高,保证建立释放池发生在其余全部回调以前。 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并建立新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其余全部回调以后。

设置_wrapRunLoopWithAutoreleasePoolHandler符号断点,能够从汇编代码,看到autoreleasepush、pop

界面更新

不知道怎么证实。。。。


参考文章

NSRunLoop的退出方式

戴铭写的Runloop

NSURLConnection的执行过程

不少runloop的问题和例子

深刻理解RunLoop(YYModel做者)

源码__CFRunLoopRun分析

相关文章
相关标签/搜索