思考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;

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

AutoreleasePoolPage结构示意图
  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标识

结论

Autorelease的处理方式基本印证了我的想法,就是靠“全局+栈结构”的方式来处里。AutoreleasePoolPage的双链表就是栈的行为

略微的区别是:

  • 自动释放池本身是没有封装成一个结构或者对象的,它只是链表中的一节。即我想的结构是是:
    全局的栈-->CATransaction-->layer的改变。而自动释放池的结构是:全局的栈-->AutoreleasePoolPage-->变量。实际它是直接抹平了中间层,把数据直接放到栈里操控。
  • 它的“全局”,是线程唯一的数据,通过把数据和线程绑定来获得当前的,并不是通过全局变量。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容