背景
UIView
實(shí)際是一個復(fù)合類型,CALayer
是它內(nèi)部實(shí)際承擔(dān)繪制顯示任務(wù)的部分宋舷。
當(dāng)一個view的圖層(layer)屬性發(fā)生變化的時候,系統(tǒng)是如何知道要去重新渲染這個圖層呢要销?比如修改背景色:_testLayer.backgroundColor = [UIColor blueColor].CGColor;
。
- CATransaction會捕獲CALayer的變化谷炸,包括任何的渲染屬性,把這些都提交到一個中間態(tài)
- 然后在當(dāng)前Runloop進(jìn)入休眠或結(jié)束前禀挫,會發(fā)出Observer 消息旬陡。這是一種runloop消息類型,跟通知的方式類似特咆,會通知觀察者季惩,這時Core Animation會把這些CALayer的變化提交給GPU繪制
所以問題的核心就是CATransaction怎么捕獲layer變化的录粱。
就像下面這樣腻格,包含在begin
和commit
內(nèi)部的變化會被捕獲。
[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
隱式的事務(wù)(Implicit transactions)會在圖層樹的修改的時候自動創(chuàng)建菜职,并且在下一次runloop迭代的時候提交。而主線程有一個自動開啟的runloop旗闽,所以即使不寫CATransaction代碼也會起作用酬核。
真正問題
但我這篇文章關(guān)心并不是CATransaction、CoreAnimation或runloop的機(jī)制問題适室,而是為什么被夾在[CATransaction begin];
和[CATransaction commit];
為什么能夠被CATransaction抓到嫡意,我關(guān)心是的是代碼設(shè)計上的問題。
其實(shí)這種代碼句式有很多地方用到:
@autoreleasepool {
__autoreleasing UIButton *button = [[UIButton alloc] initWithFrame:(CGRectMake(30, 100, 100, 30))];
}
@synchronized (self) {
//資源操作
}
[UIView beginAnimations:@"" context:nil];
//動畫內(nèi)容
[UIView commitAnimations];
作為對比的反例是UITableView的更新:
UITableView *_tableview;
[_tableview beginUpdates];
[_tableview deleteSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:(UITableViewRowAnimationAutomatic)];
[_tableview endUpdates];
tableview這個和前面的有什么區(qū)別捣辆?
雖然它們都是前后包裹的一段代碼這樣的格式蔬螟,但是tableview這個是針對一個對象的,而前面的3個是沒有指定對象或變量的汽畴。
先猜測一下[_tableview beginUpdates];
的邏輯:
在調(diào)用deleteSections
之類的方法時本來會立即起作用的旧巾,但是在beginUpdates
內(nèi)部就不會,那么用一個檢查就可以達(dá)到效果忍些。deleteSections
這類更新方法的時候鲁猩,先檢查是否在begin和end之間,是就不處理罢坝,否則就處理廓握。
而到[UIView beginAnimations:@"" context:nil];
這里,你根本沒有指定是哪個view的動畫嘁酿,它是怎么怎么把內(nèi)部的動畫打包的呢隙券?
我的猜測
首先沒有綁定某個對象或變量,但是它要存儲信息痹仙,那么肯定是用了某種全局性的東西是尔,比如全局變量,或者UIApplication唯一的开仰,或者當(dāng)前線程唯一的拟枚。
用這個全局的變量來存儲薪铜,對于像下面這樣的代碼
[CATransaction begin];
_testLayer.backgroundColor = [UIColor blueColor].CGColor;
[CATransaction commit];
可以猜測它實(shí)際是這樣的:
//生成一個新的事務(wù)并返回
[CATransaction newTransaction];
{ //這一段是layer修改背景色內(nèi)部的邏輯
setBackgroundColor{
//獲取當(dāng)前的CATransaction,并把修改提供給它
CATransaction *currentTrans = [CATransaction getCurrentTransaction];
[currentTrans addLayerChange:self forKey:@"backgroundColor"];
}
}
//提交layer變化并移除當(dāng)前的事務(wù)
[CATransaction commitLayerChanges];
[CATransaction removeCurrentTransaction];
也就是只要維持一個當(dāng)前正確的CATransaction就正確了恩溅。
但是考慮到CATransaction是可以嵌套的隔箍,那么就有這樣的過程:事務(wù)1-->事務(wù)2開啟-->layer修改-->事務(wù)2提交結(jié)束-->回到事務(wù)1。
這種一看就很符合棧的行為脚乡,所以可以使用一個全局的棧來管理CATransaction:
- begin的時候蜒滩,新建一個CATransaction,push放到棧頂
- 然后獲取當(dāng)前CATransaction的時候呢奶稠,就取棧頂元素就可以
- commit的時候俯艰,pop棧頂元素。并且把layer的變化提交锌订。
驗證想法
因為CATransaction的代碼看不到竹握,沒法驗證邏輯,但是autoreleasepool的代碼是可以看的辆飘,因為OC的一些源碼都開源了啦辐,這是地址。
- 首先
@autoreleasepool {xxx}
會被解析成:
void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);
看這個樣式蜈项,跟CATransaction是一樣的芹关,{}
的結(jié)構(gòu)其實(shí)只是編譯器的作用,其實(shí)還是前后一段代碼紧卒。
- 然后先看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這條數(shù)據(jù)加入這個新的page
- 而autoreleaseFast就是直接在當(dāng)前的hotPage里加入POOL_BOUNDARY這條數(shù)據(jù)
所以這里有幾個問題:
- AutoreleasePoolPage是啥侥衬?
正如它的名字page,它就相當(dāng)于筆記本里的一頁紙,它保存了許多個對象常侦,這些對象都是加入到自動釋放池的那些浇冰。然后等一個page滿了,就開一個新的page,然后通過parent和child指針連接:
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
所以說它就是一個雙鏈表的結(jié)構(gòu)聋亡,每一個節(jié)點(diǎn)保存了若干的釋放池對象肘习。
hotPage是啥?
hotPage就是當(dāng)前最新的一個page坡倔,它還有空間漂佩,可以繼續(xù)存儲對象。所以在push時罪塔,都是把內(nèi)容加入到hotPage投蝉。POOL_BOUNDARY的作用
這個東西至關(guān)重要,從上面的結(jié)構(gòu)里可以看出征堪,當(dāng)我開啟一個新的自動釋放池的時候瘩缆,并沒有開啟一個新的對象,就得釋放池和新的釋放池是在同一個AutoreleasePoolPage的雙鏈表里佃蚜。
那么我要怎么區(qū)分哪些對象是當(dāng)前的自動釋放池里的呢庸娱?
就是用POOL_BOUNDARY這個東西着绊,它就是用來確定邊界的,它左邊和右邊不是同一個自動釋放池熟尉」槁叮看上面的示意圖里的(1)和(2)的位置。比如第二個page還有一部分空間斤儿,這時開啟了一個新的自動釋放剧包,那么就是在(1)的這部分空間最頂上插入一個POOL_BOUNDARY作為標(biāo)識,這樣之后的內(nèi)存就是屬于新的釋放池了往果。
而push里因為DebugPoolAllocation造成的兩種不同結(jié)果疆液,只是開啟一個新的釋放池的時候是直接在下一個空位加入標(biāo)識,還是另建立一個page再插入標(biāo)識棚放,也就是位置(1)和(2)的區(qū)別枚粘。
- 在pop的時候,會把當(dāng)前的hotPage的數(shù)據(jù)一致刪飘蚯,刪到最新的標(biāo)志位,也就是開啟釋放池的時候插入的POOL_BOUNDARY位置福也。
所以流程就是:
- 開啟自動釋放池:在AutoreleasePoolPage的雙鏈表里加入一個POOL_BOUNDARY標(biāo)識
- 對象調(diào)用autoRelease或者標(biāo)記__autoreleasing就會被push到當(dāng)前的hotPage里
- 自動釋放池結(jié)束:AutoreleasePoolPage的雙鏈表把對象一個個釋放局骤,直到POOL_BOUNDARY標(biāo)識
結(jié)論
Autorelease的處理方式基本印證了我的想法,就是靠“全局+棧結(jié)構(gòu)”的方式來處里暴凑。AutoreleasePoolPage的雙鏈表就是棧的行為
略微的區(qū)別是:
- 自動釋放池本身是沒有封裝成一個結(jié)構(gòu)或者對象的峦甩,它只是鏈表中的一節(jié)。即我想的結(jié)構(gòu)是是:
全局的棧-->CATransaction-->layer的改變现喳。而自動釋放池的結(jié)構(gòu)是:全局的棧-->AutoreleasePoolPage-->變量凯傲。實(shí)際它是直接抹平了中間層,把數(shù)據(jù)直接放到棧里操控嗦篱。 - 它的“全局”冰单,是線程唯一的數(shù)據(jù),通過把數(shù)據(jù)和線程綁定來獲得當(dāng)前的灸促,并不是通過全局變量诫欠。