思考CATransaction是如何捕获layer变化的代码设计

栏目: IOS · 发布时间: 7年前

内容简介:当一个view的图层(layer)属性发生变化的时候,系统是如何知道要去重新渲染这个图层呢?比如修改背景色:所以问题的核心就是CATransaction怎么捕获layer变化的。

UIView 实际是一个复合类型, CALayer 是它内部实际承担绘制显示任务的部分。

当一个view的图层(layer)属性发生变化的时候,系统是如何知道要去重新渲染这个图层呢?比如修改背景色: _testLayer.backgroundColor = [UIColor blueColor].CGColor;

  • CATransaction会捕获CALayer的变化,包括任何的渲染属性,把这些都提交到一个中间态
  • 然后在当前Runloop进入休眠或结束前,会发出Observer 消息。这是一种runloop消息类型,跟通知的方式类似,会通知观察者,这时Core Animation会把这些CALayer的变化提交给GPU绘制

所以问题的核心就是CATransaction怎么捕获layer变化的。

就像下面这样,包含在 begincommit 内部的变化会被捕获。

[CATransaction begin];
    _testLayer.backgroundColor = [UIColor blueColor].CGColor;
    [CATransaction commit];
复制代码

至于主线程里直接修改layer为什么也可以,是因为

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)会在图层树的修改的时候自动创建,并且在下一次runloop迭代的时候提交。而主线程有一个自动开启的runloop,所以即使不写CATransaction代码也会起作用。

真正问题

但我这篇文章关心并不是CATransaction、CoreAnimation或runloop的机制问题,而是为什么被夹在 [CATransaction begin];[CATransaction commit]; 为什么能够被CATransaction抓到,我关心是的是代码设计上的问题。

其实这种代码句式有很多地方用到:

@autoreleasepool {
        __autoreleasing UIButton *button = [[UIButton alloc] initWithFrame:(CGRectMake(30, 100, 100, 30))];
    }
复制代码
@synchronized (self) {
        //资源操作
    }
复制代码
[UIView beginAnimations:@"" context:nil];
    //动画内容
[UIView commitAnimations];
复制代码

作为对比的反例是UITableView的更新:

UITableView *_tableview;
    
    [_tableview beginUpdates];
    [_tableview deleteSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:(UITableViewRowAnimationAutomatic)];
    [_tableview endUpdates];
复制代码

tableview这个和前面的有什么区别? 虽然它们都是前后包裹的一段代码这样的格式,但是tableview这个是针对一个对象的,而前面的3个是没有指定对象或变量的。

先猜测一下 [_tableview beginUpdates]; 的逻辑:

在调用 deleteSections 之类的方法时本来会立即起作用的,但是在 beginUpdates 内部就不会,那么用一个检查就可以达到效果。 deleteSections 这类更新方法的时候,先检查是否在begin和end之间,是就不处理,否则就处理。

而到 [UIView beginAnimations:@"" context:nil]; 这里,你根本没有指定是哪个view的动画,它是怎么怎么把内部的动画打包的呢?

我的猜测

首先没有绑定某个对象或变量,但是它要存储信息,那么肯定是用了某种全局性的东西,比如全局变量,或者UIApplication唯一的,或者当前线程唯一的。

用这个全局的变量来存储,对于像下面这样的代码

[CATransaction begin];
    _testLayer.backgroundColor = [UIColor blueColor].CGColor;
    [CATransaction commit];
复制代码

可以猜测它实际是这样的:

//生成一个新的事务并返回
    [CATransaction newTransaction];
    
    {   //这一段是layer修改背景色内部的逻辑
        setBackgroundColor{
            
            //获取当前的CATransaction,并把修改提供给它
            CATransaction *currentTrans = [CATransaction getCurrentTransaction];
            [currentTrans addLayerChange:self forKey:@"backgroundColor"];
        }
    }
    
    //提交layer变化并移除当前的事务
    [CATransaction commitLayerChanges];
    [CATransaction removeCurrentTransaction];
复制代码

也就是只要维持一个当前正确的CATransaction就正确了。

但是考虑到CATransaction是可以嵌套的,那么就有这样的过程:事务1-->事务2开启-->layer修改-->事务2提交结束-->回到事务1。

这种一看就很符合栈的行为,所以可以使用一个全局的栈来管理CATransaction:

  • begin的时候,新建一个CATransaction,push放到栈顶
  • 然后获取当前CATransaction的时候呢,就取栈顶元素就可以
  • commit的时候,pop栈顶元素。并且把layer的变化提交。

验证想法

因为CATransaction的代码看不到,没法验证逻辑,但是autoreleasepool的代码是可以看的,因为OC的一些源码都开源了,这是地址。

  • 首先 @autoreleasepool {xxx} 会被解析成:
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
复制代码

看这个样式,跟CATransaction是一样的, {} 的结构其实只是编译器的作用,其实还是前后一段代码。

  • 然后先看push:
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
复制代码
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;
    }
复制代码

push的代码里有个DebugPoolAllocation引起分支:

  • autoreleaseNewPage这个干的事就是:新建一个AutoreleasePoolPage,把它作为 hotPage 然后把POOL_BOUNDARY这条数据加入这个新的page
  • 而autoreleaseFast就是直接在当前的 hotPage 里加入POOL_BOUNDARY这条数据

所以这里有几个问题:

  1. AutoreleasePoolPage是啥?

正如它的名字page,它就相当于笔记本里的一页纸,它保存了许多个对象,这些对象都是加入到自动释放池的那些。然后等一个page满了,就开一个新的page,然后通过parent和child指针连接:

AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
复制代码

所以说它就是一个 双链表 的结构,每一个节点保存了若干的释放池对象。

思考CATransaction是如何捕获layer变化的代码设计
  1. hotPage是啥? hotPage就是当前最新的一个page,它还有空间,可以继续存储对象。所以在push时,都是把内容加入到hotPage。

  2. POOL_BOUNDARY的作用 这个东西至关重要,从上面的结构里可以看出,当我开启一个新的自动释放池的时候,并没有开启一个新的对象,就得释放池和新的释放池是在同一个AutoreleasePoolPage的双链表里。

那么我要怎么区分哪些对象是当前的自动释放池里的呢?

就是用POOL_BOUNDARY这个东西,它就是用来确定边界的,它左边和右边不是同一个自动释放池。看上面的示意图里的(1)和(2)的位置。比如第二个page还有一部分空间,这时开启了一个新的自动释放,那么就是在(1)的这部分空间最顶上插入一个POOL_BOUNDARY作为标识,这样之后的内存就是属于新的释放池了。

而push里因为DebugPoolAllocation造成的两种不同结果,只是开启一个新的释放池的时候是直接在下一个空位加入标识,还是另建立一个page再插入标识,也就是位置(1)和(2)的区别。

  • 在pop的时候,会把当前的hotPage的数据一致删,删到最新的标志位,也就是开启释放池的时候插入的POOL_BOUNDARY位置。

所以流程就是:

  • 开启自动释放池:在AutoreleasePoolPage的双链表里加入一个POOL_BOUNDARY标识
  • 对象调用autoRelease或者标记__autoreleasing就会被push到当前的hotPage里
  • 自动释放池结束:AutoreleasePoolPage的双链表把对象一个个释放,直到POOL_BOUNDARY标识

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Data Structures and Algorithms in Java

Data Structures and Algorithms in Java

Robert Lafore / Sams / 2002-11-06 / USD 64.99

Data Structures and Algorithms in Java, Second Edition is designed to be easy to read and understand although the topic itself is complicated. Algorithms are the procedures that software programs use......一起来看看 《Data Structures and Algorithms in Java》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具