iOS RunLoop

前言

这应该是面对招聘来说最重要的一个问题了
这篇要认真写和认真学 还是一个很抽象的概念

好好学习吧
游戏不如意你可以骂队友 那生活呢

祝你好运

什么是RunLoop

顾名思义

运行循环
在程序运行过程中循环做一些事情

大概这个样子

应用范围

  • 定时器 (Timer)、PerformSelector
  • GCD Async Main Queue
  • 事件响应 手势识别 界面刷新
  • 网络请求
  • AutoreleasePool

也就是没有RunLoop 上述事件是无法实现的

RunLoop的基本作用

在iOS程序int main代码中

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

这里就是创建了一个RunLoop

  • 有了RunLoop 程序不会马上退出 而是保持运行状态
  • 处理App中的各种事件 比如触摸事件 定时器事件等
  • 节省CPU资源 提高程序性能 该做事时做事 该休息时休息

RunLoop对象

iOS的两套API来访问和使用Runloop

  • Foundation : NSRunLoop
  • Core Foundation : CFRunLoopRef

NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装

获取RunLoop对象

Foundation

    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //获得当前线程的Runloop对象
    NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
    //获得主线程的Runloop对象

Core Foundation

	CFRunLoopRef runLoop = CFRunLoopGetCurrent();
	//获得当前线程的Runloop对象
	CFRunLoopRef runLoop = CFRunLoopGetMain();
	//获得主线程的Runloop对象

RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里 线程作为Key RunLoop作为value
  • 线程刚创建时并没有RunLoop对象 RunLoop会在第一次获取它的时候创建
  • RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建) 子线程默认没有开启RunLoop

RunLoop相关的类

Core Foundation中关于RunLoop的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

底层结构是这样子 源码里面的

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _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
    pthread_t _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;
};

我们把重要的拿出来 简化一下这个结构体

struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
};

CFMutableSetRef _modes;里面装着好多CFRunLoopModeRef对象
但是其中只有一个可以称为_currentMode 也就是当前的mode

那么我们看看CFRunLoopModeRef类结构体 直接简化贴重要的了

struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
};

可以看出来
CFRunLoopModeRef中装着CFRunLoopSourceRef CFRunLoopTimerRef CFRunLoopObserverRef

CFRunLoopMode的item

Mode中的Source0/Source1/Timer/Observer被统称为mode item 一个item可以被同时加入多个mode 但一个item被重复加入到同一个mode时不会有效果的 如果一个mode中一个item都没有 那么RunLoop会直接退出 不进入循环

Mode管理mode item的接口有以下几个

