OC底层学习-RunLoop

1.RunLoop概述

  • 顾名思义: 运行循环,在程序运行过程中循环做一些事情
  • 应用范畴:
    • 定时器(Time)、PerformSelector
    • GCD Async Mian Queue
    • 事件响应、手势识别、界面刷新
    • 网络请求
    • AutoreleasePool(自动释放池)
    • 上述这些技术都是基于RunLoop实现的
  • 如果没有RunLoop,程序执行完代码就会退出程序,如:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //命令行程序默认没有RunLoop 执行完代码就退出
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 如果有了RunLoop,程序并不会马上退出,而是保持运行状态
  • RunLoop的基本作用:
    • 保持程序持续运行
    • 处理App中的各种事件(比如触摸事件、定时器事件)
    • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    // UIApplicationMain()中会自动创建一个RunLoop,来保持程序的持续运行
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

2. RunLoop对象

2.1 获取RunLoop对象

  • IOS中有两套API来访问和使用RunLoop
    • Foundation: NSRunLoop
    • Core Foundation : CFRunLoopRef
  • NSRunLoop和CFRunLoop都代表这RunLoopd对象,NSRunLoop是基于CFRunLoopRef的一层OC包裹,并且CFRunLoopRef是开源的
  • 开源地址: CFRunLoopRef开源代码地址
//Foundation获取RunLoop对象
    [NSRunLoop currentRunLoop];//获取当前线程的RUnLoop
    [NSRunLoop mainRunLoop];//获取主线程的RunLoop
    
    //Core Foundation获取RunLoop对象
    CFRunLoopGetCurrent();//获取当前线程的RunLoop对象
    CFRunLoopGetMain();//获取主线程的RunLoop

2.2 RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象

  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value

  • 线程刚开始创建的时候并没有RunLoop对象,RunLoop会在第一次获取它时创建

  • RunLoop会在线程结束时销毁

  • 主线程RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

  • 下面我们查看获取RunLoop对象的源码:

    • 根据上面地址下载CFRunLoopRef的源码
    • 根据获取方法查看源码
//获取主线程RunLoop
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;
}

// 获取当前线程RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

//获取RunLoop的核心方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
	t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    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);
    }
//首先根据key(当前线程)从CFDictionaryGetValue(全局字典)中获取RunLoop对象
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    //判断RunLoop是否存在, 如果不存在就创建一个新的RunLoop对象
    if (!loop) {
	CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
     //再次从字典中取出RunLoop
	loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	//如果loop对象不存在,则把创建的新的RunLoop对象设置到字典中, key为当前线程, 并且把newloop对象赋值给loop对象,最后返回
	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;
}
  • 由上述获取RunLoop源码可以知道,每一个RunLoop对象都有一个唯一一个与之对应的线程,底层采用字典来缓存,线程作为key,RunLoop对象作为value, 并且Runloop对象的创建是在第一个获取RunLoop的时候创建的, 并不会自动先创建好

2.3 RunLoop相关的类

  • Core Foundation中关于RunLoop的5个类

    • CFRunLoopRef : RunLoop类型
    • CFRunLoopModelRef : RunLoop中的Model(模式)对象
    • CFRunLoopSourceRef: RunLoop中的Source(事件)对象
    • CFRunLoopTimerRef: RunLoop中的Timer(定时器)对象
    • CFRunLoopObserverRef: RunLoop中的observer(观察者)对象
  • CFRunLoopRef的代码结构

//OC中头文件可以找到
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

//CFRunLoopRef源码中找到,删除部分源码之后
struct __CFRunLoop {
    pthread_t _pthread; //对应的当前线程对象
    CFMutableSetRef _commonModes;//储存标记为common(通用)的模式
    CFMutableSetRef _commonModeItems;//储存能够在common(通用)的模式下工作item
    CFRunLoopModeRef _currentMode;//RunLoop正在执行的model
    CFMutableSetRef _modes;//储存所有Model的数组
};
  • CFRunLoopModeRef的源码:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

