RunLoop

  • 所有的RunLoop保存在一个全局Dictionary里,线程作为key,RunLoop作为value。
  • RunLoop都是通过懒加载的方式创建。
  • 子线程RunLoop需要手动创建,主线程RunLoop在UIApplicationMain中创建。

以下是RunLoop,RunLoopMode和Source具体实现代码:

struct __CFRunLoop {
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;

    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;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};
struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;

    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    Boolean _stopped;
    char _padding[3];
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};
struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;         /* 优先级,越小,优先级越高*/
    CFMutableBagRef _runLoops;
    union {  // 联合,用于保存source的信息,同时可以区分source是0还是1类型
        CFRunLoopSourceContext version0;
        CFRunLoopSourceContext1 version1;
    } _context;
};

每个RunLoop包含多种模式(_modes),但同时只能处于一种模式(_currentMode)。
每种模式下又包含_sources0,_sources1,_observers,_timers四种事件,它们都是集合类型。
RunLoop在切换模式时,会先退出当前loop,再重新进入一个新的loop。
不同Mode下的Source0/Source1/Timer/Observer被分隔开来,互不影响。一个Source0/Source1/Timer/Observer,可以被加入到多个Mode下。
如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。


mode类型(_name):

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

处理事件:

  • source0(偏向应用层)
  1. 触摸事件的处理
  2. performSelector:onThread:
    source0处理触摸事件
  • source1(由RunLoop和内核管理,Mach Port驱动)
  1. 线程间通信
  2. 系统事件的捕捉(比如触摸屏幕由source1捕捉,再交给source0处理)
  3. 网络请求接收CFSocket 的通知,并交给Source0处理。

子线程捕获事件(eventfetch-thread)
  • Timer
  1. NSTimer
  2. performSelect:withObject:afterDelay:
  • Observers
  1. 监听Runloop状态
    主线程默认监听BeforeWaiting执行界面更新
    主线程默认监听Entry执行_objc_autoreleasePoolPush()
    主线程默认监听BeforeWaiting执行_objc_autoreleasePoolPop() + _objc_autoreleasePoolPush()
    主线程默认监听Exit执行_objc_autoreleasePoolPop()
    主线程默认监听BeforeWaiting执行Core Animation
Runloop状态
/* Run Loop Observer Activities */
public struct CFRunLoopActivity : OptionSet {
    public init(rawValue: CFOptionFlags)

    public static var entry: CFRunLoopActivity { get }
    public static var beforeTimers: CFRunLoopActivity { get }
    public static var beforeSources: CFRunLoopActivity { get }
    public static var beforeWaiting: CFRunLoopActivity { get }  // 即将进入休眠
    public static var afterWaiting: CFRunLoopActivity { get }  // 刚从休眠唤醒
    public static var exit: CFRunLoopActivity { get }
    public static var allActivities: CFRunLoopActivity { get }
}

实现以下代码,监听主线程default模式下Runloop状态的切换。

// viewDidLoad
let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0, observeRunLoopActivities(), nil)
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .defaultMode)

func observeRunLoopActivities() -> CFRunLoopObserverCallBack {
    
    return {(observer, activity, context) -> Void in
    
        switch(activity) {
        case CFRunLoopActivity.entry:
            print("Run Loop已经进入Loop")
        case CFRunLoopActivity.beforeTimers:
            print("Run Loop分配定时任务前")
        case CFRunLoopActivity.beforeSources:
            print("Run Loop分配输入事件源前")
        case CFRunLoopActivity.beforeWaiting:
            print("Run Loop休眠前")
        case CFRunLoopActivity.afterWaiting:
            print("Run Loop被唤醒")
        case CFRunLoopActivity.exit:
            print("Run Loop已经退出Loop")
        default:
            break
        }
    }
}

运行程序,发现log中并没有Run Loop已经进入Loop,这是因为开启监听的时候,Runloop早已经启动了。

当app处于“静止”时,滑动scrollView,会显示Run Loop被唤醒,然后执行一系列任务后,log显示Run Loop已经退出Loop,这印证了前面所说的当Runloop切换mode时,Runloop会先退出当前loop。
此时滑动scrollView,不会有任何log,因为default模式下的observer监听不到tracking模式的状态变化。
当停止滑动时,Runloop又切换到default模式,log显示Run Loop已经进入Loop

