背景
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標識
結論
Autorelease的處理方式基本印證了我的想法,就是靠“全域性+棧結構”的方式來處裡。AutoreleasePoolPage的雙連結串列就是棧的行為
略微的區別是:
- 自動釋放池本身是沒有封裝成一個結構或者物件的,它只是連結串列中的一節。即我想的結構是是: 全域性的棧-->CATransaction-->layer的改變。而自動釋放池的結構是:全域性的棧-->AutoreleasePoolPage-->變數。實際它是直接抹平了中間層,把資料直接放到棧裡操控。
- 它的“全域性”,是執行緒唯一的資料,通過把資料和執行緒繫結來獲得當前的,並不是通過全域性變數。