//删除部分源码之后
struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
};
  • CFRunLoopModeRef代表RunLoop的运行模式

  • 一个RunLoop包含若干个Model,每个Model又包含若干个Source0/Source1/Timer/Observer

  • RunLoop启动只能选择其中一个Model,作为currentModel

  • 如果需要切换Model,只能退出当前Model,在重新选择一个Model进入,不同组的Source0/Source1/Timer/Observer能分隔开,互不影响

  • 如果Model里没有任何Source0/Source1/Timer/ObserverRunLoop会立马退出

  • 大概结构图如下:在这里插入图片描述

  • CFRunLoopModeRef的常见两种Model

    • kCFRunLoopDefaultModeNSDefaultRunLoopMode):App的默认Model,通常主线程是在这个Model下运行
    • UITrackingRunLoopMode; 界面跟踪Model,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Model影响
    • 而系统还提供 kCFRunLoopCommonModesNSRunLoopCommonModes)这种模式,但是这并不是真正的模式,它只是一个标记,主要你传入这个标记,那么标记这种模式的对象,就可以在所有标记为common的模式下运行,上面两种才是真正存在的模式,而且这两种模式都是标记为common

2.4 Model内事件分类及RunLoop的状态

  • Model里面事件的分类

    1. Source0
      • 触摸事件处理(会被包装成为Source0对象给RunLoop处理)
      • performSelector: onThread:
    2. Source1
      • 基于Port的线程间通信
      • 系统的事件捕捉
    3. Timers
      • NSTimer
      • performSelector: withObject: afterDelay:inModes:
    4. Observers
      • 用于监听Runloop的状态
      • UI刷新(BeforeWating–即将休眠)
  • RunLoop运行流程:

  • RunLoop的状态:

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

2.5 示例

2.5.1 触摸屏幕事件

  • 下面我们用触摸屏幕的例子,查看是不是会把触摸事件封装成Soucre0对象,然后再给Runloop来处理
  • LLDB指令 bt 可以打印函数调用栈
  • 在这里插入图片描述
  • 由此可知,系统确实会将触摸事件,先封装成为Source0对象,然后再交给Runloop处理

2.5.2 监听RunLoop的状态

  • 下面我来监听下RunLoop的状态
  • 第一种方式:
void observerStateChanged(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry---即将进入");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers---即将处理Timers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources---即将处理Source");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting---即将休眠");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting---刚从休眠中唤醒");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit---即将推出RunLoop");
            break;
        
        default:
            break;
    }
}
//监听RunLoop的状态
    //创建一个observer的对象
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observerStateChanged, nil);
    //添加observer对象到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
    //释放observer对象
    CFRelease(observer);
  • 第二种方式:
//使用block来监听
    CFRunLoopObserverRef observer1 = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry---即将进入");
                break;
            case kCFRunLoopExit:
                NSLog(@"kCFRunLoopExit---即将推出RunLoop");
                break;
            
            default:
                break;
        }
    });
    
    //添加observer对象到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer1, kCFRunLoopCommonModes);
    //释放observer对象
    CFRelease(observer1);
  • 上述两种方式都可以监听RunLoop的状态,当RunLoop切换Model(模式)时,首先会退出当前Model,然后进入另外一个Model

2.6 RunLoop的运行逻辑

  1. 通知Observers: 进入RunLoop
  2. 通知Observers:即将处理Timers
  3. 通知Observers:即将处理Sources
  4. 处理Blocks
  5. 处理Source0(可能会再次处理Blocks)
  6. 如果存在Source1,就跳转到第8步
  7. 通知Observers:开始休眠(等待消息回复)
  8. 通知Observers:结束休眠(被某个消息唤醒)
    • 处理Timer
    • 处理GCD Async To Main Queue
    • 处理Source1
  9. 处理Blocks
  10. 根据当前的执行结果,决定如何操作
    • 回到第2步
    • 退出RunLoop
  11. 通知Observers:退出RunLoop
  • 流程图:在这里插入图片描述

  • 源码执行逻辑(删除部分源码,只留下逻辑源码):

//RunLoop的入口
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {    
    int32_t result = kCFRunLoopRunFinished;

    //通知observers:进入loop
	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //具体要做的事情,核心函数
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    // 通知observers:退出loop
	if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
	rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
  
        //通知observers: 即将处理Timer
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //通知observers: 即将处理Sources
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

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

        //处理Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
	}
            //判断有误Source1
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                //如果有Source1 就跳转到 handle_msg
                goto handle_msg;
            }
        }

        //通知observers: 即将休眠
	if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //开始睡觉
	__CFRunLoopSetSleeping(rl);
	
        do {
           
            // 等待别的消息来唤醒线程
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            
        } while (1);
        // user callouts now OK again
	__CFRunLoopUnsetSleeping(rl);
        //通知observers: 结束休眠
	if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);


        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {

            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
               
            }
        }