如果将代码改成如下,将observer加入到commonModes:

let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0, observeRunLoopActivities(), nil)
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
    
func observeRunLoopActivities() -> CFRunLoopObserverCallBack {
    
    return {(observer, activity, context) -> Void in
    
        switch(activity) {
        case CFRunLoopActivity.entry:
            print("Run Loop已经进入Loop: ", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()) ?? "")
        case CFRunLoopActivity.exit:
            print("Run Loop已经退出Loop: ", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()) ?? "")
        default:
            break
        }
    }
}

当滑动scrollView时,输出:

Run Loop已经退出模式:  CFRunLoopMode(rawValue: kCFRunLoopDefaultMode)
Run Loop已经进入模式:  CFRunLoopMode(rawValue: UITrackingRunLoopMode)

当停止滑动时,输出:

Run Loop已经退出模式:  CFRunLoopMode(rawValue: UITrackingRunLoopMode)
Run Loop已经进入模式:  CFRunLoopMode(rawValue: kCFRunLoopDefaultMode)

RunLoop的运行逻辑如下图:

RunLoop被唤醒的事件包括:

  • 一个基于mach port 的Source 的事件(source1)
  • 一个 Timer 到时间了
  • RunLoop 自身的超时时间到了
  • 被其他调用者手动唤醒

source0无法主动唤醒RunLoop。

也可通过如下函数将指定block交给RunLoop执行。

CFRunLoopPerformBlock(CFRunLoopGetCurrent(), CFRunLoopMode.commonModes.rawValue) {
    // do something
}

RunLoop的入口函数是CFRunLoopRun()。当切换模式时,会调用CFRunLoopRunInMode(),并传入新的mode。

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRunSpecific实现如下:

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

    // ...

    // 通知Observers:进入Loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // 进入RunLoop循环
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    // 通知Observers:退出Loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    // ...

    return result;
}

在源码中可以看到大量USE_DISPATCH_SOURCE_FOR_TIMERSUSE_MK_TIMER_TOO的定义,前者代表gcd定时器,后者代表普通定时器。在 macOS 下则同时支持使用 dispatch_source 和 MK_TIMER 来构建RunLoop定时器,其他平台则只支持 MK_TIMER。

#if DEPLOYMENT_TARGET_MACOSX
#define USE_DISPATCH_SOURCE_FOR_TIMERS 1
#define USE_MK_TIMER_TOO 1
#else
#define USE_DISPATCH_SOURCE_FOR_TIMERS 0
#define USE_MK_TIMER_TOO 1
#endif

__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);

    // 处理自定义RunLoop blocks
    __CFRunLoopDoBlocks(rl, rlm);
    // 处理source0
    Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
    if (sourceHandledThisLoop) {
        // 处理自定义RunLoop blocks
        __CFRunLoopDoBlocks(rl, rlm);
    }

    Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
    // 判断是否有source1
    if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
        msg = (mach_msg_header_t *)msg_buffer;
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            goto handle_msg;
        }
    }

    didDispatchPortLastTime = false;

    // 通知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 (MACH_PORT_NULL == livePort) {
        CFRUNLOOP_WAKEUP_FOR_NOTHING();
        // handle nothing
    } else if (livePort == rl->_wakeUpPort) {
        CFRUNLOOP_WAKEUP_FOR_WAKEUP();
        // do nothing on Mac OS
    }
    // 定时器唤醒
    else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
        __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()))
    }
    // gcd唤醒
    else if (livePort == dispatchPort) {
        // 处理gcd
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
    } else {
    // source1唤醒
    // 处理source1
        CFRUNLOOP_WAKEUP_FOR_SOURCE();
    }
    // 处理自定义RunLoop blocks
    __CFRunLoopDoBlocks(rl, rlm);
        
    // 确定下一步是否需要退出RunLoop
    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)) {
        // source/timer/observer一个都没有了
        retVal = kCFRunLoopRunFinished;
    }
    } while (0 == retVal);

    return retVal;
}

CFRunLoop的操作最终都会调用一系列全大写命名的函数,在CFRunLoop.c文件中有具体定义。

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
// ...

gcd一般不依赖于RunLoop,但是gcd切换回主线程执行的任务将会交给RunLoop执行。

DispatchQueue.main.async {
    print("123")
}

可以看到主队列任务由RunLoop执行。

