Core Animation Background Thread CATransaction

Hey everyone 👋

I'm trying to initialize a part of a CALayer hierarchy on a background thread and then attach the root of that hierarchy to a CALayer that backs a UIView.

The motivation is to keep the main thread responsive when constructing a complex layer hierarchy. This isn't a case where I'm creating two or three layers and then switching back to the main thread. The hierarchy can potentially contain a large number of layers, with animations being created/configured for those layers as well.

My first approach was to create and configure the layers entirely on a background thread. While the output might be the expected one (not always), CoreAnimation emits an assertion along the lines of: "Modifications to the layer tree from a background thread may not be committed" (or something like this).

This makes sense to me if implicit CATransaction is thread-local. In that case, the implicit transaction opened by the layer modification on the background thread would not be part of the transaction that is already open on the main thread. Therefore, committing the main-thread transaction would not commit the changes made on the background thread.

My second approach was to explicitly create and commit a CATransaction on the background thread. This appears to be accepted by Core Animation's threading model, but I'm seeing unreliable results. Sometimes parts of the hierarchy are missing, and in other cases the hierarchy is present but the animations don't appear to run at all.

I do understand that this is private behavior of the framework, but I wanted to know if what I am trying to achieve is possible and, if so, what the solution would be (obviously if you can share this information).

Besides this, I would also like to know what behavior CATransactions have when they are created on different threads. What I mean by this is that the transactions work as a stack, and the changes are committed when the stack is empty. Does this behavior still apply when having transactions on different threads? Any weird behaviours that might appear between transactions operated on main vs background threads?

Thank you!

Vlad.

The hierarchy can potentially contain a large number of layers, with animations being created/configured for those layers as well.

What is "large"?

What happens if you do it all on the main thread? Do you notice any hitches? Does instruments pick up any problem? If not, then there's no problem.

If there is a problem, this seems like a great opportunity to use Task yield to do everything on the main thread.

Thanks for the answer! I appreciate the suggestion, and I definitely understand the reasoning behind first checking whether the layer construction actually causes noticeable hitches before trying to optimize it.

