This paper mainly monitors the lag from three aspects: FPS, main thread and child thread operation UI

A, FPS

1. Monitoring principle

Based on CADisplayLink’s feature of synchronous drawing at the screen refresh rate, we try to implement an indicator that can observe the current frame number on the screen. FPS based on CADisplayLink is only instructive in production scenes and cannot represent real FPS, because FPS based on CADisplayLink cannot fully detect the current performance of Core Animation. It can only detect the frame rate of the current RunLoop. FPS refresh rate is very fast and prone to jitter, so it is difficult to detect stutter directly by comparing FPS.

2. Implementation method

  1. CADisplayLink defaults to 60 times per second;
  2. Add CADisplayLink to mainRunLoop;
  3. The timestamp attribute of CADisplayLink is used to record the last timestamp when CADisplayLink ticks each time.
  4. Count is used to record the number of CADisplayLink tick executions.
  5. The difference between CADisplayLink’s current timestamp and lastTimeStamp during tick calculation;
  6. If the difference is greater than 1, FPS = count/delta and the number of FPS is calculated.

The code is as follows:

- (void)startMonitorWithBlock:(MCXFPSMonitorBlock)monitorBlock {
    self.isMonitoring = YES;
    self.monitorBlock = monitorBlock;
    if (self.displayLink) {
        self.displayLink.paused = NO;
        return;
    }
    __weak typeof(self) weakSelf = self;
    self.displayLink = [CADisplayLink mcx_displayLinkWithBlock:^(CADisplayLink * _Nonnull displayLink) {
        [weakSelf tick:displayLink];
    }];
//self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)tick:(CADisplayLink *)link {
    if (self.lastTime == 0) {
        self.lastTime = link.timestamp;
        return;
    }
    self.count++;
    NSTimeInterval delta = link.timestamp - self.lastTime;
    if (delta < 1) {
        return;
    }
    self.lastTime = link.timestamp;
    double fps = self.count / delta;
    if (self.monitorBlock) {
        self.monitorBlock(self, fps);
    }
    self.count = 0;
}
Copy the code

3. Pit using CADisplayLink

Usually code below self. DisplayLink = [CADisplayLink displayLinkWithTarget: self selector: @ the selector (tick)];

For design reasons, CADisplayLink/NSTimer will strongly reference target, while NSRunLoop will hold CADisplayLink/NSTimer, and the user will hold CADisplayLink/NSTimer at the same time, which causes circular reference. The schematic diagram is as follows:

Solution 1: Block form

