内容简介:当一个view的图层(layer)属性发生变化的时候,系统是如何知道要去重新渲染这个图层呢?比如修改背景色:所以问题的核心就是CATransaction怎么捕获layer变化的。
UIView
实际是一个复合类型, CALayer
是它内部实际承担绘制显示任务的部分。
当一个view的图层(layer)属性发生变化的时候,系统是如何知道要去重新渲染这个图层呢?比如修改背景色: _testLayer.backgroundColor = [UIColor blueColor].CGColor;
。
- CATransaction会捕获CALayer的变化,包括任何的渲染属性,把这些都提交到一个中间态
- 然后在当前Runloop进入休眠或结束前,会发出Observer 消息。这是一种runloop消息类型,跟通知的方式类似,会通知观察者,这时Core Animation会把这些CALayer的变化提交给GPU绘制
所以问题的核心就是CATransaction怎么捕获layer变化的。
就像下面这样,包含在 begin
和 commit
内部的变化会被捕获。
[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这条数据
所以这里有几个问题:
- AutoreleasePoolPage是啥?
正如它的名字page,它就相当于笔记本里的一页纸,它保存了许多个对象,这些对象都是加入到自动释放池的那些。然后等一个page满了,就开一个新的page,然后通过parent和child指针连接:
AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; 复制代码
所以说它就是一个 双链表 的结构,每一个节点保存了若干的释放池对象。
-
hotPage是啥? hotPage就是当前最新的一个page,它还有空间,可以继续存储对象。所以在push时,都是把内容加入到hotPage。
-
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标识
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- js捕获错误信息
- Python捕获所有异常
- Android NativeCrash 捕获与解析
- Wireshark如何捕获USB流量
- 在 Docker 容器中捕获信号
- Laravel异常:捕获,处理和创建
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。