An overview of the

UIView is a class that we come across every day in iOS development, and almost all controls related to page display inherit from it. However, the author has little knowledge about UIView layout, display, and drawing principle. Only by truly understanding its principle can we better serve our development. And in the context of higher and higher market requirements for iOS developers, the optimization of App page fluency is also a must-ask interview question for senior and above developers, which requires us to have a deeper understanding of UIView.

A UIView and CALayer

UIView: A UIView is a rectangular block (such as an image, text, or video) displayed on the screen that intercepts user input such as mouse clicks or touch gestures. Views can be nested within each other in a hierarchy. A view can manage the position of all its children. In iOS, all views are derived from a base class called UIView, which can handle touch events, can support Core Graphics-based drawing, You can do affine transformations (such as rotation or scaling), or simple animations like sliding or gradients.

CALayer: The CALayer class is conceptually similar to UIView; it is also a rectangular block managed by a hierarchical tree. It can also contain content (like images, text, or background colors) and manage the position of sublayers. They have methods and properties to animate and transform. The biggest difference with UIView is that CALayer doesn’t handle user interaction.

CALayer doesn’t know the exact response chain (the mechanism iOS uses to transmit touch events through view hierarchies), so it can’t respond to events, even though it provides some way to determine if a touch is within the scope of a layer.

1. The relationship between UIView and CALayer

Each UIView has a layer property called the backing Layer of a CALayer instance, which the view creates and manages to make sure that when subviews are added or removed from the hierarchy, their associated layers also have the same backing action in the hierarchy tree.

The relationship between the two: in fact, these associated layers are really used for displaying and animating on the screen. UIView is just an encapsulation of it, providing some specific functions of iOS like handling touch, as well as the high-level interface of the underlying methods of Core Animation.

This brings up a common interview question: Why does iOS offer two parallel hierarchies based on UIView and CALayer? Why not have a simple hierarchy for everything?

The reason is to do separation of responsibilities (the single responsibility principle), which also avoids a lot of duplicate code. Events and user interaction are different in many ways on iOS and Mac OS. A multi-touch based user interface is fundamentally different from a mouse and keyboard based user interface. That’s why iOS has UIKit and UIView, but Mac OS has AppKit and NSView. They are similar in function, but there are significant differences in implementation. By separating the logic of this function and packaging it into an independent Core Animation framework, Apple can share the code between iOS and Mac OS, making it more convenient for Apple’s own OS development team and third-party developers to develop applications on both platforms.

2. Some common attributes of CALayer

contentsattribute

The contents property of CALayer allows us to set an image for the Layer layer, so let’s look at its definition

/* An object providing the contents of the layer, typically a CGImageRef, * but may be something else. (For example, NSImage objects are * supported on Mac OS X 10.6 and later.) Default value is nil. strong) id contents;Copy the code

The type of this property is defined as ID, which means it can be any type of object. In this case, you can put any value in the contents property, and your app will compile. However, if you assign contents to something other than CGImage, the layer you get will be blank. In fact, the type you really need to assign is CGImageRef, which is a pointer to a CGImage structure. UIImage has a CGImage property, which returns a CGImageRef, but to use it requires a strong cast:

layer.contents = (__bridge id _Nullable)(image.CGImage);
Copy the code

contentGravityattribute

/* A string defining how the contents of the layer is mapped into its
 * bounds rect. Options are `center', `top', `bottom', `left',
 * `right', `topLeft', `topRight', `bottomLeft', `bottomRight',
 * `resize', `resizeAspect', `resizeAspectFill'. The default value is
 * `resize'. Note that "bottom" always means "Minimum Y" and "top"
 * always means "Maximum Y". */

@property(copy) CALayerContentsGravity contentsGravity;
Copy the code

If we set contents to an image for the layer layer, then we can use this property to make the image adapt to the layer size. It’s similar to UIView’s contentMode property, but it’s an NSString, not like the corresponding UIKit part, where the values are enumerated. ContentsGravity Optional constant values are the following:

kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill
Copy the code

For example, if you want to scale images to adapt the size of the layer, you can do this directly

layer.contentsGravity = kCAGravityResizeAspect;
Copy the code

contentsScaleattribute

/* Defines the scale factor applied to the contents of the layer. If
 * the physical size of the contents is '(w, h)' then the logical size
 * (i.e. for contentsGravity calculations) is defined as '(w /
 * contentsScale, h / contentsScale)'. Applies to both images provided
 * explicitly and content provided via -drawInContext: (i.e. if
 * contentsScale is two -drawInContext: will draw into a buffer twice
 * as large as the layer bounds). Defaults to one. Animatable. */

@property CGFloat contentsScale
Copy the code

The contentsScale property defines how contents sets the pixel size of the image to the size of the view, which is a floating point number with a value of 1.0 by default. This property is actually part of the Retina screen support mechanism, and its value is equal to the ratio of the current physical size to the logical size of the device. If contentsScale is set to 1.0, images will be drawn at 1 pixel per point, and if set to 2.0, images will be drawn at 2 pixels per point. When manipulating contents to set the image in code, it is important to manually set the layer’s contentsScale property, otherwise the image will display incorrectly on Retina devices. The code is as follows:

