In the daily development often appear the phenomenon of stalling (frame loss), to the user’s feeling is very bad. So how does this phenomenon occur, how to detect frame drop, how to optimize? This paper will analyze these problems

Interface rendering process

CPU and GPU play an important role in the interface rendering process

CPU and GPU

  • The Central Processing Unit(CPU) is used by programs to load resources, create and destroy objects, adjust object properties, calculate layouts, Autolayout, render text, The computation and typesetting of text, the transcoding and decoding of image format, and the drawing of Core Graphics all depend on CPU

  • The full name of GPU is Graphics Processing Unit(GPU). It is a Processing Unit specially tailored for high concurrent Graphics computation. It uses less electricity than CPU to complete the work and the floating point computing power of GPU is much more than CPU. Compared to the CPU, the GPU can do a single thing: take the submitted Texture and vertex description, apply the transform, mix and render, and then print it to the screen. The main things you can see are textures (pictures) and shapes (vector shapes for triangle simulation). The rendering performance of GPU is much more efficient than that of CPU, and the load and consumption of the system are also lower

In the development, we should try to let THE CPU responsible for the main thread UI mobilization, graphics display related work to GPU to deal with, when it comes to rasterization and other work, the CPU will also participate in.

Rendering process

  • Before we talk about the rendering principle, let’s introduce the CRT display principle.

    • The CRT electron gun scans the screen line by line from top to bottom, rendering a single frame. The gun then returns to its original position for the next scan. To synchronize the display process with the system’s video controller, the display generates a series of timing signals using a hardware clock.

    • When the gun switches lines to scan, the monitor emits a horizontal synchronization signal, or HSync;

    • When a frame is drawn and the gun returns to its original position, the display sends a vertical synchronization signal, or VSync, before it is ready to draw the next frame. The monitor usually refreshes at a fixed rate, which is the frequency at which the VSync signal is generated.

    • CRT electron gun scanning process is shown as follows:



    • Although today’s displays are basically LCD screens, the principle is basically the same.
  • The interface rendering process is as follows: CPU calculation -> GPU rendering -> Frame buffer -> Video controller reading the data of frame buffer -> display, as shown below:



Double buffering +VSync

  • If after GPU rendering, for some reason, the frame buffer is not saved, thus forming a wait, in order to solve this problem, a double buffer mechanism is generated, that is, before and after frames.

    • whenGPUAfter rendering a frame, it’s stored in the frame cache, and then the video controller reads the buffer frame cache, and at the same timeGPURender another frame and store it in another frame cache, thus toggle back and forth to display the interface, as shown below:



    • This switch is not arbitrary, we usually render in one second60 frames, that is,VSyncevery16.67 msSend primary signal
    • So, the video controller will followVSyncsignalFrame by frame to readFrame buffer data

caton

Caton principle of production

  • We know VSync every 16.67 ms sends a signal, calculates the two signals between the CPU and gpu rendering after deposited in the frame buffer, so if the calculation steps to spend time is longer, then apply colours to a drawing part of the deposited in the frame buffer is less, when the video controller didn’t read the whole, while reading frame buffer is created at this time frame, It’s called Caton.

  • The caton process is shown below:



Caton detection

  • Frames per secondFPS(Frames Per Second)In general60fpsFor the best, we can detectApptheFPSTo observeAppIs it smooth?

The FPS of an App can be detected in the following ways:

