Head

In performance optimization, an important knowledge point is the optimization of lag. We measure its smoothness by FPS (Frames Per Second). The recommended refresh rate of Apple iPhone is 60Hz, which means that the GPU refreshes the screen 60 times Per Second, and each refresh is a frame. The FPS value of a static page is 0, which has no reference significance. Only when the page is animated or sliding, the FPS value has reference value. The FPS value reflects the smoothness of the page, and the lag will be obvious when it is lower than 45

Screen image principle

The image we see on a dynamic screen is actually frame by frame, just like video. To synchronize the display with the system’s video controller, the display (or other hardware) 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.

The birth of Caton

The next steps to complete the display are as follows: CPU calculates data -> GPU renders -> the rendering result is stored in the frame buffer -> The video controller will read the data of the frame buffer frame by frame according to the VSync signal -> Image. If the screen has issued VSync but the GPU has not finished rendering, only the last data can be displayed. As a result, the current calculated frame data is lost, thus resulting in a stoop, the current calculated frame data can only wait for the next cycle to render.

Catton’s optimization

The solution is to reduce the CPU and GPU resource consumption of this frame as much as possible before the next VSync arrives. To reduce the consumption, we need to understand the specific division of rendering between the two frames and how views are generated in iOS

UIView and CALayer

As we all know, the view’s job is to create and manage layers to make sure that when a subview is added or removed from the hierarchy, its associated layers do the same thing in the layer tree, so that the view tree and the layer tree are structurally consistent, So why does iOS provide two parallel hierarchies based on UIView and CALayer? The reason for this is separation of responsibilities, which also avoids a lot of duplicate code. Events and user interaction are different in a lot of ways on iOS and Mac OS X. There’s a fundamental difference between a multi-touch based user interface and a mouse and keyboard based interaction. That’s why iOS has UIKit and UIView. Mac OS X has AppKit and NSView. They are similar in function, but there are significant differences in implementation.

CALayer

So why is CALayer able to present visualizations? Because a CALayer is basically a texture. Texture is an important basis for GPU image rendering. Texture is essentially an image, so CALayer also contains a contents property pointing to a cache, called the backing Store, which can store a Bitmap. IOS refers to the images stored in this cache as boarding maps

Manual drawing
Use pictures

  • Use pictures:contents image
  • Manual drawing:custom drawing

Contents Image

Contents Image means to configure the Image through the Contents property of CALayer. However, the contents property is of type ID. In this case, you can give the contents property any value you want, and your app will still compile. But in practice, if the content value is not CGImage, the resulting layer will be blank

    // Contents Image
    UIImage *image = [UIImage imageNamed:@"cat.JPG"];
    UIView *v = [UIView new];
    v.layer.contents = (__bridge id _Nullable)(image.CGImage);
    v.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:v];
Copy the code

Custom Drawing

Custom Drawing refers to using Core Graphics to draw boarding diagrams directly. In practice, you typically customize your drawing by inheriting UIView and implementing the -drawRect: method.

  • UIView has an associated layer, i.eCALayer.
  • CALayer has an optional delegate property, implementedCALayerDelegateThe agreement. UIView is implemented as a proxy for CALayerCALayerDelegaeThe agreement.
  • Called when a redraw is required-drawRect:CALayer asked his agent to give him a boarding map to display.
  • CALayer will first try to call-displayLayer:Method, in which case the agent can set the contents property directly.
- (void)displayLayer:(CALayer *)layer;
Copy the code
  • If the proxy does not implement the -DisplayLayer: method, CALayer will try to call it-drawLayer:inContext:Methods. Before calling this method, CALayer creates an empty host diagram (size determined by bounds and contentScale) and a Core Graphics drawing context in preparation for drawing the host diagram, passed in as the CTX argument.
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
Copy the code
  • Finally, the boarding image generated by Core Graphics is stored in the backing Store.

    If UIView subclass is rewrittendrawRectUIView completes executiondrawRectAfter that, the system will be layercontentCreate a cache for the contents drawn by drawRect. Even if the overwritten drawRect does nothing, it opens up cache, consumes memory,So try not to overwrite drawRect and do nothing.

  • In fact, when the operation of the UI, such as changing the Frame, update the UIView/CALayer level, or manually invoked the UIView/CALayer setNeedsLayout/setNeedsDisplay method, The app may need to update the view tree during this process, and the layer tree will be updated accordingly

  • Second, the CPU computs what to Display, including Layout, Display, and Prepare. It notifies registered listeners when runloop is in BeforeWaiting and Exit. The backing store is then sent to the Render Server, a separate rendering process

  • The data will be deserialized after reaching the Render Server to obtain the layer tree and filter the shielded part of the layer tree according to the layer order, RBGA value and layer frame. After filtering, the layer tree will be converted into a rendering tree and the information of the rendering tree will be transferred to OpenGL ES/Metal

At this point, the previous CPU processes these things collectively referred to as Commit transactions

Render Server