#endif
		// 是被 GCD唤醒
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);

        } else {
           //是被Source1唤醒
          
        //处理Source1
		sourceHandledThisLoop = __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;
	}
    return retVal;
}

2.7 RunLoop休眠细节

 // 等待别的消息来唤醒线程
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
  • RunLoop一旦进入休眠状态,就意味着当前线程就不作任何事情了,那CPU就不给当前线程分配任何资源,就会阻塞当前线程,不会继续往下执行了
  • RunLoop的休眠最终是调用内核层面的API 达到线层的真正休眠(达到节省CPU资源的目的),而不是代码阻塞试的“假”休眠(阻塞)
  • RunLoop休眠的实现原理:在这里插入图片描述

2.8 RunLoop的应用

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

  • AFNetworking: 通过RunLoop的方式让子线程一直不死,保存在内存中 ,在频繁使用子线程的过程中,有助于提高性能
  • 线程的任务一旦执行完毕,生命周期就结束,无法在使用,保住线程的命为什么要用RunLoop,用强指针不就好了吗?准确的来讲,使用RunLoop是为了让线程保持激活状态。
  • 实现线程保活
    • 保持线程活下来,我们可以启动当前线程的RunLoop,通过RunLoop的特性来保持线程不死
    • 如果只调用[[NSRunLoop currentRunLoop] run];,达不到线程不死的目的,因为当RunLoop中当前执行的Model中没有任何Source0/Source1/Timer/Observer的时候,RunLoop会立马退出‘,代用该方法底层是调用runMode: beforeDate:方法,传入一个默认Modle,但是该Model下没有任何的Source0/Source1/Timer/Observer,所以会立刻退出
    • 保持Runloop不退出,我们需要添加Source0/Source1/Timer/Observer,可以执行[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
@interface GYThread : NSThread
@end
@implementation GYThread

- (void)dealloc
{
    NSLog(@"%s",__func__);
}
@end


@interface ViewController ()
@property (nonatomic, strong)GYThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[GYThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //waitUntilDone: 如果是YES: 表示需要等到子线程执行完成,在往下执行   NO: 表示不等待 继续往下执行
    [self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
}
//线程需要执行的事情
- (void)run {
    NSLog(@"methodName====%s  currentThread=====%@",__func__,[NSThread currentThread]);
}
/// 该方法目的:线程保活
- (void)threadTest {
    NSLog(@"methodName====%s  currentThread=====%@",__func__,[NSThread currentThread]);
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; 
    [[NSRunLoop currentRunLoop] run];
    //上述两句代码保证了线程一直处在运行中
    
    NSLog(@"END--------");
}
@end
  • 缺点:
    • 无法控制Thread(线程)的生命周期,线程一直处在运行中,无法销毁
    • 控制器Thread对象形成循环引用,都无法进行释放
  • 无法销毁线程的原因:
    • 首先因为改线程对应的RunLoop一直没有停止,表示该线程一直出在运行中, 只有当线程中的事情做完了,线程才回销毁,所以要想线程销毁,那么必须控制当前RunLoop对象停止,所以可以执行以下代码:
- (void)stop {
    //停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
}

//停止runLoop
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
  • 但是调用停止当前RunLoop的方法之后, 最后我们发现还是无法停止RunLoop 。原因如下:
  • [[NSRunLoop currentRunLoop] 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的是开启一个无限循环,底层在不断的循环调用runMode: beforeDate:方法,底层代码结构如下:
  while(1) {
     [[NSRunLoop currentRunLoop] runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate];
     }
  • 所以你停止的只是其中一次的Runloop,然后又会进入RunLoop,所以这个run方法是不会停止的,它专门用于开启一个永不停止的线程(RunLoop),所以需要停止RunLoop,我们需要模仿这个结构自己编译一个可以控制的循环,来控制RunLoop

  • 解决上述两个问题,最终代码

@interface ViewController ()
@property (nonatomic, strong)GYThread *thread;
@property (nonatomic, getter=isStop) BOOL stop;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.stop = NO;
    __weak typeof(self) weakSelf = self;
    self.thread = [[GYThread alloc] initWithBlock:^{
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.isStop) {
        //执行这句代码之后,当前线程就卡在这里休眠,等待被事件唤醒,如果没有事件唤醒,也没有结束RunLoop,那么当前线程会一直卡在这里、睡眠,保证当前线程一直有一个runLoop存在
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"END--------%@",[NSThread currentThread]);
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run {
    NSLog(@"methodName====%s  currentThread=====%@",__func__,[NSThread currentThread]);
}
//停止RunLoop执行方法
- (IBAction)stopBtnClick:(UIButton *)sender {
	 if (!self.thread) return;
    //停止runLoop
    [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}

- (void)stop {
    //设置标记
    self.stop = YES;
    
    //停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    
    NSLog(@"---%s----",__func__);
    //当子线程停掉之后,最后把线程对象清空
    self.thread = nil;
}
- (void)dealloc {
//停止runLoop
    if (self.thread) {
        [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
    NSLog(@"%s",__func__);
}
@end
  • 有人可能有疑问,开启Runloop那里为什么需要添加循环, 在NSDefaultRunLoopMode模式下,如果没有外层循环开启RunLoop的话,只开启一次,那么当你唤醒线程,执行一次任务之后,RunLoop就会退出,好似开启一次RunLoop,执行一次任务之后,RunLoop就结束了,所以为了保证RunLoop的不退出,我们需要开启一个外循环,来不断的进入runloop,根据需要来进入和结束runloop的循环

  • 当我们在viewController界面没有点击停止按钮,而是直接退出界面时, 我们在delloc方法中调用[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];方法,如果waitUntilDone参数为NO可能会引起程序崩溃,抛出坏内存访问异常,因为如果设置为NO,意味着我们不需要等待子线程任务执行完成,就接着执行主线程, delloac被调用完成时,意味着控制器销毁了,但是与此同时,子线程还在访问控制器,所以可能会发生坏内存访问,是因为控制器已经销毁了。解决方法:参数设置为YES

  • 当我们直接返回时,返现控制器销毁了,但是线程还没有销毁,当你返回时,控制会调用delloac方法, 而发现控制器要销毁时,weakSelf(弱指针)会被清空,设置成nil,所以你停止掉当前RunLoop时,当再次判断!weakSelf.isStop该条件,由于weakSelf=nil,那么值是NO,取反就是YES,所以会再次进入循环,重新进入RunLoop,导致线程不会死亡

  while (!weakSelf.isStop) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
  • 解决方案:
//判断当前对象是否存在,如果不存在就直接不执行循环
while (weakSelf && !weakSelf.isStop) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }

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

* 创建的NSTime对象,不管是你手动启动,还是自动启动,默认都是添加到RunLoop的`NSDefaultRunLoopModel`模式下,所以当滑动时,RunLoop进入`UITrackingRunLoopMode`下时,就没办法执行默认模式下的Timer任务了
* 解决方法就是把Timer手动添加到NSRunLoopCommonModes下,这两可以在滑动时也可以解决定时器停止工作的问题
 //该方法创建定时器,系统默认直接添加到NSDefaultRunLoopMode 模式下 ,所以在滑动状态下是无法执行定时器的任务的
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"123");
    }];
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"123");
    }];
    //把timer对象添加到NSRunLoopCommonModes下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