CADisplayLink

  • CADisplayLink will be triggered when the system sends VSync every time. We can count the number of VSync sent per second to check whether the FPS of the App is stable. The main code is as follows:

    @interface ViewController(a)
    @property (nonatomic.strong) CADisplayLink *link;
    @property (nonatomic.assign) NSTimeInterval lastTime; // Record the time every 1 second
    @property (nonatomic.assign) NSUInteger count; // Record the number of messages sent in VSync1 seconds
    @end
    
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkAction:)];
    [_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    
    - (void)linkAction: (CADisplayLink *)link {
        if (_lastTime == 0) {
            _lastTime = link.timestamp;
            return;
        }
        _count++;    
        NSTimeInterval delta = link.timestamp - _lastTime;
        if (delta < 1) return;
      
        _lastTime = link.timestamp;
        float fps = _count / delta;
        _count = 0;
          
        NSLog(🎈 FPS: %f", fps);
    }
    Copy the code

RunLoop

  • In the Runloop principle, we analyze the Runloop in detail. Its exit and entry are essentially Observer notifications. We can monitor the status of Runloop and send signals in the relevant callback. If no signal is received within the set time, a stall has occurred. The concrete implementation is as follows:

    @interface WSBlockMonitor(a){
      CFRunLoopActivity activity;
    }
    
    @property (nonatomic.strong) dispatch_semaphore_t semaphore;
    @property (nonatomic.assign) NSUInteger timeoutCount;
    @end
    
    - (void)registerObserver{
        CFRunLoopObserverContext context = {0,(__bridge void*)self.NULL.NULL};
        //NSIntegerMax: has the lowest priority
        CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                                kCFRunLoopAllActivities,
                                                                YES.NSIntegerMax,
                                                                &CallBack,
                                                                &context);
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    }
    
    static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        WSBlockMonitor *monitor = (__bridge WSBlockMonitor *)info;
        monitor->activity = activity;
        // Send a signal
        dispatch_semaphore_t semaphore = monitor->_semaphore;
        dispatch_semaphore_signal(semaphore);
    }
    
    - (void)startMonitor {
        // Create a signal
        _semaphore = dispatch_semaphore_create(0);
        // Monitor the duration of the child thread
        dispatch_async(dispatch_get_global_queue(0.0), ^ {while (YES)
            {
                // The timeout is 1 second, st is not equal to 0 without waiting for the semaphore, RunLoop all tasks
                long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
                if(st ! =0)
                {
                    if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting) // About to process sources, about to go to sleep
                    {
                        if(+ +self->_timeoutCount < 2) {
                            NSLog(@"timeoutCount==%lu", (unsigned long)self->_timeoutCount);
                            continue;
                        }
                        // The scale of one second or so is very possible continuously to avoid large-scale printing!
                        // You can record the stack information for troubleshooting
                        NSLog("More than two consecutive freezes detected"); }}self->_timeoutCount = 0; }}); }// Call the method
    - (void)start{
        [self registerObserver];
        [self startMonitor];
    }
    Copy the code
    • Listen mainly on the main threadRunloopThere are two states: about to process something and about to sleep, and the child thread monitors the duration if two times in a row1 secondIf no signal is received, it indicates that there is a holdup. At this time, the stack of holdup can be recorded for easy troubleshooting.

WeChat matrix

  • The WeChatmatrixAnd with the aid ofrunloopImplementation of the general process aboveRunloopSame as it usesAnnealing algorithmOptimize the efficiency of capturing cards, preventing the same cards from being captured consecutively, and by saving the most recent20The main thread stack information, obtain the most recent time stack. Therefore, an accurate analysis of the causes of Caton can be made with the help of wechatmatrixTo analyze Catton.

Drops DoraemonKit

  • DoraemonKitThe Caton detection protocol is not correctrunloopOperation, it’s alsowhileIn the loop according to a certain state judgment, through the main thread constantly send signalssemaphore, the waiting time for the signal in the cycle is5 seconds, wait for timeout, it indicates that the main thread is stuck, and reports related information.

Optimization scheme

After caton is detected, relevant optimization should be carried out next. We can adopt the following schemes