The Render Server will call the GPU, and the GPU will start the six stages of vertex shader, shape assembly, geometry shader, rasterization, fragment shader, test and mix. After completing these six stages, the data calculated by CPU and GPU will be displayed on every pixel of the screen

  • Vertex Shader
  • Shape Assembly, also known as pixel Assembly
  • Geometry Shader
  • Rasterization
  • Fragment Shader
  • Tests and Blending

The second stage, shape (pixel) assembly. This stage takes all vertices output by the vertex shader as input and assembles all points into the shape of the specified primitives. This is a triangle. Primitive is used to show how to render vertex data such as points, lines, and triangles.

The third stage, geometry shader. This stage takes as input a set of vertices in the form of primitives, which can generate other shapes by generating new primitives to construct new (or other) primitives. In this case, it generates another triangle.

The fourth stage is rasterization. This stage maps primitives to corresponding pixels on the final screen, generating fragments. Fragments are all the data needed to render a pixel.

The fifth stage, fragment shader. This phase starts with Clipping the input fragment. Crop will discard all pixels outside the view to improve execution efficiency.

The sixth stage, testing and mixing. This phase detects the corresponding depth value of the fragment (z coordinate), determines whether the pixel is in front of or behind other objects, and decides whether it should be discarded. In addition, this phase checks the alpha value (which defines an object’s transparency) to mix objects. Therefore, even if the output color of one pixel is calculated in the fragment shader, the final pixel color may be completely different when rendering multiple triangles. Formula is:

R = S + D * (1 - Sa)
Copy the code

Suppose there are two pixels S(source) and D(destination), S is relatively forward in the z-axis direction (above) and D is relatively backward in the Z-axis direction (below), So the final color value is S (top pixel) color + D (bottom pixel) color * (1-S (top pixel) color transparency)

Therefore, we need to make the page as few layers as possible, and try not to use alpha

  • Finally, the GPU passesFrame Buffer (double Buffer),Video controllerAnd other related components, display the image on the screen.

This is the end of the native rendering process.

Native rendering caton optimization scheme

Therefore, the main idea to solve the lag phenomenon is to reduce the consumption of CPU and GPU resources as much as possible. ######CPU

  • Try to use lightweight objects such as CALayer for UI controls that don’t handle events.
  • Do not frequently call UIView properties such as frame, bounds, transform, etc.
  • Try to calculate the layout in advance, adjust the corresponding attributes once when necessary, do not modify many times;
  • Autolayout consumes more CPU than frame;
  • The image size is the same as the UIImageView size;
  • Controls the maximum number of concurrent threads.
  • Time-consuming operations are put into child threads; Such as text size calculation, drawing, picture decoding, drawing, etc.; ######GPU
  • Try to avoid a large number of pictures in a short time;
  • The maximum texture size that GPU can process is 4096 * 4096. Exceeding this size will occupy CPU resources, so the texture cannot exceed this size.
  • Minimize the number and level of perspectives;
  • Reduce transparent views (alpha < 1) and set opaque to YES for opaque views;
  • Try to avoid off-screen rendering;

Big front-end rendering

Big front-end development frameworks are mainly divided into two types: the first type is webView-based, and the second type is similar to React Native.

For large front-end rendering of the first type of WebView, the main work is done in WebKit. WebKit’s Rendering Layer is derived from the previous Layer Rendering architecture of macOS, which iOS is based on. So, in essence, WebKit and iOS native rendering aren’t that different.

The React Native class is simpler, and renders directly on iOS. So why do we feel like WebViews and React Native render slower than Native?

Judging from the first load of content, even if it is local, the big front end has more scripting to parse than native.

Webviews require additional parsing of HTML + CSS + JavaScript code, while React Native solutions require parsing of JSON + JavaScript. HTML + CSS is more complex than JSON, so parsing is slower than JSON. That is, the WebView will be slower than the React Native class when the first content loads.

In terms of the interpretation performance of the language itself, interface updates after large front-end loading will be implemented through JavaScript interpretation, and the performance of JavaScript interpretation is worse than that of native interpretation, especially when the interpretation performs complex logic or a large number of calculations. So, the big front end is much slower than the native.

After talking about big front-end rendering, you will find that there is a performance gap between WebView and React Native due to the performance problems of scripting language itself, compared with Native rendering. How does Flutter render differently, and how does it perform, with a framework that doesn’t use a scripting language and has a new rendering engine?

Flutter rendering

The Flutter interface is made up of widgets. All widgets make up the Widget Tree. When the page is updated, the Widget Tree is updated, then the Element Tree is updated, and finally the RenderObject Tree is updated.

The subsequent rendering process of Flutter includes Build, Wiget Tree, Element Tree, RenderObject Tree, Layout, Paint, Composited Layer, etc. Combine layers, generate textures, and submit rendering content to GPU for rasterization and composition using OpenGL interface. This is the C++ Layer of Flutter, using Skia library. After submitting to the GPU process, the process of compositing and displaying the screen is basically similar to iOS native, so the performance is similar.