思考CATransaction是如何捕獲layer變化的代碼設(shè)計

背景

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變化的录粱。

就像下面這樣腻格,包含在begincommit內(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ù)

所以這里有幾個問題:

  1. AutoreleasePoolPage是啥侥衬?

正如它的名字page,它就相當(dāng)于筆記本里的一頁紙,它保存了許多個對象常侦,這些對象都是加入到自動釋放池的那些浇冰。然后等一個page滿了,就開一個新的page,然后通過parent和child指針連接:

    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;

所以說它就是一個雙鏈表的結(jié)構(gòu)聋亡,每一個節(jié)點(diǎn)保存了若干的釋放池對象肘习。

AutoreleasePoolPage結(jié)構(gòu)示意圖
  1. hotPage是啥?
    hotPage就是當(dāng)前最新的一個page坡倔,它還有空間漂佩,可以繼續(xù)存儲對象。所以在push時罪塔,都是把內(nèi)容加入到hotPage投蝉。

  2. 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)前的灸促,并不是通過全局變量诫欠。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浴栽,隨后出現(xiàn)的幾起案子荒叼,更是在濱河造成了極大的恐慌,老刑警劉巖典鸡,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件被廓,死亡現(xiàn)場離奇詭異,居然都是意外死亡萝玷,警方通過查閱死者的電腦和手機(jī)嫁乘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進(jìn)店門英遭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人亦渗,你說我怎么就攤上這事挖诸。” “怎么了法精?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵多律,是天一觀的道長。 經(jīng)常有香客問我搂蜓,道長狼荞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任帮碰,我火速辦了婚禮相味,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘殉挽。我一直安慰自己丰涉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布斯碌。 她就那樣靜靜地躺著一死,像睡著了一般。 火紅的嫁衣襯著肌膚如雪傻唾。 梳的紋絲不亂的頭發(fā)上投慈,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機(jī)與錄音冠骄,去河邊找鬼伪煤。 笑死,一個胖子當(dāng)著我的面吹牛凛辣,可吹牛的內(nèi)容都是我干的抱既。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼蟀给,長吁一口氣:“原來是場噩夢啊……” “哼蝙砌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起跋理,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤择克,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后前普,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肚邢,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了骡湖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贱纠。...
    茶點(diǎn)故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖响蕴,靈堂內(nèi)的尸體忽然破棺而出谆焊,到底是詐尸還是另有隱情,我是刑警寧澤浦夷,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布辖试,位于F島的核電站,受9級特大地震影響劈狐,放射性物質(zhì)發(fā)生泄漏罐孝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一肥缔、第九天 我趴在偏房一處隱蔽的房頂上張望莲兢。 院中可真熱鬧,春花似錦续膳、人聲如沸改艇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遣耍。三九已至,卻和暖如春炮车,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酣溃。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工瘦穆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赊豌。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓扛或,卻偏偏與公主長得像,于是被迫代替她去往敵國和親碘饼。 傳聞我的和親對象是個殘疾皇子熙兔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容