preface

When we optimize performance at ordinary times, the main thread processing time-consuming tasks are basically can move to the child thread move, can’t move the big task, can tear open come out to the child thread is open, but there are always some unable to break large, time-consuming tasks, in order to avoid the user action in caton feeling, we can take full advantage of the characteristics of runloop, To place the task to execute when runloop is not busy (i.e. the moment the user stops).

Therefore, using Runloop, you can reasonably reduce the priority of large tasks and execute them when Runloop is about to sleep to ensure smooth user UI operation

Note: If the user does not feel stuck in the operation process, there is no need to blindly use this optimization scheme, which will greatly reduce the experience of quick browsing applications, such as news. However, for some pages, the processing logic is very large, and there will be obvious lag without optimization, so this scheme can be used to solve the problem, such as: You can use this solution to view large images, giFs, and videos

The implementation code uses two scenarios for processing tasks, with different orients and relatively simple logic, as explained step-by-step below

The source address

Runloop profile

As mentioned earlier, runloop has multiple modes, that is, multiple different operating modes. It can only run in one mode at a time, and the effect of state switching is achieved through mode switching, as shown below

NSDefaultRunLoopMode

NSConnectionReplyMode

NSModalPanelRunLoopMode

NSEventTrackingRunLoopMode

NSRunLoopCommonModes

The last NSRunLoopCommonModes is actually a set of all modes, that is, the code with NSRunLoopCommonModes set can be executed in all modes

If you want to minimize the events during user operations, you can run the task in NSDefaultRunLoopMode. When the user stops the operation, the task will be switched to this mode

In addition, if you review the flow diagram of runloop running, you can clearly see the invocation of the Observer

Then look at the observer types that the Runloop code enumeration gives you to listen on

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),           // About to enter Loop
    kCFRunLoopBeforeTimers = (1UL << 1),    // Timer is about to be processed
    kCFRunLoopBeforeSources = (1UL << 2),   // Source is about to be processed
    kCFRunLoopBeforeWaiting = (1UL << 5),   // About to go to sleep
    kCFRunLoopAfterWaiting = (1UL << 6),    // Just woke up from hibernation
    kCFRunLoopExit = (1UL << 7),            // About to exit Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   // All states change
};
Copy the code

The actual mode we use is NSDefaultRunLoopMode. When the user slides, this mode will automatically cut out and suspend. When the user stops the operation interface (scroll view), it will resume the execution in defaultMode. When reverting to defaultModel, if no task is executed, runloop will go to sleep and the Observer that was set to listen in defaultMode will be executed

Implementation Logic introduction

There are two types of implementation logic:

Runloop is about to go to sleep, and all tasks are completed at once.

Second, use hash table according to the key value to heavy, when runloop is about to enter dormancy, the child thread awakens the tasks one by one to the queue, when the end user switching operation task execution, and the rest of the next round of task execution, implementation process, found that the effect is general, due to the processing of large task can only be run in the land of the home team, the main thread will still be in caton operation, In this case, time slice management must be carried out for the task, that is, when a task is executed and hibernated for a period of time, the actual execution efficiency is general). Therefore, the design of scheme 2 is more suitable for adding tasks, and the operation of executing tasks in serial while waiting for hibernation can be used for calculation or statistics, database information query and other scenarios, which will be introduced later

Solution one source code introduction (large picture load recommended this solution)

This solution is the LSRunloopTaskManager file in the source code

Declare a mapTable and Oberver to facilitate the preservation of the content and the establishment of the observer, and NSMapTable can set the weak reference release object, avoiding the maintenance of the content

{
    NSMapTable *_mapTable;
    CFRunLoopObserverRef _observer;
}
- (instancetype)init
{
    self = [super init];
    if (self) {
        _mapTable = [NSMapTable mapTableWithKeyOptions:
            NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality 
            valueOptions:NSPointerFunctionsStrongMemory];
    }
    return self;
}
Copy the code

There are two use cases of global singletons and weak reference singletons. One is suitable for global singletons and the other is suitable for certain scenarios. After use, the interface will exit and release

+ (instancetype)sharedInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

+ (instancetype)weakSingleInstance {
    static __weak LSRunloopTaskManager *weakInstance = nil;
    __strong id strongInstance = weakInstance;
    @synchronized (self) {
        if (!weakInstance) {
            strongInstance = [[self alloc] init];
            weakInstance = strongInstance;
        }
    }
    return strongInstance;
}
Copy the code