Preliminary layout

  • For layout, we would like to use navigation or SnapKit, which are all based on AutoLayout. Automatic layout usually determines the size of UI controls after assignment. According to Apple, frame is much less expensive than AutoLayout

  • For example, in a complex UITableViewCell, assignment will generate the size of the UI control. If the cell is large, it will perform frequent calculations, which will consume performance. In this case, we can calculate the Rect of each control when the data is requested, and then assign the frame value of each control when the data is assigned, which will greatly reduce the calculation. This scheme is also called pre-typesetting and the code is as follows:

  • Data DataModel code

    @interface DataModel : NSObject
    @property (nonatomic.strong) NSString *name;
    @end
    Copy the code
  • Layout the LayoutModel code

    // .h
    @interface LayoutModel : NSObject
    @property (nonatomic.assign) CGRect nameRect;
    @property (nonatomic.strong) DataModel *data;
    @property (nonatomic.assign) CGFloat height;
    
    - (instancetype)initWithModel:(DataModel *)model; // Initialize the code
    @end
    
    // .m
    - (instancetype)initWithModel:(DataModel *)model {
      self = [super init];
      if (self) {
          self.data = model;
          // Calculate the size of the related control based on the data
          CGSize size = [self getSizeWithContent:model.name];
          self.nameRect = CGRectMake(15.100, size.width, size.height);
          self.height = 200; // Calculate the cell height
      }
      return self;
    }
    
    - (CGSize)getSizeWithContent: (NSString *)content {
        // Calculate size from text...
        return CGSizeMake(100.40);
    }
    Copy the code
    • layoutModelTo initialize, pass in the correspondingmodelAnd then according tomodelThe related fields in thesizeAnd then talk about the correspondingrectAssigned tolayoutModelRelated fields in.
  • Cell code

    // .h
    - (void)configCellWithModel: (LayoutModel *)model;
    
    // .m
    @interface TestCell(a)
    @property (nonatomic.strong) UILabel *nameLbl;
    @end
    
    @implementation TestCell
    - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
        self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
        if (self) {
            self.nameLbl = [UILabel new];
            [self.contentView addSubview:_nameLbl];
        }
        return self;
    }
    
    - (void)configCellWithModel:(LayoutModel *)model {
        self.nameLbl.frame = model.nameRect; / / frame assignment
        self.nameLbl.text = model.data.name; // Data assignment
    }
    Copy the code
    • cellAfter the control is created, set the associated color font, and then set it at the same time when the data is assignedframeFor the assignment
  • VC code

    @property (nonatomic.strong) NSMutableArray<LayoutModel *> *dataSource;
    
    // Simulate a network request
    dispatch_async(dispatch_get_global_queue(0.0), ^ {NSDictionary *dataDic;  // Network requests data
        NSMutableArray<DataModel *> *modelArray = [NSMutableArray array];
        for (NSDictionary *dic in dataDic[@"data"]) {
            DataModel *model = [DataModel yyModel: dic]; // The associated json to model method
            [modelArray addObject:model];
        }
      
        self.dataSource = [NSMutableArray arrayWithCapacity:modelArray.count];
        for (DataModel *model in modelArray) {
            LayoutModel *layout = [[LayoutModel alloc] initWithModel:model]; // According to the data model, initialize the layoutModel and calculate the layout of the control
            [self.dataSource addObject:layout];
        }
      
        // reloadData after the calculation is complete
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];
        });
    });
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
        return _dataSource.count;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        NSString *identifier = @"cellID";
        TestCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
        if(! cell) { cell = [[TestCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:identifier];
        }
      
        [cell configCellWithModel:self.dataSource[indexPath.row]];
        return cell;
    }
    
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        LayoutModel *model = self.dataSource[indexPath.row];
        return model.height;
    }
    Copy the code
    • vcIs mainly created after the network requestThe data modelAfter, then according to willdataModelcreatelayoutModelAnd the relevant calculation, after the completion ofreloadData, so that the relevant layout is calculated at once, slidecellOnly perform the assignment without additional layout time calculation

