Recommended precursors to this article: Full parsing of iOS rendering

My Github blog address is Rickey – Github

First, why should I drop frames

First of all, there’s one thing, and that’s why do we drop frames for animation?

As we all know, the higher the refresh frequency, the better the experience. For iOS app, the refresh frequency should be as close as 60fps, the better. In this case, lowering the frame of animation actively will definitely affect the experience of animation. But on the other hand, we also know that the animation rendering process needs to consume a large amount of GPU resources, so reducing the animation frame can reduce the load on THE GPU and reduce the GPU utilization peak.

Therefore, frame reduction for animation is actually a decision to trade experience for performance. In the case that the animation is not complicated but has a large number of animations (such as some bullet screen animations and thumbs-like animations), frame reduction for animation will not affect the animation effect. In this case, frame reduction can save a lot of GPU performance.

Second, the performance of animation rendering consumption

The principle of screen rendering in iOS can be referred to the previous article: Full analysis of iOS Rendering, which will explain the whole process of screen rendering, explain in detail the whole principle of Core Animation rendering pipeline, and why the rendering process has a large consumption of GPU.

The following figure shows Core Animation’s single rendering pipeline, that is, the rendering process of a frame of Animation:

  • Handle Events: This process will first process click Events. During this process, it may be necessary to change the layout and interface level of the page.
  • Commit Transaction: At this point, the APP uses the CPU to process the pre-processing of the displayed content, such as layout calculation, image decoding, etc., which will be explained in detail next. Then package the calculated layer and send it to Render Server.
  • Render server-decode: After the packaged layers are transferred to Render Server, they are first decoded. Note that after decoding, you need to wait for the next RunLoop before executing the next Draw call.
  • Render server-draw Calls: After decoding is completed, Core Animation will call the method of the lower rendering framework to Draw, and then call the GPU.
  • Gpu-render: This stage is mainly rendered by the GPU.
  • Display: Display stage, need to wait for the render end of the next RunLoop trigger Display.

In a word, the rendering of each frame of animation has certain consumption on CPU and GPU, especially on GPU performance.

Screen refresh FPS vs CoreAnimation FPS

We all know how vSync’s vertical signal refreshes the screen, but there’s more than one FPS in iOS.

Screen refresh FPS

The screen refresh rate is commonly referred to as the FPS. Due to the transient effect of the human eye, when the screen refresh rate is high enough (usually around 50 to 60 FPS), the picture looks continuous and smooth. When a render takes too long, a frame drop occurs, the FPS drops, and the user can intuitively feel the lag.

For iOS users, the screen refresh frame rate is a direct reflection of the smoothness experience, and obviously the higher the FPS, the closer it is to 60, the better.

CoreAnimation FPS

CoreAnimation FPS refers to the running frame rate of CoreAnimation Render Server, corresponding to the GPU Render stage which is very important in the previous rendering pipeline.

It can be found that every rendering pipeline must involve the Render Server, so the frequency of Render Server running directly reflects the frequency of GPU being invoked. The higher the FPS of CoreAnimation, the more frequently the GPU is used by the rendering pipeline, the higher the corresponding GPU utilization rate will be.

Therefore, to put it simply, CoreAnimation FPS directly affects the utilization rate of GPU. Generally speaking, the lower CoreAnimation FPS is, the better.

Normally, if the interface does not have frequent UI changes and does not require frequent re-rendering, then the CoreAnimation FPS should be very low. However, if a high frame rate animation is used, it will inevitably increase the FPS of CoreAnimation due to the need to quickly update the animation effect.

The FPS measured by using the CoreAnimation FPS option in Instrument is CoreAnimation FPS. As shown in the figure, AVG CoreAnimation FPS can be detected by Instrument. As well as GPU utilization, these indicators can be used as the result of frame rate optimization indicators.

4. Frame drop scheme

Before we investigate frame reduction solutions, let’s review our end goal: to investigate various animation implementation methods, select ways to control or reduce the rendering frame rate, and re-implement existing animation. Thus, the GPU usage can be reduced.

Override DrawRect:

A common way to customize an animation is to override the drawRect: method: Change the view properties -> trigger drawRect: redraw -> change the view presentation.

In the Commit Transaction phase of the rendering pipeline mentioned above, the Display step will be used to draw the view using Core Graphics. Note that this step is not really displayed, but obtains primitives data.

Note that in normal cases, only primitives information can be obtained in the Display stage, while bitmap can be drawn on the GPU according to the information. However, if you override the drawRect: method, this method will call the Core Graphics drawing method directly to get the bitmap data, and the system will request an extra block of memory to hold the drawn bitmap temporarily.

Because the drawRect: method was overwritten, the drawing process was moved from the GPU to the CPU, which resulted in a loss of efficiency. At the same time, this process uses extra CPU and memory, so if the drawing is not efficient enough, it can easily lead to CPU stuttering or memory exploding.

Obviously, an animation that overwrites the DrawRect: method is not suitable for frame reduction.

The Core Animation Animation

Since CALayer holds a static bitmap and some state information (transparency, rotation Angle, etc.), it is actually the state of the layer that changes during an animation, not the static content. This means that when the Animation happens, Core Animation will transfer the static bitmap and the changed state to THE GPU, and the GPU will draw the new style on the screen according to the bitmap and the new state.

In contrast to the more traditional uiView-based animation implementation of drawRect:, each animation change requires a new parameter to be redrawn, resulting in expensive CPU consumption for the main thread. Calayer-based animation can avoid these costs because the GPU draws directly from the bitmap, and the GPU will cache the bitmap, which can greatly save CPU performance and improve efficiency.