Join the event and register to remove the Observer

// When starting a register, runloop is always woken up when there are no actual tasks
// While the system optimizes the runloop wake up logic, it avoids excessive performance cost
- (void)setTaskBlock:(void(^) (void))taskBlock forKey:(id)key {
    NSAssert([NSThread isMainThread], @"Tasks must be set in the main thread");
    // Add tasks to mapTable
    [_mapTable setObject:[taskBlock copy] forKey:key];
    if(! _observer) [self registerObserver]; }// The method to execute when runloop is about to sleep
void __runloopTaskCallback(CFRunLoopObserverRef observer, 
    CFRunLoopActivity activity, void *info) {
    // Iterate over all blocks saved by mapTable execution
    NSMapTable *mapTable = [LSRunloopTaskManager sharedInstance]->_mapTable;
    for (id key in mapTable) {
        void (^block)(void) = [mapTable objectForKey:key];
        block();
    }
    // Remove the task after the task is completed
    [mapTable removeAllObjects];
    // After all tasks have been completed, close the Observer to prevent runloop from being constantly woken up and being unable to go to sleep
    [[LSRunloopTaskManager sharedInstance] removeObserver];
}

// Call the callback method every time you are about to enter hibernation
- (void)registerObserver {
    // Register the Observer and set the observation type and callback method
    // The observation type is kCFRunLoopBeforeWaiting, i.e. the loop when runloop is about to go to sleep
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting,
        YES, 0, &__runloopTaskCallback, &context); And put Observer in DefaultMode of main queue to observe CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopDefaultMode); }// When the task is finished, to ensure that the runloop can immediately go to sleep, you need to remove the runloop immediately to avoid wasting power in the background
Runloop is optimized to wake up runloop even if it is not removed in a timely manner. If runloop is executed a certain number of times or if the user has not performed any operation for a period of time, runloop will be hibernated
// But not sure what the condition is, so manual removal is the safest, after all, power saving is also important
- (void)removeObserver {
    if (_observer == NULL) return;
    
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopDefaultMode);
    CFRelease(_observer);
    _observer = NULL;
}
Copy the code

Summary of Scheme 1

This solution is the simplest and most crude runloop tool class for handling large task loads, and it works well

This method is applicable to scenarios where the total task execution time is not long or the number of tasks is small when runloop stops

When this scheme performs UI tasks, tasks will not be executed when sliding, so some experiences will be deficient, which can be optimized in other scenarios. For example, when a video or GIF appears in the list, sliding will display a first picture (small picture) by default, and when it stops running, it will play and place high-definition pictures, etc

Therefore, when using this tool class, try to put the important actions that affect the lag in it. There is no need to put all of them in it, otherwise the experience will be slightly worse

This program is also a small tool recommended at present

Scheme two source code introduction

Scheme two introduces another task processing means, is also the use of runloop load picture, the execution of the task is indeed one by one, so that the user interrupted in time, but the more convenient the user, the more efficiency back to the poor, the source code design is introduced below

This plan is LSRunloopTaskAsyncManager of source files

Register and remove Observer. Different from Scheme 1, kCFRunLoopBeforeSources is added during registration so that users can stop the execution of subsequent tasks in time

// Task execution starts when the task is going to sleep when the state is switched to kCFRunLoopBeforeWaiting, otherwise set task execution state to false
// Stop performing subsequent tasks
void __runloopTaskExCallback(CFRunLoopObserverRef observer, 
    CFRunLoopActivity activity, void *info) {
    if (activity == kCFRunLoopBeforeWaiting) {
        [[LSRunloopTaskAsyncManager sharedInstance] executeTasks];
    }else {
        [LSRunloopTaskAsyncManager sharedInstance]->_canExecute = false; }}// Call the callback method every time you are about to enter hibernation
- (void)registerObserver {
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, 
        kCFRunLoopBeforeWaiting | kCFRunLoopBeforeSources, 
        YES, 0, &__runloopTaskExCallback, &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopDefaultMode);
}

/ / remove the observer
- (void)removeObserver {
    if (_observer == NULL) return;
    
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopDefaultMode);
    CFRelease(_observer);
    _observer = NULL;
}
Copy the code

Set up the task