__CFRunLoopServiceMachPort内部会调用内核函数mach_msg()使得线程进入休眠状态。

用户点击手机屏幕的处理过程

事件传递链和响应链深度分析:https://www.jianshu.com/p/f9967f1a0ac1

App外:用户点击->硬件响应->参数量化->数据转发->App接收

当一个硬件事件(触摸/锁屏/摇晃等)发生之后,屏幕硬件会接受用户的操作,并采集关键的参数传递给IOKit.framework,而IOKit将这些数据打包成IOHIDEvent,并传给SpringBoard.app,继而通过mach port转发给前台App。

App内:子线程接收事件(source1)->主线程封装事件(source0)->UIWindow启动hitTest确定目标视图(事件传递链)->UIApplication开始发送事件(事件响应链)->touch事件开始回调

子线程接收事件:App启动时便会启动一个com.apple.uikit.eventfetch-thread子线程,负责接收SpringBoard.app转发过来的数据。
eventfetch-thread在RunLoop注册了一个source1,回调方法是__IOHIDEventSystemClientQueueCallback。
SpringBoard会利用mach port,产生source1,来唤醒目标APP的com.apple.uikit.eventfetch-thread的RunLoop,并触发__IOHIDEventSystemClientQueueCallback回调。
__IOHIDEventSystemClientQueueCallback内部会将main RunLoop 中__handleEventQueue所对应的source0设置为signalled == YES状态,同时唤醒main RunLoop。mainRunLoop则调用__handleEventQueue进行事件队列处理。

主线程封装事件:
主线程同样在启动时监听source0,回调方法是__handleEventQueue。
__handleEventQueue会调用 _UIApplicationHandleEventQueue() ,_UIApplicationHandleEventQueue()接收eventfetch-thread线程发送的IOHIDEvent数据,再封装成UIEvent,根据UIEvent的类型判断是否需要启动hitTest。motion事件不需要hitTest,touch事件也有部分不需要hitTest,比如说touch结束触发的事件。

接下来是事件传递链的过程,寻找响应事件的目标视图。

确定目标视图之后,就会创建UITouch,UIApplication将UITouch和UIEvent发送给目标视图,触发其touches系列的方法。

手势识别

当时间传递链找到目标视图后,会开始手势识别。当手势识别成功之后,默认会cancel后续touch操作,从目标视图开始的响应链都会收到touchesCancelled方法,而不是正常的touchesEnded方法。

也可以通过设置下面的cancelsTouchesInView=NO来避免触发touchesCancelled方法。

随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并向APP的event queue中投递一个gesture event。RunLoop处理source0时,最终会回调到我们自己所写的gesture回调中。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

UIButton

UIButton是UIControl的子类,通过追踪touch事件的变化得到一些UIControl定义的事件(UIControlEvents);UIButton的点击操作是通过UIControlEvents的事件变化回调来触发,本质依赖的是响应链回调过程中的touches系列方法。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并通过CATransaction提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并提交到 GPU 去显示;如果此处有动画,Core Animation 会通过 DisplayLink 等机制多次触发相关流程。

定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。

线程保活

比如子线程内请求一个网络数据,但是等待完成需要等一会,这就会导致得到数据的时候子线程就被回收掉了。要想子线程一直存在,我们可以利用RunLoop,在子线程写下如下代码就可以使得线程保活。

@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}

添加一个mach port防止RunLoop主动退出。

AFN

AFNetworking早期版本使用NSURLConnection发送网络请求,NSURLConnection的特点是每个请求都需要重新创建。AFNetworking维护了一个常驻线程来负责请求的发起与接收,避免频发开辟线程。
AFN参考了MVCNetworking : https://developer.apple.com/library/archive/samplecode/MVCNetworking/Introduction/Intro.html

[self performSelector:@selector(operationDidStart) 
              onThread:[[self class] networkRequestThread]
            withObject:nil 
         waitUntilDone:NO 
                 modes:[self.runLoopModes allObjects]]
卡顿监控

卡顿监控的本质是通过监测主线程Runloop执行loop(未进入休眠)的时长判断是否存在卡顿。当执行loop的时间过长时,将会导致主线程卡住,无法及时响应触摸等事件。

常见的实现方式是在子线程通过 while循环+信号量 来保证子线程的存活,
并通过信号量的超时机制,连续一定次数超时则认为存在卡顿。