layer.contentsScale = [UIScreen mainScreen].scale;
Copy the code

maskToBoundsattribute

The maskToBounds property works like the UIView clipsToBounds property. If set to YES, images outside the layer range will be cropped.

contentsRectattribute

The contentsRect property is not used much in our daily development, and its main purpose is to allow us to display a subarea of the image set by contents. It’s a unit coordinate between 0 and 1. The default is {0,0, 1, 1}, which means that the entire image is visible by default. If we specify a smaller rectangle, such as {0,0,0.5,0.5}, then layer displays only the upper left corner of the image, which is 1/4 of the area.

In fact, assigning a value of CGImage to layer contents is not the only way to set its host map. We can also draw directly using Core Graphics. By inheriting UIView and implementing the -drawRect: method, If you use CALayer alone, you can implement its delegate (CALayerDelegate) method – (void)drawLayer:(CALayer *)layer inContext:(CGContextRef) CTX; I do my own drawing in here. The actual method drawing process is discussed below.

2.View layout and display

1. Principle of image display

Before introducing the layout and display of the image, it is necessary for us to understand the display principle of the image first, that is, how we create a display control through CPU and GPU operation display on the screen. This process can be divided into six stages:

  • Layout: First, a view is framed by the CPU, the view and layer hierarchy are prepared, layer properties are set (position, background color, border) and so on.
  • According to: View’s display layer, the stage at which the image is drawn. The so-called boarding diagram is what the layer we mentioned above shows. It has two Settings: one is directlayer.contents, and assign a valueCGImageRef; The second one is to rewrite UIViewdrawRect:orCALayerDelegatethedrawLayer:inContext:Method to achieve custom drawing. Note: If these two methods are implemented, there is an additional cost to CPU performance.
  • Ready: This is the stage where Core Animation is ready to send data to the render service. This stage mainly decodes the pictures used by the view and converts the format of the pictures. PNG or JPEG compressed image files are much smaller than bitmaps of the same quality. But before an image can be drawn on the screen, it must be expanded to its full unextracted size (usually equivalent to the image width x length x 4 bytes). To save memory, iOS usually doesn’t decode images until they’re actually drawn.
  • Commit: The CPU packages the hierarchy of processing views and layers and submits them to the rendering service via the IPC (Internal Processing Communication) channel, which consists of OpenGL ES and GPU.
  • Generate frame cache: The rendering service first hands the layer data to OpenGL ES for texture generation and coloring, and generates the before and after frame cache. Then, according to the refresh frequency of the display hardware, the VSync signal and CADisplayLink of the device are generally used as the standard to switch the frame cache before and after.
  • Render: The cache of the final frame to be displayed on the screen is handed to the GPU to collect images and shapes, run transformations, apply textures and blends, and finally display on the screen.

Note: After the layers have been successfully packaged and sent to the rendering server, the CPU still has to do the following: To display the layers on the screen, Core Animation must loop through OpenGL to convert each visible layer of the rendering tree into a texture triangle. Since the GPU doesn’t know anything about the structure of the Core Animation layer, it has to be done by the CPU.

The first four stages are handled at the software level (via the CPU), the fifth stage also involves the CPU, and only the last stage is performed entirely by the GPU. Also, there are only the first two phases that you can really control: layout and display. The Core Animation framework handles the rest internally, and you can’t control it either. So let’s focus on the layout and display phase.

2. Layout

Layout: Layout is the position and size of a view on the screen. UIView has three important layout properties: frame, bounds, and center.UIView provides methods for notifying the system that a view layout has changed, as well as rewritable methods that are called after the view layout has been recalculated.

layoutSubviews()methods

LayoutSubviews (): When a view “thinks” it should rearrange its child controls, it automatically calls its own layoutSubviews method, in which it “refreshes” the layout of the child controls. This method and no system implementation, we need to re-this method, in the implementation of the child control layout. This method is expensive because it works on each subview and calls its corresponding layoutSubviews method. The system will trigger the call mechanism of layoutSubviews according to the different states of the current Run loop, and we do not need to call it manually. Here are his triggers:

  • Triggered when the view is resized directly
  • calladdSubviewWill trigger the subviewlayoutSubviews
  • The user scrolls on the UIScrollView (layoutSubviews will be inUIScrollViewAnd its parentviewIs called on)
  • User rotating equipment
  • Update the constraints of the view

Each of these methods tells the system that the position of the view needs to be recalculated, and layoutSubviews is called. You can also trigger layoutSubviews directly.

setNeedsLayout()methods

A call to the setNeedsLayout() method triggers layoutSubviews, which represent to the system that the layout of the view needs to be recalculated. But calling this method just puts a dirty mark on the current view, telling the system that it needs to rearrange the view in the next run loop. That is, setNeedsLayout() will be called for a certain amount of time before layoutSubviews are triggered. Of course, this gap does not affect the user because it is never long enough to cause the interface to lag.