CFRunLoopAddTimer(<#CFRunLoopRef rl#>, <#CFRunLoopTimerRef timer#>, <#CFRunLoopMode mode#>)
CFRunLoopRemoveTimer(<#CFRunLoopRef rl#>, <#CFRunLoopTimerRef timer#>, <#CFRunLoopMode mode#>)
CFRunLoopAddSource(<#CFRunLoopRef rl#>, <#CFRunLoopSourceRef source#>, <#CFRunLoopMode mode#>)
CFRunLoopRemoveSource(<#CFRunLoopRef rl#>, <#CFRunLoopSourceRef source#>, <#CFRunLoopMode mode#>)
CFRunLoopAddObserver(<#CFRunLoopRef rl#>, <#CFRunLoopObserverRef observer#>, <#CFRunLoopMode mode#>)
CFRunLoopRemoveObserver(<#CFRunLoopRef rl#>, <#CFRunLoopObserverRef observer#>, <#CFRunLoopMode mode#>)

CFRunLoopModeRef

  • CFRunLoopModeRef代表RunLoop的运行模式
  • 一个RunLoop包含若干个Mode 每个Mode又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动的时候只能选择其中一个Mode 作为currentMode
  • 如果需要切换Mode 只能退出当前RunLoop 再重新选择一个Mode进入 (不会导致程序退出)
    1. 这样做主要是为了分割开不同组的Source0/Source1/Timer/Observer

常见的两种Mode

  • kCFRunLoopDefaultMode (NSDefaultRunLoopMode) : APP的默认Mode 默认主线程是在这个Mode下运行
  • UITrackingRunLoopMode : 界面跟踪Mode 用于ScrollView追踪触摸滚动 保证界面滑动时不受其他Mode影响

不太常见的几种Mode

  • UIInitializationRunLoopMode : 在刚启动App时进入的第一个Mode 启动完就不再使用
  • GSEventReceiveRunLoopMode : 接受系统事件内部的Mode 通常用不到

特殊Mode

  • kCFRunLoopCommonModes : 这是一个用于占位的Mode 作为标记UITrackingRunLoopMode和kCFRunLoopDefaultMode用 并不是一种真正的Mode

CFRunLoopModeRef的成员变量

Source0

  • 负责触摸事件处理
  • performSelector:onThread:

Source1

  • 基于Port的线程间通信
  • 系统事件捕捉 (之后分发给Source0处理)

Timers

  • NSTimer
  • performSelector:withObject:afterDelay:

Observers

  • 用于监听RunLoop的状态
  • UI刷新 (BeforeWaiting) 也就是在线程睡眠之前
  • autorelease pool

CFRunLoopObserverRef

RunLoop一共有这么几种状态 源码里的
上面说了Observers用于监听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
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

添加Observer监听RunLoop的所有状态

    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"kCFRunLoopBeforeTimers");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"kCFRunLoopBeforeSources");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"kCFRunLoopBeforeWaiting");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"kCFRunLoopAfterWaiting");
                break;
            case kCFRunLoopExit:
                NSLog(@"kCFRunLoopExit");
                break;;
            default:
                break;
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);

RunLoop的运行逻辑以及分析

  1. 通知Observers : 进入Loop
  2. 通知Observers : 即将处理Timers
  3. 通知Observers : 即将处理Sources
  4. 处理Blocks
  5. 处理Sources (可能会再次遇到Blocks)
  6. 如果存在Source1 就跳转到第8步
  7. 通知Observers : 开始休眠 (等待消息唤醒)
  8. 通知Observers : 结束休眠 (被某个消息唤醒)
    1. 处理Timer
    2. 处理GCD Async To main Queue
    3. 处理Source1
  9. 处理Blocks
  10. 根据前面的执行结果 决定如何操作
    1. 回到第2步
    2. 退出Loop
  11. 通知Observers : 退出Loop

循环的就是第2步到第10步的1.

源码流程分析

我们知道点击事件是由RunLoop处理的 那我们只需要处理一个点击事件 看看它的调用栈就可以了

在这里插入图片描述
在这里打一个断点

在这里插入图片描述
昨天就会出来这些东西
可以看到1和14中间省略了好多 那么中间这些应该就是RunLoop的调用过程

在这里插入图片描述
在下面输入一个 bt 就可以看到完整的调用栈
找到第一个带有RunLoop的函数 还看到了Run的字样 那么RunLoop的入口应该就是这个函数 我们寻找这个函数就好了

在这里插入图片描述
在源码中找到了这个函数 很长很复杂
我们只需要关注一些重要的 另外太细节的东西没必要盯着

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {  
    //通知Observers : 进入Loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    //具体要做的事情
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    //通知Observers : 进入Loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    return result;
}

剩下一些重要的大概就是这样子 一些锁啊 栈进出啊我们没有必要关心 只需要知道这个流程是什么样子的就好了

然后我们在看看具体要做的事情的 __CFRunLoopRun函数里面的流程
也是一样 我删除了需要没有必要的 留下了重要的 在代码附近都写了注释

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    int32_t retVal = 0;
    do {
        // 通知Observers : 即将处理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        // 通知Observers : 即将处理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        // 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);

        // 处理Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }

        // 判断有无Source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 如果有Source1 就跳转到handle_msg
            goto handle_msg;
            }
        }

        //通知Observers : 即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);

        //等待别的消息来唤醒当前线程
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

        __CFRunLoopUnsetSleeping(rl);
        //通知Observers : 结束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    handle_msg:;
        // if被timer唤醒
        if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            //处理Timers
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        }
    // if被GCD唤醒
        else if (livePort == dispatchPort) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { //被Source1唤醒
            // 处理Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
	
        // 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 设置返回值
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
	    } else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
	    retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }

    } while (0 == retVal);

    return retVal;
}

RunLoop的源码还是很抽象的 从源码看下来基本上和这个标题开始的流程是差不多的

在这里插入图片描述
最终是通过这个很长的函数 去调用UIKit处理事件的

小总结一下这些很长的函数

通知Observers : 进入Loop
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

处理Blocks
CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

处理Source0
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

处理Timer
CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

处理GCD
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

处理Source1

CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

休眠的细节

        //等待别的消息来唤醒当前线程
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

这一句代码是真正阻塞了线程 因为RunLoop休眠了 是真的什么都不干了 所以是真正阻塞了线程 跟别的线程阻塞是任务卡在这里不太一样 调用了内核的API 底层是调用了mach_msg()函数

从用户态调用mach_mag()函数之后 会自动转到内核态 调用了内核态的mach_msg()函数