Pre-decode & pre-render

  • Pre-decoding mainly optimizes image and video classes, such as UIImage, and its loading process is as follows:



    • Data BufferAfter decoding cache toImage BufferAnd store it in the frame buffer for rendering.
    • The decoding process is relatively resource consuming, so you can put the decoding work into the child thread, do some decoding work in advance
    • Picture decoding specific practice can refer toSDWebImageWhile audio and video decoding can be referencedFFmpeg

According to the need to load

  • Load on Demand means load on demand, for exampleTableView, each time it appears when slidingcellWill gocellForRowSome of the assignment methods incellIt appears and disappears in the interface immediately, like this can listen to the sliding state, when the sliding stops according totableViewthevisibleCellsGet current visibilitycellAnd then for thesecellPerform the assignment, which also saves a lot of overhead.

Asynchronous rendering

  • Asynchronous rendering is to process the graphics that need to be drawn in advance in the child thread, and then send the processed image data directly back to the main thread for use, which can reduce the pressure of the main thread.

  • Asynchronous rendering is done on layer, drawing the content shown using UIGraphics into an image and displaying it on layer.content

  • We know that drawing executes the drawRect: method, and looking at the stack in the method tells us:



    • We know that in the stackCALayerIn the calldisplayMethod to call the drawing related method, according to the process we will achieve a simple drawing:
    // WSLyer.m
    - (void)display {
        / / create the context
        CGContextRef context = (__bridge CGContextRef) [self.delegate performSelector:@selector(createContext)];
        [self.delegate layerWillDraw:self]; // Prepare the drawing
        [self drawInContext:context]; / / to draw
        [self.delegate displayLayer:self]; // Display bitmap
        [self.delegate performSelector:@selector(closeContext)]; // Finish drawing
    }
    
    
    // WSView.m
    - (void)drawRect:(CGRect)rect {
        // Drawing code
    }
    + (Class)layerClass {
        return [WsLayer class];
    }
    / / create the context
    - (CGContextRef)createContext {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.layer.opaque, self.layer.contentsScale);
        CGContextRef context = UIGraphicsGetCurrentContext(a);return context;
    }
    
    - (void)layerWillDraw:(CALayer *)layer {
        // Prepare the drawing
    }
    
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
        [super drawLayer:layer inContext:ctx];
        / / shape
        CGContextMoveToPoint(ctx, self.bounds.size.width / 2- 20.20);
        CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 20.20);
        CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 + 40.60);
        CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 40.60);
        CGContextAddLineToPoint(ctx, self.bounds.size.width / 2 - 20.20);
        CGContextSetFillColorWithColor(ctx, UIColor.magentaColor.CGColor);
        CGContextSetStrokeColorWithColor(ctx, UIColor.magentaColor.CGColor); / / stroke
        CGContextDrawPath(ctx, kCGPathFillStroke);
    
        / / text
        [@ "unique" drawInRect:CGRectMake(self.bounds.size.width / 2 - 40.70.80.24) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:15].NSForegroundColorAttributeName: UIColor.blackColor}];
        / / picture
        [[UIImage imageNamed:@"buou"] drawInRect:CGRectMake(self.bounds.size.width / 2 - 40.100.60.50)];
    }
    
    // Main thread render
    - (void)displayLayer:(CALayer *)layer {
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext(a);dispatch_async(dispatch_get_main_queue(), ^{
            layer.contents = (__bridge id)(image.CGImage);
        });
    }
    
    / / end of the context
    - (void)closeContext {
        UIGraphicsEndImageContext(a); }Copy the code
    • inVCYou only need to addviewObjects can be
    // ViewController.m
    WsView *view = [[WsView alloc] initWithFrame:CGRectMake(100.100.200.200)];
    view.backgroundColor = UIColor.yellowColor;
    [self.view addSubview:view];
    Copy the code

    The running result and hierarchy are as follows:



  • Asynchronous rendering processing is relatively complex, the specific practice can refer to Meituan Graver, or YYAsyncLayer these two asynchronous rendering framework.