layoutIfNeeded()methods

The layoutIfNeeded() method tells the system that dirty views need to be updated immediately, rather than waiting for the next run loop, which triggers layoutSubviews immediately. Sure, but if you call layoutIfNeeded and there is no action to indicate to the system that the view needs to be refreshed, then layoutSubView will not be called. This method is more useful than setNeedsLayout when you need to rely on the new layout and can’t wait for the next run loop.

3. The display

Like layout, display has methods that trigger updates, which are called automatically by the system when an update is detected, or we can call a direct refresh manually.

drawRect:methods

As mentioned above, if you want to set a view’s lodging diagram, you can draw it yourself in addition to setting the view.layer.contents property directly. The way to draw is to implement the view’s drawRect: method. This method is similar to the layoutSubviews method of the layout in that it refreshes the display of the current View, except that it does not trigger subsequent calls to the View’s child methods. As with layoutSubviews, we cannot call the drawRect: method manually. Instead, we should call the indirect trigger method and let the system call it automatically at different points in the Run loop. The specific drawing process is introduced in the third section of this paper.

setNeedsDisplay()methods

This method is similar to setNeedsLayout in the layout. It sets an internal marker for the updated view, but returns it before the view is redrawn. Then in the next Run loop, the system iterates through all the tagged views and calls their drawRect: method. Most of the time, updating any UI component in a view marks the corresponding view as “dirty”, and by setting the view’s “internal update mark” it will be redrawn in the next run loop without explicitly calling setNeedsDisplay.

3. System rendering and asynchronous rendering process of UIView

UIView drawing process

So let’s seeUIViewDrawing process of

  • UIView calls setNeedsDisplay, which we’ve already introduced, and it doesn’t immediately start drawing.
  • UIView callsetNeedsDisplay, actually calls the method of the same name on its Layer property, which is equivalent to marking the layer with a draw flag.
  • In the currentrun loopAt the end, CALayer’s display method is called to enter the actual drawing
  • So in CALayer’s display method, it sayslayerProxy method ofdisplayLayer:If the agent does not implement this method, enter the system draw process, otherwise enter the asynchronous draw entry.

System to draw

  • At the beginning of system drawing, a drawing context is created inside CALayer, known as CGContextRef, which is the currentRef we get in the drawRect: method.

  • And then layer determines if there’s a delegate, and if there’s no delegate, calls CALayer’s drawInContext method, and if there’s a delegate, And if you implement the -drawLayer:inContext: method in the CALayerDelegate protocol or the -drawRect: method in UIView (which is a wrapper for the former), the system will call one of the two methods you implemented.

    If you use UIView directly, then the layer proxy is the current view, and you just implement -drawRect:, and then you do your own drawing in this method; If you’re using a CALayer created separately, you need to set layer.delegate = self; And of course self is the view or controller that’s holding the layer, so you need to implement the -drawLayer:inContext: method, and then draw inside that method.

  • Finally, CALayer passes the bitmap to the GPU for rendering, that is, assigning the generated bitmap to the layer.content property.

Note: Drawing with CPU is expensive, and you should avoid redrawing your view unless absolutely necessary. The secret to improving drawing performance is to avoid drawing as much as possible.

Asynchronous rendering

What is asynchronous drawing?

Through the above introduction we are familiar with the system drawing process, system drawing is in the main thread context creation, control independent drawing, which leads to the main thread frequently processing UI drawing work, if the elements to draw too much, too often, it will cause trouble. Asynchronous drawing, on the other hand, is to put the complex drawing process into the background thread to reduce the main thread load and improve the UI fluency.

Asynchronous drawing process

The asynchronous drawing process is clearly shown above:

  • From the image above, the entry for asynchronous drawing is inlayerProxy method ofdisplayLayer:If we want to draw asynchronously, we must implement this method in our custom View
  • indisplayLayer:Method we open up child threads
  • In the child thread we create the drawing context and useCore GraphicsRelevant API to complete the independent drawing
  • Image is generated after drawing
  • Finally, go back to the main thread and assign the Image Image to the contents property of the layer.

Of course, we also need to consider the management of threads and the timing of drawing in daily development. Using the third-party library YYAsyncLayer allows us to focus on specific drawing, and the specific use process can be viewed here.

4. To summarize

We know that when we implement the -drawLayer:inContext: method in the CALayerDelegate protocol or the -drawRect: method in UIView, the layer creates a drawing context. The amount of memory required for this context can be determined by this calculation: Layer width X layer height X4 bytes in pixels. For a full-screen layer on a Retina iPad, this amount of memory is 2,048 x 15,264 bytes, equivalent to 12MB of memory, which needs to be erased and reallocated each time the layer is redrawn. Using Core Graphics to draw on CPU is expensive, so how to draw efficiently? Ios-core-animation-advanced-techniques gives the answer. In daily development, we can completely use CAShapeLayer of Core Animation to draw Graphics instead of Core Graphics. The specific method is not introduced here, interested can go to check.

Ios-core-animation-advanced-techniques YYAsyncLayer juejin.cn/post/684490…