In general, Core Animation is the first choice to achieve complex Animation, but for frame reduction purposes, Core Animation cannot reduce the rendering frame rate below 60FPS.

UIView animation block

UIView Animation block is based on the encapsulation of Core Animation, which is simpler to use. At the same time, provide UIViewAnimationOptionPreferredFramesPerSecond30 UIView properties, can support specified animation refresh rate of 30 FPS.

Note: CoreAnimation can also reduce frames by setting the CAAnimation private property preferredFramesPerSecond. See iOS Run-time Headers for more information.

Through this attribute, the purpose of frame drop can be easily realized, but this scheme is not perfect, because only linear displacement is supported, so when it comes to the displacement of Bezier curve, it is necessary to manually calculate the point on bezier curve to carry out approximate displacement.

Frame by frame animation using CADisplayLink

CADisplayLink is a timer that allows us to draw content on the screen at the same rate as the screen refreshes. At the end of each refresh of the screen, the Runloop sends a selector method to the target, and the selector is called once.

Compared with the other two timers, NSTimer is slightly less accurate, and the delay time accumulates gradually. When runloop is blocked, NSTimer’s operation will be postponed to the next runloop, which is easy to cause animation control. The Dispatch_source_t timer is also not 100% accurate and its triggering event will be delayed if all threads managed internally by the GCD are occupied.

In fact, CADisplayLink is the ultimate solution. Finally, you can control the frame rate of animation by setting CADisplayLink’s timing frame rate. Instead of just 30 and 60 frames, you can better adapt to multiple situations and better balance GPU utilization and user experience.

Another advantage of CADisplayLink is the ability to accurately control multiple animation trigger times. If there are multiple low-frame animations going on at the same time, but the refresh time of these low-frame animations is not necessarily triggered at the same time, then the overall effect of frame drop is likely to be poor (extreme case, like the one above, where two 30-frame animations are going on at the same time but just stagger, resulting in a 60-frame effect). With CADisplayLink, you can accurately control the refresh time of multiple animations to ensure the effect of frame drop.

However, CADisplayLink requires a lot of code rewriting and a lot of work. For details, please refer to Facebook-POP. Although the library does not support custom frame rates, it has fully implemented custom animations based on CADisplayLink.

V. Test plan and conclusion

Final plan

Based on the above various reasons, this time chooses the UIView animation block for animation drop frame + UIViewAnimationOptionPreferredFramesPerSecond30 attributes, you can change the less code and the realization of the aim of drop frame.

Under this scheme, if the animation using the UIView animation block, so it is ok to add UIViewAnimationOptionPreferredFramesPerSecond30 property directly; If an animation is implemented based on CoreAnimation, it can also be quickly transformed into a UIView Animation block.

Here we take the example of entering The Tiktok live broadcast and triggering the “like” animation in the live broadcast room for comparison test:

  • Original plan (60 FPS) : Animation based on Core Animation
  • Frame scheme (30 FPS) : modify for the UIView animation block + UIViewAnimationOptionPreferredFramesPerSecond30

Effect on frame rate

In this picture, without any changes, it can be seen that Core Animation’s FPS will be fully loaded when the Animation is triggered crazily in the live broadcast room, keeping it at 59-60 FPS.

After adopting the frame drop scheme, it can be seen that the frame rate decreases significantly, and the FPS of the whole App can be reduced to about 40 FPS.

Impact on cpus and Gpus

After several tests, the data conclusions are summarized as follows:

  • When animation trigger frequency is low: Frame drop scheme can effectively reduce GPU usage (by 10%-20%) and slightly reduce CPU usage.
  • High frequency animation trigger or continuous animation trigger for a long time, after a certain critical time: under frame drop scheme, GPU and CPU occupancy increases.
  • The critical point is determined by machine performance, and the critical time of low-end machine is shorter. For newer models, critical times are hard to reach, but for iPhone 6, complex animations at slightly higher frequencies can lead to a rapid increase in GPU and CPU usage.

Critical time phenomenon interpretation

As can be seen from the above data conclusion, it is easy to encounter CPU bottleneck after the transformation to UIView Animation block scheme.

When animation blocks are too many (each animation requires too many blocks ✖️ the number of animations triggered per second is too many ✖️ the duration is too long) and cannot be digested, the CPU bottleneck will be encountered, resulting in increased CPU and GPU usage.

No specific explanation has been found for this phenomenon, but my guesses are as follows:

  • Block – -based animation of every Block, return a UIViewAdditiveAnimationAction class, for CALayer animations actionForKey: callback methods.
  • At the same time, according to the specific contents of the block to generate the corresponding class, need to convert fromValue and toValue, etc. (it is not clear whether conversion is implemented in the main thread, otherwise it may cause thread explosion). Animations require more CPU computation than animations based directly on CoreAnimation keyFrames.
  • Therefore, when blocks pile up too much, the CPU can be overtaxed.

conclusion

  • Through PreferredFramesPerSecond30 attributes can effectively reduce the frame refresh rate, thus reducing the CPU and GPU usage
  • After the refresh frame rate is reduced, the GPU occupancy decreases significantly, which can reach 10%-20%
  • When there are too many animation blocks to be digested, the CPU bottleneck will be encountered and the CPU and GPU usage will increase. And low-end phones are more likely to encounter bottlenecks.