CFRunLoopThe concept of

In simple terms, the CFRunLoop object is responsible for monitoring the event input source and managing its distribution. The types managed by CFRunLoop are generally classified into sources(CFRunLoopSource), timers(CFRunLoopTimer), and observers(CFRunLoopObserver).

CFRunLoopThe use of

1. CFRunLoopSource

CFRunLoopSourceRef is where the event is generated. Source includes Source0 and Source1.

Source0: Primarily managed by the application, it does not actively trigger events. To use this, you need to call CFRunLoopSourceSignal(source), mark the source as pending, and then manually call CFRunLoopWakeUp(runloop) to wake up the Runloop to handle the event. Usually we use the Source0 event as well.

Source1: mainly due to RunLoop and kernel management. Contains a mach_port and a callback (function pointer) that is used to send messages to each other through the kernel and other threads. This Source actively wakes up the RunLoop thread.

- (void)cfSource {
    // Create context
    CFRunLoopSourceContext context = {};
    context.perform = runLoopSourceCallback;
    context.info = (__bridge void *)self;
    
    / / create the source
    CFRunLoopRef runLoop = CFRunLoopGetCurrent(a);CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    
    / / add the source
    CFRunLoopAddSource(runLoop, source, kCFRunLoopCommonModes);
    
    NSLog(@"create source in %@"[NSDate date]);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // Execute source-related events
        CFRunLoopSourceSignal(source);
        CFRunLoopWakeUp(runLoop);
        CFRelease(source);
    });
}

/ / source correction
static void runLoopSourceCallback(void *info) {
    NSLog(@"reiceive source in %@"[NSDate date]);
}

// Execution result
create source in Wed Mar 25 16:22:08 2020
reiceive source in Wed Mar 25 16:22:11 2020
Copy the code

The CFRunLoopSourceSignal method must be called to mark the event as “pending”. CFRunLoopWakeUp can be called as required. If RunLoop is currently running, It is also OK not to call, but to avoid the possibility that the current RunLoop is dormant, it is best to add.

2. CFRunLoopTimerRef

CFRunLoopTimerRef is a time-based trigger. Similar to NSTimer, you can perform scheduled tasks.

CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, // Use kCFAllocatorDefault to allocate memory
                                       CFAbsoluteTime fireDate,  // The time when the call was first triggered
                                       CFTimeInterval interval,  // Callback interval
                                       CFOptionFlags flags,      // Apple backup parameter, pass 0
                                       CFIndex order,            // RunLoop executes the priority of the event. It is useless for the Timer
                                       CFRunLoopTimerCallBack callout, / / callback callback
                                       CFRunLoopTimerContext *context); // The context used to contact callback
Copy the code
- (void)cfTimer {
    self.timerCount = 0;
    // Create context
    CFRunLoopTimerContext context = {};
    context.info = (__bridge void*)self; // Pass the current object as an argument
    
    CFRunLoopRef runloop = CFRunLoopGetCurrent(a);CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
                                                   CFAbsoluteTimeGetCurrent() + 1.// Set the time after the first callback to 1s
                                                   3.// Callback interval
                                                   0.0, &timerFiredCallback, &context);
    // Set the runtime error range
    CFRunLoopTimerSetTolerance(timer, 0.1);
    CFRunLoopAddTimer(runloop, timer, kCFRunLoopCommonModes);
    CFRelease(timer);
    NSLog(@"start timer in %@"[NSDate date]);
}

static void timerFiredCallback(CFRunLoopTimerRef timer, void *info) {
    ViewController *controller = (__bridge ViewController *)info;
    NSLog(@"recieve timer event with count: %@, in %@", @(controller.timerCount), [NSDate date]);
    if (++controller.timerCount == 5) {
        CFRunLoopTimerInvalidate(timer); // Turn off the timer}}Copy the code
// Result Start timerin Wed Mar 25 16:42:59 2020
recieve timer event with count: 0, in Wed Mar 25 16:43:00 2020
recieve timer event with count: 1, in Wed Mar 25 16:43:03 2020
recieve timer event with count: 2, in Wed Mar 25 16:43:06 2020
recieve timer event with count: 3, in Wed Mar 25 16:43:09 2020
recieve timer event with count: 4, in Wed Mar 25 16:43:12 2020
Copy the code

According to the result, you can see that the first callback is after 1s, and the rest of the callback is every 3s.

3. CFRunLoopObserverRef

CFRunLoopObserverRef: Observer. It is used to observe the status of the RunLoop and perform operations in different states. Can pass CFRunLoopObserverCreateWithHandler and CFRunLoopObserverCreate methods create observation object, the former block, by the callback, the latter is by C function callback methods.

  • The state sets
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), // About to exit Loop};Copy the code
  • By listeningRunLoopthekCFRunLoopBeforeWaitingandkCFRunLoopExitState, callback processing related operations to achieve utilizationRunLoopDo some extra operations in the idle state.
- (void)runBlockWhenMainThreadIdle {
    __weak typeof(self) wSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"start submit block in %@"[NSDate date]);
        [wSelf runWithBlock:^{
            NSLog(@"finish block when main thread is idle in %@"[NSDate date]);
        }];
    });
}

- (void)runWithBlock:(void(^) (void))block {
    CFRunLoopActivity flag = kCFRunLoopBeforeWaiting | kCFRunLoopExit; // Listen for RunLoop to sleep and exit
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, flag, YES.0And ^ (CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        // Callback operation
        if (block) {
            block();
        }
        // Remove related listeners
        CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        CFRelease(observer);
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
Copy the code

Application scenarios of third-party libraries

1. YYTransaction

Perform layout updates by listening for the RunLoop to go to sleep and end.

/// Update layout and selection before runloop sleep/end.
- (void)_commitUpdate {
#if ! TARGET_INTERFACE_BUILDER
    _state.needUpdate = YES;
    [[YYTransaction transactionWithTarget:self selector:@selector(_updateIfNeeded)] commit];
#else
    [self _update];
#endif
}
Copy the code
@interface YYTransaction(a)
@property (nonatomic.strong) id target;
@property (nonatomic.assign) SEL selector;
@end

static NSMutableSet *transactionSet = nil;

static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (transactionSet.count == 0) return;
    NSSet *currentSet = transactionSet;
    transactionSet = [NSMutableSet new];
    [currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
    }];
}

static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain(a);CFRunLoopObserverRef observer;
        
        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                           true.// repeat
                                           0xFFFFFF.// after CATransaction(2000000)
                                           YYRunLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}


@implementation YYTransaction

+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{
    if(! target || ! selector)return nil;
    YYTransaction *t = [YYTransaction new];
    t.target = target;
    t.selector = selector;
    return t;
}

- (void)commit {
    if(! _target || ! _selector)return;
    YYTransactionSetup();
    [transactionSet addObject:self];
}

- (NSUInteger)hash {
    long v1 = (long) ((void *)_selector);
    long v2 = (long)_target;
    return v1 ^ v2;
}

- (BOOL)isEqual:(id)object {
    if (self == object) return YES;
    if(! [object isMemberOfClass:self.class]) return NO;
    YYTransaction *other = object;
    return other.selector == _selector && other.target == _target;
}
@end
Copy the code

2. NSThread+Add

Manually add automatic release pools for child threads. Through monitoring the RunLoop kCFRunLoopEntry state, to guarantee the execution before insert NSAutoreleasePool, then through listening kCFRunLoopBeforeWaiting | kCFRunLoopExit state, When the RunLoop goes to sleep or ends, release the related object.

static NSString *const YYNSThreadAutoleasePoolKey = @"YYNSThreadAutoleasePoolKey";
static NSString *const YYNSThreadAutoleasePoolStackKey = @"YYNSThreadAutoleasePoolStackKey";

static const void *PoolStackRetainCallBack(CFAllocatorRef allocator, const void *value) {
    return value;
}

static void PoolStackReleaseCallBack(CFAllocatorRef allocator, const void *value) {
    CFRelease((CFTypeRef)value);
}


static inline void YYAutoreleasePoolPush() {
    NSMutableDictionary *dic =  [NSThread currentThread].threadDictionary;
    NSMutableArray *poolStack = dic[YYNSThreadAutoleasePoolStackKey];
    
    if(! poolStack) {/* do not retain pool on push, but release on pop to avoid memory analyze warning */
        CFArrayCallBacks callbacks = {0};
        callbacks.retain = PoolStackRetainCallBack;
        callbacks.release = PoolStackReleaseCallBack;
        poolStack = (id)CFArrayCreateMutable(CFAllocatorGetDefault(), 0, &callbacks);
        dic[YYNSThreadAutoleasePoolStackKey] = poolStack;
        CFRelease(poolStack);
    }
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // create
    [poolStack addObject:pool]; // push
}

static inline void YYAutoreleasePoolPop() {
    NSMutableDictionary *dic =  [NSThread currentThread].threadDictionary;
    NSMutableArray *poolStack = dic[YYNSThreadAutoleasePoolStackKey];
    [poolStack removeLastObject]; // pop
}

static void YYRunLoopAutoreleasePoolObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    switch (activity) {
        case kCFRunLoopEntry: {
            YYAutoreleasePoolPush();
        } break;
        case kCFRunLoopBeforeWaiting: {
            YYAutoreleasePoolPop();
            YYAutoreleasePoolPush();
        } break;
        case kCFRunLoopExit: {
            YYAutoreleasePoolPop();
        } break;
        default: break; }}static void YYRunloopAutoreleasePoolSetup() {
    CFRunLoopRef runloop = CFRunLoopGetCurrent(a);CFRunLoopObserverRef pushObserver;
    pushObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopEntry,
                                           true.// repeat
                                           -0x7FFFFFFF.// before other observers
                                           YYRunLoopAutoreleasePoolObserverCallBack, NULL);
    CFRunLoopAddObserver(runloop, pushObserver, kCFRunLoopCommonModes);
    CFRelease(pushObserver);
    
    CFRunLoopObserverRef popObserver;
    popObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                          true.// repeat
                                          0x7FFFFFFF.// after other observers
                                          YYRunLoopAutoreleasePoolObserverCallBack, NULL);
    CFRunLoopAddObserver(runloop, popObserver, kCFRunLoopCommonModes);
    CFRelease(popObserver);
}

@implementation NSThread (YYAdd)

+ (void)addAutoreleasePoolToCurrentRunloop {
    if ([NSThread isMainThread]) return; // The main thread already has autorelease pool.
    NSThread *thread = [self currentThread];
    if(! thread)return;
    if (thread.threadDictionary[YYNSThreadAutoleasePoolKey]) return; // already added
    YYRunloopAutoreleasePoolSetup();
    thread.threadDictionary[YYNSThreadAutoleasePoolKey] = YYNSThreadAutoleasePoolKey; // mark the state
}

@end
Copy the code

It is also worth noting that releasepool is stored in threadDictionary, a thread’s private space. In addition, regarding whether operations in child threads need to be released manually, please refer to the memory management of the Autorelease object of each iOS thread. Personally, I think it is better to add, after all, the official documentation does not explicitly state that child threads do not need to add.

3. SMLagMonitor

RunLoop call methods are mainly focused on the kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting states. A child thread can be opened to calculate the time between the two states in real time to see if a certain threshold is exceeded, so as to determine whether the main thread is running late.

@interface SMLagMonitor(a){
    int timeoutCount;
    CFRunLoopObserverRef runLoopObserver;
    @public
    dispatch_semaphore_t dispatchSemaphore;
    CFRunLoopActivity runLoopActivity;
}
@end

@implementation SMLagMonitor

#pragma mark - Interface
+ (instancetype)shareInstance {
    static id instance = nil;
    static dispatch_once_t dispatchOnce;
    dispatch_once(&dispatchOnce, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (void)beginMonitor {
    self.isMonitoring = YES;
    // Monitor the lag
    if (runLoopObserver) {
        return;
    }
    dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore to ensure synchronization
    // Create an observer
    CFRunLoopObserverContext context = {0,(__bridge void*)self.NULL.NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES.0,
                                              &runLoopObserverCallBack,
                                              &context);
    // Add the observer to the common mode of the main thread runloop
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    
    // Create a child thread monitor
    dispatch_async(dispatch_get_global_queue(0.0), ^ {// The child thread opens a continuous loop for monitoring
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, STUCKMONITORRATE * NSEC_PER_MSEC));
            if(semaphoreWait ! =0) {
                if(! runLoopObserver) { timeoutCount =0;
                    dispatchSemaphore = 0;
                    runLoopActivity = 0;
                    return;
                }
                // The state of two runloops, BeforeSources and AfterWaiting, can be used to detect whether they are stuck or not
                if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                    // the result is generated three times
                    if (++timeoutCount < 3) {
                        continue;
                    }
// NSLog(@"monitor trigger");
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {NSString *stackStr = [SMCallStack callStackWithType:SMCallStackTypeMain];
                        SMCallStackModel *model = [[SMCallStackModel alloc] init];
                        model.stackStr = stackStr;
                        model.isStuck = YES;
                        [[[SMLagDB shareInstance] increaseWithStackModel:model] subscribeNext:^(id x) {}];
                    });
                } //end activity
            }// end semaphore wait
            timeoutCount = 0;
        }// end while
    });
    
}

- (void)endMonitor {
    self.isMonitoring = NO;
    [self.cpuMonitorTimer invalidate];
    if(! runLoopObserver) {return;
    }
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    CFRelease(runLoopObserver);
    runLoopObserver = NULL;
}

#pragma mark - Private

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
    lagMonitor->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

@end
Copy the code

RunLoop series of articles

  • RunLoop concept and usage
  • Demo

The resources

  • Memory management of iOS Autorelease objects for each thread
  • Summary of iOS caton monitoring scheme
  • Understand RunLoop in depth