随后等待消息 没有消息就让线程休眠 有消息就唤醒线程 转换到用户态处理消息

在这里插入图片描述

真正休眠了线程就会节省cpu资源

RunLoop在实际开发中的应用

控制线程生命周期 (线程保活)

让一个线程不要销毁的那么快
我们经常需要在子线程进行一些任务 如果任务结束 线程就销毁
那么在后面的任务里可能又需要创建子线程 这样比较麻烦
所以我们需要控制线程的生命周期 也就是线程保活

正常情况

先看看正常情况下的子线程

创建一个NSThread类
重写dealloc方法

- (void)dealloc {
    NSLog(@"dealloc");
}

在viewController里

    WXThread *thread = [[WXThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start];
- (void)run {
    NSLog(@"%s -- %@", __func__, [NSThread currentThread]);
}

在这里插入图片描述
可以看到 几乎是在同一时间子线程就已经销毁了

无用方法

第一时间可以想到的办法可能是这样的

- (void)run {
    NSLog(@"%s -- %@", __func__, [NSThread currentThread]);
    while(1);
}

可是这样有什么意义呢 这个线程虽然没有被销毁 但是已经做不了其他任务了 因为它一直卡在while(1)的循环里 就算安排了别的任务 也不能进行
这种保命的方法没有什么意义

RunLoop方法

那是不是我们让RunLoop在这个线程里跑起来 然后休眠就可以了呢

- (void)run {
    NSLog(@"%s -- %@", __func__, [NSThread currentThread]);
    
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"end");
}

在这里插入图片描述
也是失败了 这里的原因是什么呢

如果Mode里面没有任何Source0/Source1/Timer/Observer RunLoop会直接退出

所以我们就需要往RunLoop里面添加Source/Timer/Observer

- (void)run {
    NSLog(@"%s -- %@", __func__, [NSThread currentThread]);
    
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"end");
}

这里加什么都可以 这样RunLoop就会休眠 因为它没有什么事情可以做 但是也不会导致线程销毁 因为里面有RunLoop阻塞了线程

在这里插入图片描述

而在实际开发中
在这里插入图片描述
这个方法只是为了线程保活

子线程实际上要执行的任务是另外的任务

@property (nonatomic, strong) WXThread *thread;

    self.thread = [[WXThread 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.thread withObject:nil waitUntilDone:NO];
}
//子线程要执行的任务
- (void)test {
    NSLog(@"%s -- %@", __func__, [NSThread currentThread]);
}
self.thread = [[WXThread alloc] initWithTarget:self selector:@selector(run) object:nil];

这一句代码对当前的控制器A进行了强引用 在我们返回另一个控制器B的时候
控制器A是不会dealloc的 同样的 self.thread这个子线程 也不会dealloc

可以这样写

    self.thread = [[WXThread alloc] initWithBlock:^{
        NSLog(@"%s -- %@", __func__, [NSThread currentThread]);
        
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
         
        NSLog(@"end");
    }];
    [self.thread start];

这样写也是保了线程的活
而且我们返回另一个控制器B的时候 控制器A就会dealloc
但是 self.thread这个子线程 还是不会dealloc

为什么呢

[[NSRunLoop currentRunLoop] run];

因为这个线程一直卡在这个地方 线程一直不会dealloc
这样这个线程就会一直不销毁 如果我们希望的就是它一直不销毁那还可以 这样就相当于是一个全局的线程 一直不会销毁 谁都可以调用它

但是我们的目标是 控制线程的生命周期
要能做到想让这个线程什么时候销毁就什么时候销毁

首先 我们不能在控制器A的dealloc方法中停止RunLoop CFRunLoopStop(CFRunLoopGetCurrent());
因为控制器A的dealloc方法默认是在主线程调用的 而子线程的RunLoop是子线程的
这样子做就是停止了主线程的RunLoop

那是不是可以在控制器A的dealloc方法里调用子线程的停止RunLoop的方法呢

- (void)dealloc {
    [self performSelector:@selector(stopSelfThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)stopSelfThread {
    CFRunLoopStop(CFRunLoopGetCurrent());
}

还是不行

这时候就涉及到[[NSRunLoop currentRunLoop] run];的底层

在run的解释中

it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.

这一句的意思是 run的底层一直在调用
在这里插入图片描述
而且是无限循环 相当于

while(1) {
	[NSRunLoop currentRunLoop] runMode:<#(nonnull NSRunLoopMode)#> beforeDate:<#(nonnull NSDate *)#>
}

所以CFRunLoopStop(CFRunLoopGetCurrent());这一句代码 相当于只是停止了其中的一次循环 很快就会开启之后的循环
所以NSRunLoop的run方法是无法停止的 它专门用于开启一个永不销毁的线程

@property (nonatomic, assign, getter=isStoped) BOOL stopped;

    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[WXThread alloc] initWithBlock:^{
        NSLog(@"%s -- %@", __func__, [NSThread currentThread]);
        
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.stopped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }   
    }];
    [self.thread start];

