[iOS开发]Runloop

·  阅读 152

什么是RunLoop?

所以什么是RunLoop

【跑圈】可太贴切了

  • 之所以iOS App能持续响应,保持程序运行状态,在于其有一个事件循环(Event Loop)

  • 事件循环机制,即线程能随时响应并处理事件的机制,这种机制要求线程不能退出,而且需要高效的完成事件调度与处理。

  • 事件循环这种机制就叫RunLoop

  • ==RunLoop实际上是一个对象==,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如触摸事件、UI刷新时间、定时器时间、Selector事件等等),从而保持程序的持续运行,而且在没有事件处理的时候,会进入睡眠状态,从而节省CPU资源,提高程序性能。

默认情况下主线程的RunLoop原理

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}


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);
}
复制代码

其中的UIApplicationMain函数内部帮我们开启了主线程的RunLoop。 UIApplicationMain内部拥有一个无限循环的代码。

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}
复制代码

程序会一直在do-while循环中执行

看一下苹果官方给出的RunLoop模型图 在这里插入图片描述

RunLoop就是线程中的一个循环,RunLoop在循环中不断检测,通过Input sources(输入源)Timer sources(定时源)两种来源等待接受消息,然后对接收到的事件通知线程进行处理,并在没有事件的时候进行休息。

RunLoop对象

RunLoop对象的获取

  • 介绍一下RunLoop对象
  • Fundation框架(基于CFRunLoopRef的封装) NSRunLoop对象
    • NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的
[NSRunLoop currentRunLoop];//获得当前RunLoop对象
[NSRunLoop mainRunLoop];//获得主线程的RunLoop对象
复制代码
  • CoreFoundation CFRunLoopRef对象
    • CFRunLoopRef是在CoreFoundation框架内的,其提供了纯C语言函数的API,所有这些API都是线程安全
CFRunLoopGetCurrent();//获得当前线程的RunLoop对象
CFRunLoopGetMain();//获得主线程的RunLoop对象
复制代码

在这里插入图片描述 那么你对应的两种方式其实就是

//Foundation
NSRunLoop *runLoop1 = [NSRunLoop currentRunLoop];
NSRunLoop *mainRunLoop1 = [NSRunLoop mainRunLoop];
//Core Foundation
CFRunLoopRef runLoop2 = CFRunLoopGetCurrent();
CFRunLoopRef mainRunLoop2 = CFRunLoopGetMain();
复制代码

看一下这两个函数的具体实现

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

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;
}
复制代码

其都调用了_CFRunLoopGet0这个函数,等会再看吧

CFRunLoopRef源码部分(引入线程相关)

看一下CFRunLoopRef的源码

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;            /* locked for accessing mode list */
    __CFPort _wakeUpPort; 【通过该函数CFRunLoopWakeUp内核向该端口发送消息可以唤醒runloop】
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread; 【RunLoop对应的线程】
    uint32_t _winthread;
    CFMutableSetRef _commonModes;  【存储的是字符串,记录所有标记为common的mode】
    CFMutableSetRef _commonModeItems;【存储所有commonMode的item(source、timer、observer)】
    CFRunLoopModeRef _currentMode;【当前运行的mode】
    CFMutableSetRef _modes;【存储的是CFRunLoopModeRefstruct _block_item *_blocks_head;【do blocks时用到】
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};
复制代码
  • 除了记录一些属性外,重点是三个成员变量
pthread_t _pthread;【RunLoop对应的线程】
CFRunLoopModeRef _currentMode;【当前运行的mode】
CFMutableSetRef _modes;【存储的是CFRunLoopModeRef复制代码

引入下面的问题了

RunLoop和线程

先看一下_CFRunLoopGet0这个函数是怎么实现的,和RunLoop和线程有什么关系

//全局的Dictionary,key是pthread_t,value是CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问__CFRunLoops的锁
static CFSpinLock_t loopsLock = CFSpinLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//t==0是始终有效的“主线程”的同义词

//获取pthread对应的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    //pthread为空时,获取主线程
    t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
        //第一次进入时,创建一个临时字典dict
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    //根据传入的主线程获取主线程对应的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    //保存主线程,将主线程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    //此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
    //释放dict
        CFRelease(dict);
    }
    //释放mainRunLoop
    CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建

	//从全局字典里获取对应的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
    //如果取不到,就创建一个新的RunLoop
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    //创建好之后,以线程为key,runLoop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runLoop
    if (!loop) {
    //把newLoop存入字典__CFRunLoops,key是线程t
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&loopsLock);
    CFRelease(newLoop);
    }

	//如果传入线程就是当前线程
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
        //注册一个回调,当线程销毁时,销毁对应的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
复制代码

通过这段源码我们可以看到

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

RunLoop的相关类

与RunLoop相关的类有5个

  1. CFRunLoopRef 代表了RunLoop的对象
  2. CFRunLoopModeRef RunLoop的运行模式
  3. CFRunLoopSourceRef 就是RunLoop模型图中提到的输入源(事件源)
  4. CFRunLoopTimerRef 定时源
  5. CFRunLoopObserverRef观察者,监听RunLoop状态的改变

  1. 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer

在这里插入图片描述 2. 每次调用RunLoop的主函数时,只能指定其中的一个Mode,这个Mode被称作CurrentMode 3. 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响 4. 如果一个mode中一个Sourcr/Timer/Observer都没有,则RunLoop会直接退出,不进入循环

RunLoop中相关类的实现

一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer 这句话其实就是5个相关类的关系

CFRunLoopModeRef

还是那句话:CFRunLoopModeRef代表RunLoop的运行模式

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    CFStringRef _name; //mode名称,运行模式是通过名称来识别的
    Boolean _stopped; //mode是否被终止
    char _padding[3];
    //整个结构体最核心的部分
------------------------------------------
    CFMutableSetRef _sources0;//Sources0
    CFMutableSetRef _sources1;//Sources1
    CFMutableArrayRef _observers;//观察者
    CFMutableArrayRef _timers;//定时器
------------------------------------------
    CFMutableDictionaryRef _portToV1SourceMap;//字典    key是mach_port_t,value是CFRunLoopSourceRef
    __CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};
复制代码
  • 一个CFRunLoopModeRef对象有一个name,若干source0source1,timer,observerport,可以看出来事件都是由mode在管理,而RunLoop管理着Mode

五种运行模式

系统默认注册的五个Mode

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
  5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

其中kCFRunLoopDefaultModeUITrackingRunLoopModekCFRunLoopCommonModes是我们开发中需要用到的模式

CommonModes

在RunLoop对象中,前面有一个有一个叫CommonModes的概念

//简化版本
struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
    CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;//当前运行的mode
    CFMutableSetRef _modes;//存储的是CFRunLoopModeRef对象,不同mode类型,它的mode名字不同
};
复制代码
  • 一个Mode可以将自己标记为Common属性,通过将其ModeName添加到RunLoop的commonModes中。
  • 每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems里的Source/Observer/Timer同步到具有Common标记的所有Mode里。其底层原理
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
    //获取所有的_commonModeItems
    CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
    //获取所有的_commonModes
    CFSetAddValue(rl->_commonModes, modeName);
    if (NULL != set) {
        CFTypeRef context[2] = {rl, modeName};
        //将所有的_commonModeItems逐一添加到_commonModes里的每一个Mode
        CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
        CFRelease(set);
    }
    } else {
    }
    __CFRunLoopUnlock(rl);
}
复制代码

CFRunLoop对外暴露的管理Mode接口只有下面两个

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
复制代码

什么是Mode Item? Mode到底包含哪些类型的元素?

  • RunLoop需要处理的消息,包括time以及source消息,他们都属于Mode item
  • RunLoop也可以被监听,被监听的对象是observer对象,也属于Mode item
  • 所有的mode item都可以被添加到Mode中,Mode中可以包含多个mode item,一个item也可以被加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则RunLoop会退出,不进入循环

  • mode暴露的mode item的接口有下面几个
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
复制代码
  • 我们仅能通过操作mode name来操作内部的mode,当你传入一个新的mode nameRunLoop内部没有对应的mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef
  • 对于一个RunLoop来说,其内部的mode只能增加不能删除

苹果公开提供了两个Mode

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode,你可以用这两个Mode Name来操作其对应的Mode
  • 同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 "Common"。使用时注意区分这个字符串和其他 mode name。

CFRunLoopSourceRef

事件产生的地方

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;   //执行顺序
    CFMutableBagRef _runLoops;//包含多个RunLoop
    //版本
    union {
    CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};
复制代码

两个版本 Source0Source1

  • Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件
  • Source1包含了一个mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop的线程。

随便写个button的点击事件 通过thread backtrace查看调用栈 我们可以看到点击事件是怎样来的 在这里插入图片描述

  • 首先程序启动,调用18行的main函数,main函数调用17行UIApplicationMain函数,然后一直往上调用函数,一直到第0行的点击事件
  • 同时我们可以看到上面调用了Source0,也就是说我们点击事件属于Source0函数的,点击事件就是在Source0中进行处理的。

在这里插入图片描述

  • 而至于Source1,则是用来接受、分发系统事件,然后再分发到Source0中处理

CFRunLoopTimerRef

基于时间的触发器

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;//包含timer的mode集合
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;        /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    CFIndex _order;            /* immutable */
    CFRunLoopTimerCallBack _callout; //timer的回调   
    CFRunLoopTimerContext _context;   //上下文对象
};
复制代码
  • CFRunLoopTimerRef是基于时间的触发器,它和NSTimer可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调

对于NSTimer scheduledTimerWithTimeInterval和RunLoop的关系

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
复制代码

会自动加入NSDefaultRunLoopMode

两者一样

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
复制代码

定时器滑动时不准确

我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况

举个例子

新建个tableView

将定时器添加到当前RunLoop的NSDefaultRunLoopMode下 在这里插入图片描述 正常情况下:一秒钟打印一次 在这里插入图片描述

