思考CATransaction是如何捕獲layer變化的程式碼設計

FindCrt發表於2018-10-25

背景

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-->變數。實際它是直接抹平了中間層,把資料直接放到棧裡操控。
  • 它的“全域性”,是執行緒唯一的資料,通過把資料和執行緒繫結來獲得當前的,並不是通過全域性變數。

相關文章