思考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標識