2.8.3对线程保活封装成

@interface GYPermanentThread : NSObject

/// 开始一个可控制生命周期的线程
- (void)start;

/// 停止线程
- (void)stop;

/// 执行任务
/// @param block <#block description#>
- (void)executeTask:(void(^)(void))block;
@end

@interface GYThread : NSThread

@end
@implementation GYThread

- (void)dealloc
{
    NSLog(@"--%s",__func__);
}
@end


@interface GYPermanentThread ()
// 这里声明GYThread是为了打印线程声明周期, 如果不需要刻意换乘NSThread
@property (nonatomic, strong) GYThread *innerThread;

/// OC下使用,在使用C语言时  可能用不到
@property (nonatomic, getter=isStop) BOOL stop;

@end
@implementation GYPermanentThread

#pragma mark --  public method

//OC版实现
//- (instancetype)init
//{
//    self = [super init];
//    if (self) {
//
//        __weak typeof(self) weakSelf = self;
//        self.innerThread = [[GYThread alloc] initWithBlock:^{
//            NSLog(@"begin-----------");
//            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//
//            while (weakSelf && !weakSelf.isStop) {
//                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//            }
//
//            NSLog(@"end-----------");
//        }];
//
//        //可以直接启动
//        [self.innerThread start];
//    }
//    return self;
//}


//C语言实现 : 使用C语言我们更加灵活的控制RunLoop
- (instancetype)init
{
    self = [super init];
    if (self) {
        __weak typeof(self) weakSelf = self;
        
        self.innerThread = [[GYThread alloc] initWithBlock:^{
            NSLog(@"begin-----------");
            //创建一个context对象 因为这个对象是一个结构体,而且放在栈上, 所以最好初始化,如果不初始划, 当前这个内存可能存在一些垃圾直,导致后面出现问题
            CFRunLoopSourceContext context = {0};
            
            //创建一个source对象
            CFRunLoopSourceRef sourceRef = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            
            //添加一个Source对象到RunLoop中
            CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopDefaultMode);
            
            //运行RunLoop  returnAfterSourceHandled: true 代表处理一次Source事件之后就返回 false: 表示处理完Soucre事件之后不返回
            
            //如果设置为true, 就需要添加外循环
//            while (weakSelf && !weakSelf.isStop) {
//                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
//            }
            
            //如果参数设置为false
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
            
            NSLog(@"end-----------");
        }];
        
        //可以直接启动
        [self.innerThread start];
    }
    return self;
}


- (void)start {
    if (!self.innerThread) return;
    [self.innerThread start];
}

- (void)executeTask:(void (^)(void))block {
    if (!self.innerThread || !block) return;
    [self performSelector:@selector(__exectueTask:) onThread:self.innerThread withObject:block waitUntilDone:NO];
}

- (void)stop {
    if (!self.innerThread) return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc {
    NSLog(@"---%s---", __func__);
    
    [self stop];
}

#pragma mark --  private method
- (void)__stop {
    self.stop = YES;
    
    //停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    
    self.innerThread =  nil;
}

- (void)__exectueTask:(void (^)(void))task {
    task();
}
@end

3. 面试题

3.1 讲讲RunLoop,项目中有用到吗?

  • 控制线程声明周期(线程保活)->参考文章2.8.1
  • 解决NSTimer在滑动时停止工作的问题->参考文章2.8.2
  • 监控应用卡顿(后续待学习)
  • 性能优化(后续待学习)

3.2 RunLoop内部实现逻辑?

  • 参考本文2.6 RunLoop的运行逻辑

3.3 RunLoop和线程的关系?

  • 一对一的关系,一条线程对应一个RunLoop对象,默认线程的RunLoop对象是没有创建的,在你第一此获取RunLoop的时候创建

3.4 timer与RunLoop的关系?

  • 结构上:RunLoop下面有许多Model(模式),而Model中又有_timers数组,存放着许多Timer
  • RunLoop会通知Observers,即将处理Timers,而且Time也可以唤醒RunLoop,让RunLoop结束休眠
  • RunLoop控制timer什么时候执行

3.5 程序中添加每3秒一次的NSTimer,当拖动tableview时timer可能无法响应怎么解决?

  • 创建一个NSTimer对象,然后把该对象添加到当前RunLoop的kCFRunLoopCommonModes(通用)模式下

3.6 RunLoop是怎么响应用户操作的,具体流程是什么样的?

  • 首先是由Source1把系统事件捕捉,一旦用户操作,事件会在Source1中处理,相当于一个Source1事件,而Source1又会将事件包装成事件队列,而事件队列是在Source0中处理的。(先由Source1捕捉,然后放在Source0中处理)

3.7 说说RunLoop的几种状态?

  • kCFRunLoopEntry—即将进入
  • kCFRunLoopBeforeTimers—即将处理Timers
  • kCFRunLoopBeforeSources—即将处理Source
  • kCFRunLoopBeforeWaiting—即将休眠
  • kCFRunLoopAfterWaiting—刚从休眠中唤醒
  • kCFRunLoopExit—即将推出RunLoop

3.8 RunLoop的Model作用是什么?

  • 能分隔开不同组的Source0/Source1/Timer/Observer,互不影响
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值