- (void)dealloc {
    
    [self performSelector:@selector(stopSelfThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)stopSelfThread {
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

开始我认为这样是可以的 在控制器dealloc的时候调用stopSelfThread以为可以结束这个线程 结果发现没有

正确的方法应该是 有一个点击事件 点击了之后调用stopSelfThread方法 然后就可以结束线程的生命

这个我的停止按钮 里面添加了销毁线程的点击事件 调用stopSelfThread方法

- (void)stopp {
    if (!self.thread) return;
    
    [self performSelector:@selector(stopSelfThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

至于为什么在控制器调用stopSelfThread方法程序会崩溃

[self performSelector:@selector(stopSelfThread) onThread:self.thread withObject:nil waitUntilDone:NO];

单独看看这个方法 使用self进行调用stopSelfThread方法 而这个方法如果在控制器的dealloc方法调用 能走到dealloc方法 说明控制器已经销毁了 自然无法调用

首先[self performSelector:@selector(stopSelfThread) onThread:self.thread withObject:nil waitUntilDone:NO]; 这句代码的最后一个参数要设置为YES
设置为NO的意思就是相当于异步async 可以先执行后面的
所以我们需要先执行这一句代码 保证控制器在存活的状态下执行

随后在前面要多加一个判断条件weakSelf有值
我们知道一个弱指针指向的对象如果要销毁了 那么弱指针会置nil
既然置nil了 那么即使执行了self.stopped = YES
在原先的判断条件!weakSelf.stopped中 这里也会是nil 那么就会接着循环RunLoop 所以无法停止RunLoop
那么我们只需要加上要判断weakSelf也有值就可以停止RunLoop了

        while (weakSelf && !weakSelf.stopped) {
             [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }

在这里插入图片描述

最后一件事 真最后一件了

上面这样做我们解决了当控制器销毁时也同时销毁线程
但是试试就知道 如果我们先点击了停止按钮 也就是先销毁线程
然后我们再点击back 退出当前的控制器A 程序就会崩溃
因为子线程已经被销毁了 你还要再控制器销毁之前再进入子线程一次
肯定是不可以的🙅

所以加上几个判断条件就好了嘛

首先在stopSelfThread方法的最后将子线程置空

self.thread = nil;

然后在方法

- (void)stopp {
    [self performSelector:@selector(stopSelfThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

中的前面添加判断条件 if (!self.thread) return;

完整代码 :
省略了一些UI部分

@property (nonatomic, strong) WXThread *thread;
@property (nonatomic, assign, getter=isStoped) BOOL stopped;

- (void)viewDidLoad {
    __weak typeof(self) weakSelf = self;

    self.stopped = NO;
    self.thread = [[WXThread alloc] initWithBlock:^{
        NSLog(@"%s -- %@", __func__, [NSThread currentThread]);

        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (weakSelf && !weakSelf.stopped) {
             [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"%@ -- end", [NSThread currentThread]);

    }];
    [self.thread start];
}

- (void)dealloc {

    NSLog(@"%s", __func__);
    [self stopp];
}
- (void)stopp {
    if (!self.thread) return;
    
    [self performSelector:@selector(stopSelfThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)stopSelfThread {
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    
    self.thread = nil;
}

解决NSTimer在滑动时停止工作的问题

    self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) style:UITableViewStyleGrouped];
    self.tableView.backgroundColor = [UIColor yellowColor];
    
    static int count = 0;
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", count++);
    }];
    
    [self.view addSubview:self.tableView];

我们在tableView里设置一个定时器 每过一秒钟count++
正常来说就是一秒钟加1
但是我们在滚动tableView的时候 定时器就会停止

可以看到本来时40 - 41 下一个变成了46 - 47 中间停了五秒钟
这就是NSTimer在滑动时停止工作的问题
在这里插入图片描述

RunLoop在同一时间只能运行一种Mode

那我们需要解决的问题就是让这个定时器在两种模式下都能工作

    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"%d", count++);
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

NSRunLoopCommonModes不是一种真的模式 而是一种标记
提醒一下这个timer 将来可以运行在什么模式下
默认模式和滑动模式都属于NSRunLoopCommonModes

这样就解决了问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

waxuuuu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值