In this particular case, I'm looking at this in the context of the Lottie library, where the layer hierarchy can become quite large and convoluted, with many layers and animations being created/configured. So I'm trying to understand whether there is a way to move as much of that work as possible off the main thread. (More context here: https://github.com/airbnb/lottie-ios/discussions/1886?utm_source=chatgpt.com).

To come back to your question, I don't have an exact number at the moment. I would say, though, that even one of the simplest Lottie files, Bouncing Ball, creates a hierarchy of around 10 layers with 7 animations added.

Of course, that's still a very small hierarchy and isn't an issue to construct on the main thread. I'm mentioning it mainly as a reference point, since more complex Lottie files can result in significantly larger and more convoluted hierarchies, especially when nested compositions are involved.

The Task.yield() suggestion is interesting and may well be a good solution for keeping the main thread responsive. That said, the library's current implementation doesn't really support this approach, so it would require a significant change for this. Before considering it, I'd like to focus on optimizing the interaction with Core Animation itself, if that's possible.

Vlad.

one of the simplest Lottie files, Bouncing Ball, creates a hierarchy of around 10 layers with 7 animations added.

I would classify that as "minuscule". But even so, when I search for Lottie Bouncing Ball, I get hundreds of them. Are you talking about this one? https://lottiefiles.github.io/lottie-docs/breakdown/bouncy_ball/

I don't see how that would be 10 layers. It looks like 1 layer and 3-4 animations.

That issue you referenced was from 2022. They were trying to do animations in a table view while scrolling. That's really not a good idea.

But someone in that thread did mention hundreds of layers taking several seconds. That seems unreasonable. Most likely they are simply doing something fundamentally wrong. I'm not familiar with that project. But CoreAnimation is very fast. It's more likely the slowdown is coming from something else, like reading JSON files.

Putting tasks on a background thread because they take too long, especially configuration tasks, is a bad idea. Typically Apple solves these kinds of issues by eliminating the configuration task and configuring structures on-the-fly, as needed.

Core Animation is good for objects that need to interact with the UI. If you're trying to do something that Core Animation can't handle, maybe you need to look at gaming APIs like SpriteKit.

Apologies, I was in a bit of a rush and sent the wrong link for the discussion above: https://github.com/airbnb/lottie-ios/discussions/1886. This does not represent what I want to achieve, I sent the wrong reference.

There was a separate discussion around this topic, but to be completely honest, I can’t seem to find it anymore. In any case, I had several private discussions with one of Lottie’s code owners about this. The main idea was to explore ways to make the main thread more responsive when working with large files, primarily by moving as much work as possible off the main thread (this includes only the UI work not decoding the json file work).

For the Bouncing Ball, that is the correct link. I thought it was pretty popular so I did not sent the link for it. I should have done that and assume nothing, apologies.

Regarding the number of layers, I’ll shortly show the layer hierarchy and the animations that Lottie creates for that file. As you can see, the BouncingBall file has 7 layers (8 in total, but one belongs to the container where the animation is added) and 8 animations. The setup and associated overhead become significantly more costly as the complexity of the file increases.

1. CALayer
2. LottieAnimationLayer
3. CoreAnimationLayer - animations: 1
4. ShapeLayer
5. BaseAnimationLayer - animations: 2
6. GroupLayer frame - animations: 4
7. ShapeItemLayer
8. CAShapeLayer - animations: 1

Putting tasks on a background thread because they take too long, especially configuration tasks, is a bad idea. Typically Apple solves these kinds of issues by eliminating the configuration task and configuring structures on-the-fly, as needed.

I’ll definitely look into this, but if you have any good resources or examples you’d recommend, I’d really appreciate it if you could share them. Thx!

Vlad.

One small clarification regarding the example above: when you see something like CoreAnimationLayer, that's a CALayer subclass. Also, the example was intentionally linear, meaning layer 1 is the parent of layer 2, layer 2 is the parent of layer 3, and so on. A more realistic layer tree would have multiple children attached to each parent.

That said, I think I may have deviated a bit from my original question. While your input is valuable and I really appreciate it, what I was primarily curious about is whether CoreAnimation supports any kind of offloading to worker/background threads.

If it does, I'm particularly interested in understanding what the execution model looks like: which parts of the work can happen off the calling thread, when that work is scheduled, and how it relates to the layer tree & transaction model.

From what I understand from your answer, the kind of offloading I'm describing shouldn't really happen. If so, the question I have is why.

I've looked through quite a few resources on this, but I haven't been able to find a clear explanation of the underlying model. If you don't have the time for the explanation part I would appreciate if you have any good resources on this topic. Thank you!

Vlad.

For the Bouncing Ball, that is the correct link. I thought it was pretty popular so I did not sent the link for it.

Sorry, I had never heard of Lottie until you mentioned it. Judging by how poorly it implements a simple bouncing ball, I wouldn't touch it.

Apple platforms lean very heavily on the main thread. Internally, they may do a lot of work on background threads. But for the 3rd party developer, trying to circumvent inefficient main thread code by doing it on a background thread is definitely "there be dragons" territory.

There are 3 main problems with this approach:

  1. You aren't doing anything to address the actual problem of inefficient main thread code.
  2. You're introducing a delay that's going to be visible, all because of a delay that's visible.
  3. These APIs were, in most cases, never designed to be used off the main thread. If they work at all, that's just bad luck. I say bad luck rather than good luck because it will encourage you to keep going down this path that can only lead to failure.

I suggested a quick and easy trick, which you can't use because of the cross-platform framework. And yet, it's the cross-platform framework that's creating this problem in the first place.

Maybe review why you are using the framework in the first place. See what you could do without it. Or look for a different framework. I would expect this to be a great use case for AI.

No problem, my bad. I should have provided the full context and not assumed that someone without my context could infer the information.

Regarding the implementation of the library, while I do agree that there is some complexity involved, I wouldn’t necessarily say that it is poorly implemented. As far as I understand it, part of the hierarchy complexity comes from the way the file is decoded. I don’t have enough information about that part to draw a meaningful conclusion, but I’m pretty sure there is a good reason for it to work this way.

That being said, I think I now have a better understanding of what you were trying to explain. While one solution would be to keep the current implementation and try to optimize it at the CoreAnimation level, that would just add more complexity to the existing solution & would work against the way the framework is intended. A better approach would be to optimize the configuration(as you have suggested in a previous message) that creates the complexity in the first place. With a simpler tree hierarchy, the issue I mentioned will most likely not even appear in the first place. I will try and see what I can obtain in that regard.

Appreciate your time and explanations!

Vlad.

Core Animation Background Thread CATransaction
 
 
Q