background
UIView is actually a compound type, and CALayer is the part of it that actually does the drawing and display.
How does the system know to re-render a view when its layer property changes? Such as change the background color: _testLayer. BackgroundColor = [UIColor blueColor]. CGColor; .
- The CATransaction captures CALayer’s changes, including any render properties, and submits them to an intermediate state
- An Observer message is then sent before the current Runloop goes to sleep or ends. This is a runloop message type, similar to notification, which notifies the observer. Core Animation submits these CALayer changes to the GPU for drawing
So the core of the problem is how CATransaction captures layer changes.
Changes contained within BEGIN and COMMIT are captured, as shown below.
[CATransaction begin];
_testLayer.backgroundColor = [UIColor blueColor].CGColor;
[CATransaction commit];
Copy the code
Why is it possible to modify layer directly in the main thread
Core Animation supports two types of transactions: implicit transactions and explicit transactions. Implicit transactions are created automatically when the layer tree is modified by a thread without an active transaction and are committed automatically when the thread’s runloop next iterates
Implicit transactions are automatically created when the layer tree is modified and committed during the next runloop iteration. The main thread has a runloop that opens automatically, so this works even if you don’t write the CATransaction code.
The real problem
However, this article is not concerned with the mechanics of CATransaction, CoreAnimation, or Runloop, but why it is sandwiched in [CATransaction Begin]; And [CATransaction commit]; Why can be caught by CATransaction, I care is the code design problem.
There are many ways to use this code syntax:
@autoreleasepool {
__autoreleasing UIButton *button = [[UIButton alloc] initWithFrame:(CGRectMake(30, 100, 100, 30))];
}
Copy the code
@synchronized (self) {// synchronized (self)Copy the code
[UIView beginAnimations:@""context:nil]; [UIView commitAnimations];Copy the code
As a counterexample to the comparison is the UITableView update:
UITableView *_tableview;
[_tableview beginUpdates];
[_tableview deleteSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:(UITableViewRowAnimationAutomatic)];
[_tableview endUpdates];
Copy the code
Tableview what’s the difference between this and the previous one? So they’re all in the same format as a piece of code wrapped in front and back, but the TableView is for an object, whereas the first three don’t specify an object or variable.
Guess [_tableView beginUpdates]; The logic of:
This should work immediately when calling methods like deleteSections, but not inside beginUpdates, so a check will do the trick. When updating methods like deleteSections, check whether they are between begin and end and do not handle them if they are, and do not handle them if they are not.
Instead, [UIView beginAnimations:@”” Context :nil]; In this case, you didn’t specify which view animation it was, how did it package the animation inside of it?
I guess
First of all, it’s not bound to an object or variable, but it’s storing information, so it must be using something global, like a global variable, or uiApplication-unique, or thread-unique.
Use this global variable for storage, for code like the following
[CATransaction begin];
_testLayer.backgroundColor = [UIColor blueColor].CGColor;
[CATransaction commit];
Copy the code
You can guess what it actually looks like:
// generate a newTransaction and return [CATransaction newTransaction]; {// This section is the logic inside layer to modify the background colorsetBackgroundColor{// Get the current CATransaction and provide changes to it. CATransaction *currentTrans = [CATransaction getCurrentTransaction]; [currentTrans addLayerChange:selfforKey:@"backgroundColor"]; } // commitLayerChanges and remove the current transaction [CATransaction commitLayerChanges]; [CATransaction removeCurrentTransaction];Copy the code
That is, just maintain a currently correct CATransaction.
But given that CATransaction can be nested, there is a process like this: transaction 1- > transaction 2 open -> Layer modification -> transaction 2 commit end -> back to transaction 1.
This looks like stack behavior, so you can use a global stack to manage CATransaction:
- Begin by creating a new CATransaction that is pushed to the top of the stack
- And then to get the current CATransaction, just get the top element on the stack
- At commit time, pop the top element of the stack. And commit the layer changes.
Verify the idea
Because the CATransaction code is invisible, you can’t validate the logic, but the Autoreleasepool code is, because some of the OC source code is open source. Here is the address.
- First of all,
@autoreleasepool {xxx}
Will be resolved into:
void *context = objc_autoreleasePoolPush(); // the code in {} is objc_autoreleasePoolPop(context);Copy the code
This is the same as CATransaction. The {} structure is only the function of the compiler.
- Then look at push first:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
Copy the code
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
Copy the code
There is a DebugPoolAllocation in the push code that causes the branch:
- AutoreleaseNewPage Creates an AutoreleasePoolPage as hotPage and adds the POOL_BOUNDARY data to the new page
- AutoreleaseFast adds POOL_BOUNDARY to the current hotPage
So here are a few questions:
- AutoreleasePoolPage is what?
As its name suggests, page is the equivalent of a page in a notebook, and it holds a number of objects that have been added to the auto-release pool. Then, when a page is full, open a new page and connect with the parent and child Pointers:
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
Copy the code
So it’s a double-linked list structure, where each node holds a number of release pool objects.
-
HotPage is what? HotPage is the latest page that has space to store objects. So when you push, you add content to the hotPage.
-
The POOL_BOUNDARY is used in the same AutoreleasePoolPage as the new release pool. The POOL_BOUNDARY is used in the same AutoreleasePoolPage as the new release pool.
So how do I tell which objects are in the current auto-release pool?
That’s the POOL_BOUNDARY thing, that’s what defines the boundary, it’s not the same automatic release pool on the left and right. Look at the positions of (1) and (2) in the diagram above. Insert a POOL_BOUNDARY on the top of (1) to indicate that the memory in the second page belongs to the new free pool.
However, the two different results caused by DebugPoolAllocation in push are the difference between (1) and (2). When a new release pool is opened, it is necessary to directly add the identity in the next slot or to create another page and insert the identity in the next slot.
- The POOL_BOUNDARY position was inserted when the hotPage was set to the latest flag bit.
So the flow is:
- Enable automatic release pool: Add a POOL_BOUNDARY identifier to the double linked list of AutoreleasePoolPage
- Object calls to autoRelease or the __autoreleasing flag are pushed into the current hotPage
- End of AutoreleasePoolPage: The double-linked list of AutoreleasePoolPage releases objects one by one until POOL_BOUNDARY identifies them
conclusion
Autorelease basically confirms my idea, which is to use the “global + stack structure” to deal with the inside. The AutoreleasePoolPage double-linked list is the behavior of the stack
The slight differences are:
- The auto-release pool itself is not encapsulated as a structure or object, it is just a section of a linked list. The structure I want is: global stack >CATransaction >layer change. The structure of the auto release pool is: global stack –>AutoreleasePoolPage–> variables. Instead, it flattens out the middle layer and puts data directly on the stack.
- Its “global”, which is thread-unique data, is obtained by binding the data to the thread, not by using global variables.