This post first appeared on my personal blog


In terms of performance optimization, I wrote about iOS performance optimization before, and after the optimization, our APP, cold startup, was optimized from 2.7 seconds to 0.6 seconds.

Close RunLoop, write RunLoop detailed source analysis, and detailed RunLoop and multithreading, so how to use RunLoop to monitor performance lag? As we know from iOS performance optimization, App lag simply means that FPS cannot reach 60 frame rate, and the phenomenon of frame loss leads to lag. But a lot of times, we just know we lost frames. It is not clear why frames are missing, so how do we monitor them? First, we need to understand that to find out what the main thread is doing, and thread messages rely on RunLoop, so we can use RunLoop to monitor them.

RunLoop is used to listen for input sources for scheduling processing. If the thread of the RunLoop takes too long to execute the pre-sleep method and is unable to go to sleep, or if the thread wakes up and takes too long to receive messages and is unable to proceed to the next step, the thread is considered blocked. If the thread is the main thread, it will appear to be stuck.

RunLoop and semaphore

We can use the CFRunLoopObserverRef to monitor the state of NSRunLoop, which allows us to obtain changes in these state values in real time.


For details about Runloop, see the runloop source code analysis article. Here’s a quick summary:

  • The state of the 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), // About to exit Loop kCFRunLoopAllActivities = 0x0FFFFFFFU // All state changes};Copy the code
  • CFRunLoopObserverRef usage flow

    1. Set the runtime environment for the Runloop Observer
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
    Copy the code
    1. Create a Runloop Observer object
    The first argument is used to allocate the memory of the Observer. The second argument is used to set the events that the Observer is concerned about. The third argument is used to identify whether the Observer executes on its first entry into the Runloop or on each entry to the Runloop. The fifth argument to set the priority of the Observer: the sixth argument to set the observer's callback function: _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);Copy the code
    1. Add the new observer to the runloop of the current thread
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    Copy the code
    1. Remove the Observer from the runloop of the current thread
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    Copy the code
    1. The release of the observer
    CFRelease(_observer); _observer = NULL;
    Copy the code

A semaphore

See GCD semaphore -dispatch_semaphore_t for details

In short, there are three main functions

dispatch_semaphore_create(long value); // Dispatch_semaphore_signal (dispatch_semaphore_t deem); // Dispatch_semaphore_wait (dispatch_semaphore_t dsema, dispatch_time_t timeout); // Wait for semaphoreCopy the code

dispatch_semaphore_create(long value); This function creates a semaphore of type dispatch_semaphore_ and specifies the size of the semaphore. dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); Wait for the semaphore. If the semaphore value is 0, the function will wait, that is, not return (equivalent to blocking the current thread), until the value of the semaphore it is waiting for is greater than or equal to 1, the function will subtract the semaphore value by one, and then return. dispatch_semaphore_signal(dispatch_semaphore_t deem); Send semaphore. This increments the value of the semaphore by one. Usually the waiting semaphore and the sending semaphore functions come in pairs. When a task is executed concurrently, the dispatch_semaphore_WAIT function is used to wait (block) before the current task is executed. After the last task is executed, the semaphore is sent via dispatch_semaphore_signal function (increment the semaphore value by 1). The dispatch_semaphore_WAIT function determines that the semaphore value is greater than or equal to 1 and then reduces the semaphore value by 1. Then the current task can be executed. The dispatch_semaphore_signal function sends the semaphore (incremented by 1) to notify the next task…… In this way, through semaphores, tasks in concurrent queues are executed synchronously.

Monitor the caton

Principle: By observing the duration of various state changes in the Runloop to detect whether the computation has stalled

The judgment strategy of “N times of delay exceeding threshold T” is adopted, that is, the collection and reporting are triggered only when the accumulated times of delay in a period are greater than N. For example, if the threshold T=500ms and the number of delay N=1, a single valid delay that takes a long time can be considered. However, the stalling threshold T=50ms and stalling times N=5 can be judged as the effective stalling with fast frequency

The main code