A Category that encapsulates a CADisplayLink and provides a block interface: /* When initializing an NSTimer/CADisplayLink object, specify a target that preserves its target. Our goal is to get around the problem of the timer object forcibly referencing the target object. In the classification, the target specified by the timer object is an NSTimer/CADisplayLink class object, which is a singleton, so it doesn’t matter if the timer keeps it. This way, the circular reference still exists, but because the class object does not need to be recycled, it solves the problem.

+ (CADisplayLink *)mcx_displayLinkWithBlock:(MCXDisplayLinkBlock)block {
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(mcx_displayLink:)];
    displayLink.mcx_displayLinkBlock = [block copy];
    return displayLink;
}

+ (void)mcx_displayLink:(CADisplayLink *)displayLink {
    if (displayLink.mcx_displayLinkBlock) {
        displayLink.mcx_displayLinkBlock(displayLink);
    }
}

- (void)setMcx_displayLinkBlock:(MCXDisplayLinkBlock)mcx_displayLinkBlock {
    objc_setAssociatedObject(self, @selector(mcx_displayLinkBlock), mcx_displayLinkBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (MCXDisplayLinkBlock)mcx_displayLinkBlock {
    return objc_getAssociatedObject(self, @selector(mcx_displayLinkBlock));
}
Copy the code

Solution two, WeakProxy form

NSProxy is rarely used in daily development. It is a proxy middleman that can be used to proxy other classes/objects. First post a schematic diagram:

Part of the code is as follows:

[CADisplayLink displayLinkWithTarget:[MCXWeakProxy proxyWithTarget:self] selector:@selector(tick:)];

MCXWeakProxy.h ... @property (nullable, nonatomic, weak, readonly) id target; . MCXWeakProxy.m ... - (instancetype)initWithTarget:(id)target { _target = target; return self; } + (instancetype)proxyWithTarget:(id)target { return [[MCXWeakProxy alloc] initWithTarget:target]; } - (id)forwardingTargetForSelector:(SEL)selector { return _target; }...Copy the code

Two, the main thread stuck monitoring

1. Monitoring principle

This is a common technique used in the industry to detect stackage by opening a child thread to monitor the RunLoop of the main thread. When the time between two state regions is greater than the threshold, a stackage is recorded as occurring. Meituan mobile terminal performance monitoring schemeHertzThis is the way, as shown below

Main thread stuck monitoring implementation ideas: Create a child thread, and then calculate whether the time between the two state areas of kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting exceeds a certain threshold in real time to determine whether the main thread is stuck. You can imagine this process as an athlete running laps on the playground. We will judge whether we have run a lap at regular intervals. If we find that we have not completed a lap at the specified interval, we consider that it takes too much time in message processing and the main thread is stuck.

2. Schematic principles

  • Under normal circumstances

  • In the caton case

3. Implementation method

  1. Create a runLoopObserver to observe the runloop status of the main thread.
  2. Also create a dispatchSemaphore to ensure synchronous operations.
  3. Add the runLoopObserver to the main thread runloop to observe.
  4. Start a child thread and open a continuous loop in the child thread to monitor the status of the main thread runloop.
  5. If the status of the main thread runloop is stuck with BeforeSources or AfterWaiting for more than a certain amount of time, the main thread is currently stuck.

The specific code is as follows

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ MCXCatonMonitor *monitor = (__bridge MCXCatonMonitor*)info; monitor->runLoopActivity = activity; dispatch_semaphore_t semaphore = monitor->dispatchSemaphore; dispatch_semaphore_signal(semaphore); } - (void)startMonitor { self.isMonitoring = YES; If (runLoopObserver) {return; } dispatchSemaphore = dispatch_semaphore_create(0); CFRunLoopObserverContext Context = {0,(__bridge void*)self,NULL,NULL}; runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes); // Add the observer to the observation in common mode of the main runloop. Dispatch_async (dispatch_get_global_queue(0, 0)) While (YES) {long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, kMCXCatonMonitorDuration * NSEC_PER_MSEC)); if (semaphoreWait ! = 0) { if (! self->runLoopObserver) { self->timeoutCount = 0; self->dispatchSemaphore = 0; self->runLoopActivity = 0; return; } // The state of the two runloops, BeforeSources and AfterWaiting can detect whether the two state interval time caton the if (self - > runLoopActivity = = kCFRunLoopBeforeSources | | Self ->runLoopActivity == kCFRunLoopAfterWaiting) {if (++self->timeoutCount < kMCXCatonMonitorMaxCount) { continue; }// end activity}// end semaphore wait self->timeoutCount = 0; }// end while }); }Copy the code

Runloop Observer: The Runloop Observer has seven states

/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),// Run the entry of the runloop. Kcfrunlooptimers = (1UL << 1),// (before handling any Timer Timer) kCFRunLoopBeforeSources = (1UL << 2),// (before handling any Sources source) KCFRunLoopBeforeWaiting = (1UL << 5),// (before waiting for Source and Timer) kCFRunLoopAfterWaiting = (1UL <<. 6),// (after waiting for the Source and Timer, and before being woken up.) KCFRunLoopExit = (1UL << 7),// (runloop exit) kCFRunLoopAllActivities = 0x0FFFFFFFU};Copy the code

The child thread operates the UI

Child thread operations will also cause interface lag. During any UI operation, at least one of the four methods setNeedsLayout, setNeedsDisplay, setNeedsDisplayInRect and layoutSubviews in UIView will normally be triggered. Simply swap these four methods using the Runtime, do thread checking, and log the current thread call stack.

+ (void)load { [self mcx_swizzleSEL:@selector(setNeedsLayout) withSEL:@selector(mcx_setNeedsLayout)]; [self mcx_swizzleSEL:@selector(setNeedsDisplay) withSEL:@selector(mcx_setNeedsDisplay)]; [self mcx_swizzleSEL:@selector(setNeedsDisplayInRect:) withSEL:@selector(mcx_setNeedsDisplayInRect:)]; [self mcx_swizzleSEL:@selector(layoutSubviews) withSEL:@selector(mcx_layoutSubviews)]; } - (void)mcx_setNeedsLayout { [self mcx_setNeedsLayout]; [self childThreadUICheck]; } - (void)mcx_setNeedsDisplay { [self mcx_setNeedsDisplay]; [self childThreadUICheck]; } - (void)mcx_setNeedsDisplayInRect:(CGRect)rect { [self mcx_setNeedsDisplayInRect:rect]; [self childThreadUICheck]; } - (void)mcx_layoutSubviews { [self mcx_layoutSubviews]; [self childThreadUICheck]; } - (void)childThreadUICheck { if ([NSThread isMainThread]) { return; } // There is a child thread operating UI, record the current thread stack information}Copy the code