// 信号
semaphore = dispatch_semaphore_create(0);
// 注册RunLoop状态观察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                               kCFRunLoopAllActivities,
                               YES,
                               0,
                               &runLoopObserverCallBack,
                               &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    PerformanceMonitor *moniotr = (__bridge PerformanceMonitor*)info;
    
    moniotr->activity = activity;
    
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    // RunLoop状态变化释放信号量
    dispatch_semaphore_signal(semaphore);
}
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    while (YES)
    {
        // RunLoop状态变化或超时获取信号量
        long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
        // 超时
        if (st != 0)
        {
            // 当RunLoop状态在kCFRunLoopBeforeSources或kCFRunLoopAfterWaiting停留了超过250ms认为存在卡顿(未进入休眠kCFRunLoopBeforeWaiting)
            if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
            {
                if (++timeoutCount < 5)
                    continue;
                
                NSLog(@"卡顿");
            }
        }
        timeoutCount = 0;
    }
});
性能优化

案例:tableView的Cell中有多个ImageView,同时加载大图,导致UI卡顿。
解决思路:使用Runloop每次循环址添加一张图片。

1、把加载图片等代码保存起来,先不执行 (保存一段代码,block)
2、监听Runloop循环(CFRunloopObserver)
3、每次都从任务数组中取出一个加载图片等代码执行(执行block代码)
通过以下代码,监听即将进入休眠kCFRunLoopBeforeWaiting。

defaultModeObserver = CFRunLoopObserverCreate(NULL,
                                              kCFRunLoopBeforeWaiting, YES,
                                              NSIntegerMax - 999,
                                              &Callback,
                                              &context);

//添加当前runloop的观察着
CFRunLoopAddObserver(current, defaultModeObserver, kCFRunLoopDefaultMode);
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    //如果没有任务,就直接返回
    if (tasks.count == 0) {
        return;
    }
    
    BOOL result = NO;
    while (result == NO && tasks.count) {
        //取出任务
        RunloopBlock unit = tasks.firstObject;
        //执行任务
        result = unit();
        //删除任务
        [tasks removeObjectAtIndex:0];
    }
}

通过定时器唤醒RunLoop,避免RunLoop一直处于休眠。

[NSTimer scheduledTimerWithTimeInterval:0.01 repeats:YES block:^(NSTimer * _Nonnull timer) { 
    if (tasks.count == 0) {
        [timer invalidate];
    }
}];

Texture(AsyncDisplayKit)

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成。Texture(AsyncDisplayKit)所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

Texture模拟了RunLoop的CA界面更新机制:所有针对ASNode的修改和提交,总有些任务是必须放入主线程执行的。当出现这种任务时,ASNode会把任务用ASAsyncTransaction(Group)封装并提交到一个全局的容器去。Texture也在Runloop中注册了一个Observer,监视的事件和CA一样,但优先级比CA要低。当Runloop进入休眠前、CA处理完事件后,Texture就会执行该loop内提交的所有任务。通过这种机制,Texture可以在合适的机会把异步、并发的操作同步到主线程去,并且能获得不错的性能。
Texture监听RunLoop的kCFRunLoopBeforeWaiting状态实现代码:https://github.com/TextureGroup/Texture/blob/b7cd0b16567a9eb10e58f4cc0886a145dc5273b8/Source/Details/Transactions/_ASAsyncTransactionGroup.m

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • RunLoop源码剖析---图解RunLoop 源码面前,了无秘密 前言 我们在iOS APP中的main函数如下...
    祀梦_阅读 893评论 0 13
  • 注:本篇博客只在 ibireme 的 深入理解RunLoop 基础上做了点方便自己复习该知识点的修改,能力有限,如...
    Cooci_和谐学习_不急不躁阅读 719评论 3 8
  • Runloop 是和线程紧密相关的一个基础组件,是很多线程有关功能的幕后功臣。尽管在平常使用中几乎不太会直接用到,...
    jackyshan阅读 9,854评论 10 75
  • 每次面试,Runloop这个概念几乎是必问的。所以,还是写点东西出来做个记录,同时也加深一下自己的记忆。 一.什么...
    码农老张阅读 1,632评论 0 4
  • RunLoop源码剖析---图解RunLoop 源码面前,了无秘密 前言 我们在iOS APP中的main函数如下...
    萨缪阅读 1,411评论 0 7