// minimum
static const NSInteger MXRMonitorRunloopMinOneStandstillMillisecond = 20;
static const NSInteger MXRMonitorRunloopMinStandstillCount = 1; / / default / / how many milliseconds for more than a caton static const NSInteger MXRMonitorRunloopOneStandstillMillisecond = 50; / / how many caton record for an effective caton static const NSInteger MXRMonitorRunloopStandstillCount = 1; @interfaceYZMonitorRunloop(){ CFRunLoopObserverRef _observer; // Dispatch_semaphore_t _semaphore; // CFRunLoopActivity _activity; } @property (nonatomic, assign) BOOL isCancel; @property (nonatomic, assign) NSInteger countTime; @property (nonatomic, strong) NSMutableArray *backtrace;Copy the code
-(void)registerObserver{ // 1. CFRunLoopObserverContext Context = {0, (__bridge void *)self, NULL, NULL}; // Create a Runloop Observer. // The first parameter is used to allocate memory for the observer. // The second parameter is used to set the events that the observer is concerned with. This parameter specifies whether the observer should be executed on the first entry to runloop or on each entry to runloop processing. // This parameter is used to set the priority of the observer. _Observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); // 3. Add the new observer to the current thread's runloop CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); / / create a signal dispatchSemaphore knowledge reference: _semaphore = dispatch_semaphore_create (0); ////Dispatch Semaphore ensures synchronization __weak __typeof(self) weakSelf = self; // dispatch_queue_t queue = dispatch_queue_create("kadun", NULL); // Dispatch_async (dispatch_get_global_queue(0, 0), ^{// dispatch_async(queue, ^{ __strong __typeof(weakSelf) strongSelf = weakSelf;if(! strongSelf) {return;
        while (YES) {
            if (strongSelf.isCancel) {
                return; } // Wait semaphore: if the semaphore is 0, the current thread is blocked; If the semaphore is greater than 0, this function will carry the semaphore -1 and continue the thread of execution. The timeout period is set tolimitMillisecond milliseconds. // Return value: If the thread is awakened, return non-zero, Long semaphoreWait = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC));if(semaphoreWait ! = 0) {// If the thread of RunLoop enters the pre-sleep method for too long, it cannot go to sleep (kcfrunbefoResources), Or if the thread wakes up and takes too long to receive the message (kCFRunLoopAfterWaiting) to proceed to the next step, it is considered blocked. // The state of two runloops, BeforeSources and AfterWaiting, can be detected to see if they are stuckif (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
                    if (++strongSelf.countTime < strongSelf.standstillCount){
                    [strongSelf logStack];
                    [strongSelf printLogTrace];
                    NSString *backtrace = [YZCallStack yz_backtraceOfMainThread];
                    NSLog(@"+ + + + % @",backtrace);
                    [[YZLogFile sharedInstance] writefile:backtrace];
                    if(strongSelf.callbackWhenStandStill) { strongSelf.callbackWhenStandStill(); } } } strongSelf.countTime = 0; }}); }Copy the code

The demo test

I put the demo on github

When you use it, you just need

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [[YZMonitorRunloop sharedInstance] startMonitor];
    [YZMonitorRunloop sharedInstance].callbackWhenStandStill = ^{
        NSLog(@"Eagle. We have Cotton.");
    return YES;

Copy the code

In the controller, each time you click the screen, sleep for 1 second, as shown below

#import "ViewController.h"@interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ usleep(1 * 1000 * 1000); // 1 second} @endCopy the code

After clicking the screen, print the following

YZMonitorRunLoopDemo [10288-1915706] = = = = = = = = = = detected after caton call stack = = = = = = = = = = ("0 YZMonitorRunLoopDemo 0x00000001022c653c -[YZMonitorRunloop logStack] + 96"."1 YZMonitorRunLoopDemo 0x00000001022c62a0 __36-[YZMonitorRunloop registerObserver]_block_invoke + 484"."2 libdispatch.dylib 0x00000001026ab6f0 _dispatch_call_block_and_release + 24"."3 libdispatch.dylib 0x00000001026acc74 _dispatch_client_callout + 16"."4 libdispatch.dylib 0x00000001026afad4 _dispatch_queue_override_invoke + 876"."5 libdispatch.dylib 0x00000001026bddc8 _dispatch_root_queue_drain + 372"."6 libdispatch.dylib 0x00000001026be7ac _dispatch_worker_thread2 + 156"."7 libsystem_pthread.dylib 0x00000001b534d1b4 _pthread_wqthread + 464"."8 libsystem_pthread.dylib 0x00000001b534fcd4 start_wqthread + 4"

libsystem_kernel.dylib          0x1b52ca400 __semwait_signal + 8
libsystem_c.dylib               0x1b524156c nanosleep + 212
libsystem_c.dylib               0x1b5241444 usleep + 64
YZMonitorRunLoopDemo            0x1022c18dc -[ViewController touchesBegan:withEvent:] + 76
UIKitCore                       0x1e1f4fcdc <redacted> + 336
UIKitCore                       0x1e1f4fb78 <redacted> + 60
UIKitCore                       0x1e1f5e0f8 <redacted> + 1584
UIKitCore                       0x1e1f5f52c <redacted> + 3140
UIKitCore                       0x1e1f3f59c <redacted> + 340
UIKitCore                       0x1e2005714 <redacted> + 1768
UIKitCore                       0x1e2007e40 <redacted> + 4828
UIKitCore                       0x1e2001070 <redacted> + 152
CoreFoundation                  0x1b56bf018 <redacted> + 24
CoreFoundation                  0x1b56bef98 <redacted> + 88
CoreFoundation                  0x1b56be880 <redacted> + 176
CoreFoundation                  0x1b56b97
Copy the code

We’re ready for Caton position

-[ViewController touchesBegan:withEvent:]

The caton log is written locally

It’s monitoring the caton and the call stack. In debug mode, you can view logs directly. If you want to view logs online, you can write logs to the local PC and upload them to the server

Write to the local database

  • Creating a Local Path
	-(NSString *)getLogPath{
    NSArray *paths  = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES);
    NSString *homePath = [paths objectAtIndex:0];
    NSString *filePath = [homePath stringByAppendingPathComponent:@"Caton.log"];
    return filePath;

Copy the code
  • If it is the first time to write, add the device information and mobile phone model information
 NSString *filePath = [self getLogPath];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if(! [fileManager fileExistsAtPath: filePath]) / / if there is no {nsstrings * STR = @"Caton log";
        NSString *systemVersion = [NSString stringWithFormat:@"Mobile version: %@",[YZAppInfoUtil iphoneSystemVersion]];
        NSString *iphoneType = [NSString stringWithFormat:@"Mobile phone Model: %@",[YZAppInfoUtil iphoneType]];
        str = [NSString stringWithFormat:@"%@\n%@\n%@",str,systemVersion,iphoneType];
        [str writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
Copy the code
  • If the local file already exists, determine whether the size is too large and decide whether to write it directly or upload it to the server first
 floatFilesize = 1.0;if ([fileManager fileExistsAtPath:filePath]) {
            NSDictionary *fileDic = [fileManager attributesOfItemAtPath:filePath error:nil];
            unsigned long long size = [[fileDic objectForKey:NSFileSize] longLongValue];
            filesize = 1.0 * size / 1024;
 NSLog(@"Filesize = %lf",filesize);
 NSLog(@"File contents %@",string);
 NSLog(@"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
if(filesize > (self.MAXFileLength > 0 ? Self. MAXFileLength: DefaultMAXLogFileLength)) {/ / uploaded to the server NSLog (@"Upload to server");
       [self update];
       [self clearLocalLogFile];
       [self writeToLocalLogFilePath:filePath contentStr:string];
        NSLog(@"Continue writing to local");
        [self writeToLocalLogFilePath:filePath contentStr:string];
Copy the code

Compressed logs and uploaded them to the server

Because it is text data, we can compress it, beat it to reduce the space occupied, and then upload it. After the upload is successful, delete the local data, and then continue to write data, waiting for the next log

Compression tools

Using SSZipArchive is also very simple to use,

// Unzipping
NSString *zipPath = @"path_to_your_zip_file";
NSString *destinationPath = @"path_to_the_folder_where_you_want_it_unzipped";
[SSZipArchive unzipFileAtPath:zipPath toDestination:destinationPath];
// Zipping
NSString *zippedPath = @"path_where_you_want_the_file_created";
NSArray *inputPaths = [NSArray arrayWithObjects:
                       [[NSBundle mainBundle] pathForResource:@"photo1" ofType:@"jpg"],
                       [[NSBundle mainBundle] pathForResource:@"photo2" ofType:@"jpg"]
[SSZipArchive createZipFileAtPath:zippedPath withFilesAtPaths:inputPaths];
Copy the code

In the code

  NSString *zipPath = [self getLogZipPath];
    NSString *password = nil;
    NSMutableArray *filePaths = [[NSMutableArray alloc] init];
    [filePaths addObject:[self getLogPath]];
    BOOL success = [SSZipArchive createZipFileAtPath:zipPath withFilesAtPaths:filePaths withPassword:password.length > 0 ? password : nil];
    if (success) {
        NSLog(@"Compression succeeded");
        NSLog(@"Compression failed");
Copy the code

If specifically uploaded to the server, the user can use AFN, etc., to upload the local ZIP file to the file server, no further details.

So far, we’ve done this, using runloop, monitoring the lag, writing to the log, and then compressing the upload server to delete the local process.

See Demo address for details



GCD semaphore -dispatch_semaphore_t


Simple monitoring of iOS caton demo

RunLoop Combat: Real-time lag monitoring

More information, welcome to pay attention to the individual public number, not regularly share a variety of technical articles.