// Start adding tasks to the taskMap, add listeners when no listener is added, and set whether to delete tasks after completion
// taskMap is a bidirectional list of tasks that are executed first and then executed last
- (void)setAsyncTaskBlock:(void(^) (void))taskBlock forKey:(id)key {
    [_taskMap setTaskBlock:taskBlock forKey:key executeLeave:YES];
    if(! _observer) [self registerObserver]; } - (void)setAsyncTaskBlock:(void(^) (void))taskBlock 
        forKey:(id)key executeLeave:(BOOL)executeLeave {
    [_taskMap setTaskBlock:taskBlock forKey:key executeLeave:executeLeave];
    if(! _observer) [self registerObserver]; }Copy the code

Sub-queue task execution method, sub-queue one by one to execute tasks, this is the more practical scenario of this scheme

// A method to execute tasks, which are executed one by one in child threads
_canExecute is set to false when the user starts the operation
- (void)executeTasks {
    dispatch_async(_queue, ^{
        self->_canExecute = true;
        while (self->_canExecute) {
            if ([self->_taskMap executeBlock]) {
                self->_canExecute = false; [self removeObserver]; }}}); }Copy the code

UI operation task execution method, child queues finishing task, one by one in the main thread of execution, the execution time segments of dormancy, due to the home as a serial queue, so you need to flow time slice to the user operation, otherwise the performance and operation will only inferior to plan a (this plan if there is a better solution, welcome everybody to discuss)

- (void)executeTasks {
    if (_canExecute) return;
    
    dispatch_async(_queue, ^{
        self->_canExecute = true;
        CFTimeInterval lastInterval = CACurrentMediaTime();
        while (self->_canExecute) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([self->_taskMap executeBlock]) {
                    self->_canExecute = false;
                    [self removeObserver];
                }
                dispatch_semaphore_signal(self->_semaphore);
            });
            dispatch_semaphore_wait(self->_semaphore, DISPATCH_TIME_FOREVER);
            // The task execution process will be stuck in the main thread, the actual experience is still general, but still ok
            CFTimeInterval interval = CACurrentMediaTime();
            if (interval - lastInterval > 0.05) {
                [NSThread sleepForTimeInterval:0.05]; lastInterval = interval; }}}); }Copy the code

Summary of Plan 2

This solution is an enhanced version of the Runloop loading utility class. It is best used for low-priority scenarios that can be moved to sub-queues, such as local data preloading, cache clearing, and local data updating. These tasks can be performed for a long time, as long as the Runloop is in hibernation

In addition, this scheme is adapted to add time slice operation, which can also realize the same task loading in the main queue as the scheme 1. The defects and advantages are equally obvious, the advantage is that the task can be interrupted in time to avoid user operation lag when there are too many tasks. The disadvantage is that the execution efficiency is relatively low, and sometimes the task will still lag

Therefore, the function of loading the main queue task in scheme 2 is not perfect and needs to be developed. Welcome to discuss

LSTaskMap

LSTaskMap is a data structure derived from scheme 2, which imitated part of the data structure of YYCache and prepared a task queue composed of two-way list and hash table to ensure that the task can be accessed quickly, add and delete, update the task. Here only the code is affixed, not much introduction

// The basic task node, which currently only supports blocks, avoids additional references to object selectors and parameters
@interface LSTaskNode : NSObject
{
@package
    id _key;
    id (^_block)(void);
    BOOL _executeLeave; // Whether the queue is completed
    // Avoid release issues, it will not be released during the LSTaskMap release period, so it can be used safely
    __unsafe_unretained LSTaskNode *_preNode; 
    __unsafe_unretained LSTaskNode *_nextNode;
}

@end

@implementation LSTaskNode


@end

// The map maintained by the task consists of a bidirectional linked list and a hash table
@interface LSTaskMap : NSObject
{
    NSMapTable<id, LSTaskNode *> *_mapTable; // It is mainly used for quick lookup, de-duplication, and avoiding key value maintenance
    
    LSTaskNode *_headNode; // header -- end in and head out
    LSTaskNode *_tailNode; / / end nodes
}
// First to execute
// Set the task Block to the end of the queue, and eliminate tasks that update duplicate keys
- (void)setTaskBlock:(void(^) (void))taskBlock forKey:(id)key executeLeave:(BOOL)executeLeave;
// The first team mission leaves
- (void)leaveTask;
// Execute a task, select whether to leave the queue according to the task type, return whether the queue is empty
- (BOOL)executeBlock;
// Get the element
- (LSTaskNode *)taskForKey:(id)key;
// Remove an element
- (void)removeTaskForKey:(id)key;
// Remove all elements
- (void)removeAllTask;

@end

@implementation LSTaskMap

- (instancetype)init
{
    self = [super init];
    if (self) {
        _mapTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory 
            | NSPointerFunctionsObjectPersonality valueOptions:NSPointerFunctionsStrongMemory];

        _headNode = nil;
        _tailNode = nil;
    }
    return self;
}

// Set the task
- (void)setTaskBlock:(void(^) (void))taskBlock forKey:(id)key executeLeave:(BOOL)executeLeave {
    LSTaskNode *node = [_mapTable objectForKey:key];
    if (node) {
        // Move to the end
        node->_block = [taskBlock copy];
        node->_executeLeave = executeLeave;
        if (node == _tailNode) return;
        if (node == _headNode) {
            _headNode = node->_nextNode;
            _tailNode->_nextNode = node;
            node->_preNode = _tailNode;
            _tailNode = node;
            node->_nextNode = nil;
        }else{ node->_preNode->_nextNode = node->_nextNode; node->_nextNode->_preNode = node->_preNode; node->_preNode = _tailNode; _tailNode->_nextNode = node; _tailNode = node; node->_nextNode = nil; }}else {
        node = [LSTaskNode new];
        node->_key = key;
        node->_block = [taskBlock copy];
        node->_executeLeave = executeLeave;
        node->_preNode = nil;
        node->_nextNode = nil;
        
        // If the head exists, the tail exists
        if(! _headNode) { _headNode = node; _tailNode = node; }else{ LSTaskNode *lastNode = _tailNode; node->_preNode = lastNode; lastNode->_nextNode = node; _tailNode = node; } [_mapTable setObject:node forKey:key]; }}// The first element of the team leaves
- (void)leaveTask {
    if(! _headNode)return;
    
    if (_headNode == _tailNode) {
        // There is only one element
        [_mapTable removeAllObjects];
        _headNode = nil;
        _tailNode = nil;
    }else{ [_mapTable removeObjectForKey:_headNode->_key]; _headNode = _headNode->_nextNode; _headNode->_preNode = nil; }}// Execute a task, select whether to leave the queue according to the task type, return whether the queue is empty
- (BOOL)executeBlock {
    if(! _headNode)return true;
    _headNode->_block();

    if (_headNode->_executeLeave) {
        if (_headNode == _tailNode) {
            // There is only one element
            [_mapTable removeAllObjects];
            _headNode = nil;
            _tailNode = nil;
            
            return true;
        }else {
            [_mapTable removeObjectForKey:_headNode->_key];
            _headNode = _headNode->_nextNode;
            _headNode->_preNode = nil;
            
            return false; }}else {
        return _headNode == _tailNode;
    }
}

- (LSTaskNode *)taskForKey:(id)key {
    return [_mapTable objectForKey:key];
}

- (void)removeTaskForKey:(id)key {
    LSTaskNode *node = [_mapTable objectForKey:key];
    if(! node)return;
    
    if (_headNode == _tailNode) {
        [_mapTable removeAllObjects];
        _headNode = nil;
        _tailNode = nil;
        return;
    }
    if (node == _headNode) {
        _headNode = node->_nextNode;
        _headNode->_preNode = nil;
    }else if (node == _tailNode) {
        _tailNode = node->_preNode;
        _tailNode->_nextNode = nil;
    }else {
        node->_nextNode->_preNode = node->_preNode;
        node->_preNode->_nextNode = node->_nextNode;
    }
    [_mapTable removeObjectForKey:key];
}

- (void)removeAllTask {
    [_mapTable removeAllObjects];
    _headNode = nil;
    _tailNode = nil;
}
Copy the code

The last

The Runloop loading task is just one direction of optimization that has yet to be developed

If the UI operations such as large images, GIfs and videos used in normal development are combined with the use of key (key can be any object type), the number of tasks will not be much, and scheme 1 is preferred for implementation, which is simple and pollution-free

If there are long-running tasks that need to be performed while Runloop is asleep and can be performed in the background (for example, partial data disk writes and updates, user behavior statistics uploads, etc.), scheme 2 is a good tool class