当我们对tableView进行拖拽时 计时器停止了 在这里插入图片描述

这个原因就是

  • 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode
  • 当我们进行拖拽时,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加NSTimer,所以我们的NSTimer就不工作了
  • 当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了

那么对应这个问题我们应该怎么解决呢?

我们不能在这两种模式下让NSTimer都正常工作吗?

使用CommonModes就可以解决了(这也就解决了之前的问题 Common到底能干哈)

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
复制代码

此时我们进行拖拽就不会有任何问题

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;//监听的RunLoop
    CFIndex _rlCount;//添加该Observer的RunLoop对象个数
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;//同时间最多只能监听一个
    CFRunLoopObserverCallBack _callout;//监听的回调
    CFRunLoopObserverContext _context;//上下文用于内存管理
};

//观测的时间点有一下几个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
    kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
复制代码

RunLoop的内部逻辑

根据文档中的描述 RunLoop的内部逻辑如下 在这里插入图片描述 在这里插入图片描述

在这里插入图片描述 精简后的 __CFRunLoopRun函数,保留了主要代码 看一下具体实现

【用DefaultMode启动】
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
【用指定的Mode启动,允许设置RunLoop超时时间】
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
【RunLoop的实现】
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
【首先根据modeName找到对应mode】
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
【如果mode里没有source/timer/observer, 直接返回。】
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
  【1. 通知 Observers: RunLoop 即将进入 loop。】
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
   【内部函数,进入loop】
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
          【 2. 通知 Observers: RunLoop 即将触发 Timer 回调。】
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            【3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。】
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
        【执行被加入的block】
            __CFRunLoopDoBlocks(runloop, currentMode);
            
         【4. RunLoop 触发 Source0 (非port) 回调。】
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
          【 执行被加入的block】
            __CFRunLoopDoBlocks(runloop, currentMode);
 
     【5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。】
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            【通知 Observers: RunLoop 的线程即将进入休眠(sleep)。】
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
           【7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。】
             • 一个基于 port 的Source 的事件。
             • 一个 Timer 到时间了
             • RunLoop 自身的超时时间到了
             • 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            【8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。】
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            【收到消息,处理消息。】
            handle_msg:
 
            【 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。】
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            【9.2 如果有dispatch到main_queue的block,执行block。】
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            【 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件】
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            【 执行加入到Loop的block】
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                【 进入loop时参数说处理完事件就返回。】
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                【 超出传入参数标记的超时时间了】
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                【 被外部调用者强制停止了】
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                【 source/timer/observer一个都没有了】
                retVal = kCFRunLoopRunFinished;
            }
            
            【 如果没超时,mode里没空,loop也没被停止,那继续loop。】
        } while (retVal == 0);
    }
    
   【 10. 通知 Observers: RunLoop 即将退出。】
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
复制代码

实际上RunLoop就是这样的一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里,知道超时或者被手动调用,该函数才会返回

RunLoop回调

  • 当App启动时,系统会默认注册五个Mode【就是上面那五个】
  • 当RunLoop进行回调时,一般都是通过一个很长的函数调用出去(call out),当你在你的代码中断点调试时,通常能在调用栈上看到这些函数。这就是RunLoop的流程:
{
    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
复制代码

实例测试

NSTimer的使用

上面有讲

ImageView延迟显示

当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能出现卡顿的情况。

如何解决这个问题?

我们应该推迟图片的实现,也就是ImageView推迟显示图片。当我们滑动时不要加载图片, 拖动结束在显示

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName.png"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
复制代码

用户点击屏幕,在主线程中,三秒之后显示图片,但是当用户点击屏幕之后,如果此时用户又开始滚动textview,那么就算过了三秒,图片也不会显示出来,当用户停止了滚动,才会显示图片。

这是因为限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滚动textview的时候,程序运行在tracking模式下面,所以方法setImage不会执行。

常驻线程

开发应用程序的过程中,如果后台操作十分频繁,比如后台播放音乐、下载文件等等,我们希望这条线程永远常驻内存

我们可以添加一条用于常驻内存的强引用子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop

@property (nonatomic, strong) NSThread *thread;
复制代码
 self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    [self.thread start];
}

- (void)run1 {
    NSLog(@"----run1-----");

    /*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
          下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/

        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    // 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
    // 方法2
//    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    // 方法3
//    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    
    
    [[NSRunLoop currentRunLoop] run];
        // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
        NSLog(@"未开启RunLoop");
}
我们同时在我们自己新建立的这个线程中写一下touchesBegan这个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) run2 {
    NSLog(@"----run2------");
}
复制代码

我们必须保证线程不消亡,才可以在后台接受时间处理,所以如果没有实现添加NSPort或者NSTimer,会发现执行完run方法,线程就会消亡,后续再执行touchbegan方法无效。

实现了上面三个方法之一,就可以发现执行完了run方法,这个时候再点击屏幕,可以不断执行test方法,因为线程self.thread一直常驻后台,等待事件加入其中,然后执行。

分类:
iOS
标签: