關(guān)于drawRect

由于?一直沒(méi)有好好學(xué)習(xí)UIView的繪制流程,關(guān)于UIView的drawRect一直以來(lái)都有兩個(gè)疑問(wèn):
1 為什么只在drawRect方法里才能獲取當(dāng)前圖層的上下文
2 drawRect不是號(hào)稱自定義實(shí)現(xiàn)UIView嗎,為什么我重寫(xiě)了drawRect原先設(shè)置的背景顏色和frame等等都沒(méi)變怕篷,不是應(yīng)該是我在drawRect寫(xiě)了什么就只顯示什么嗎?如:
代碼:

// ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.roosterView = [[RoosterView alloc] initWithFrame:self.view.bounds];
    self.roosterView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.roosterView];
}

// RoosterView
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    UIImage *myImage = [UIImage imageNamed:@"rooster"];
    CGRect myRect = CGRectMake(0, 0, myImage.size.width, myImage.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextTranslateCTM(context, 0, -(myRect.size.height-(myRect.size.height-2*myRect.origin.y-myRect.size.height)));//向上平移
    CGContextTranslateCTM (context, myRect.size.width/4, 0);
    CGContextScaleCTM (context, .25,  .5);
    CGContextRotateCTM (context, radians ( 22.));
    CGContextDrawImage(context, myRect, myImage.CGImage);
}
5F466471-C727-4C59-9DD5-18937678E2F7.png

不是只應(yīng)該顯示形變之后的圖片嗎迹炼?钓试!為什么還是占滿整個(gè)屏幕白色背景還在第美?不是說(shuō)重寫(xiě)drawRect對(duì)UIView進(jìn)行自定義嘛J啄帷L羰!

這里需要了解:真正被顯示的是layer软能,每一個(gè)在 UIKit 中的 view 都有它自己的 CALayer。每一個(gè)layer都有個(gè)content举畸,這個(gè)content指向的是一塊緩存查排,叫做backing store(后備存儲(chǔ)),backing store有點(diǎn)像一個(gè)圖像抄沮。這個(gè)后備存儲(chǔ)正是被渲染到顯示器上的跋核。
繪圖流程大概是:

  • 每一個(gè)UIView都有一個(gè)layer,每一個(gè)layer都有個(gè)content叛买,這個(gè)content指向的是一塊緩存砂代,叫做backing store。
  • UIView的繪制和渲染是兩個(gè)過(guò)程率挣,當(dāng)UIView被繪制時(shí)刻伊,CPU執(zhí)行drawRect,通過(guò)context將數(shù)據(jù)寫(xiě)入backing store椒功。
  • 當(dāng)backing store寫(xiě)完后捶箱,通過(guò)render server交給GPU去渲染,將backing store中的bitmap數(shù)據(jù)顯示在屏幕上

CALayer被繪制時(shí)方法調(diào)用棧:


drawRect的調(diào)用棧.png

首先:Core Animation 在 RunLoop 中注冊(cè)了一個(gè) Observer 監(jiān)聽(tīng) BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件 动漾。當(dāng)在操作 UI 時(shí)丁屎,比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí)旱眯,或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后晨川,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去删豺。當(dāng)Oberver監(jiān)聽(tīng)的事件到來(lái)時(shí)共虑,回調(diào)執(zhí)行函數(shù)中會(huì)遍歷所有待處理的UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整,并更新 UI 界面吼鳞。從圖中可以看到監(jiān)聽(tīng)回調(diào)看蚜。

接著:當(dāng)渲染系統(tǒng)準(zhǔn)備好,它會(huì)調(diào)用視圖圖層的-display方法.此時(shí)赔桌,圖層會(huì)裝配它的后備存儲(chǔ)供炎。然后建立一個(gè) Core Graphics 上下文(CGContextRef)渴逻,將后備存儲(chǔ)對(duì)應(yīng)內(nèi)存中的數(shù)據(jù)恢復(fù)出來(lái),繪圖會(huì)進(jìn)入對(duì)應(yīng)的內(nèi)存區(qū)域音诫,并使用 CGContextRef 繪制惨奕。
上圖監(jiān)聽(tīng)事件到來(lái)后出發(fā)一系列事件一直到-[CALayer display],根據(jù)微軟開(kāi)源WinObjc,display的主要工作有:

- (void)display {
        .......
       //  判斷contents是否有值
      if (priv->contents == NULL || priv->ownsContents || [self isKindOfClass:[CAShapeLayer class]]) {
     
        .......

        // 創(chuàng)建當(dāng)前圖層上下文
        CGContextRef drawContext = CreateLayerContentsBitmapContext32(width, height);

        priv->ownsContents = TRUE;
        CGImageRef target = CGBitmapContextGetImage(drawContext);

        CGContextRetain(drawContext);
        CGImageRetain(target);
        priv->savedContext = drawContext;

        ......
        // 設(shè)備坐標(biāo)和UIKit坐標(biāo)之間的轉(zhuǎn)換
        CGContextScaleCTM(drawContext, 1.0f, -1.0f);
        CGContextTranslateCTM(drawContext, -priv->bounds.origin.x, -priv->bounds.origin.y);
       
        CGContextSetDirty(drawContext, false);
        [self drawInContext:drawContext];

        if (priv->delegate != 0) {
            if ([priv->delegate respondsToSelector:@selector(displayLayer:)]) {
                [priv->delegate displayLayer:self];
            } else {
                [priv->delegate drawLayer:self inContext:drawContext];
            }
        }

        CGContextReleaseLock(drawContext);
        CGContextRelease(drawContext);

        // If we've drawn anything, set it as our contents
        if (!CGContextIsDirty(drawContext)) {
            CGImageRelease(target);
            CGContextRelease(drawContext);
            priv->savedContext = NULL;
            priv->contents = NULL;
        } else {
            priv->contents = target;
        }
    } else if (priv->contents) {
        priv->contentsSize.width = float(priv->contents->Backing()->Width());
        priv->contentsSize.height = float(priv->contents->Backing()->Height());
    }      
}

從調(diào)用棧截圖看出layer是在drawInContext:方法里調(diào)用了layer代理實(shí)現(xiàn)的
drawLayer:inContext:方法和以上代碼關(guān)于drawInContext:和代理函數(shù)[priv->delegate displayLayer:self];和[priv->delegate drawLayer:self inContext:drawContext];的調(diào)用時(shí)機(jī)有出入(待詳查)不過(guò)整體流程操作還是可以明白的竭钝。

代碼先判斷contents屬性是否有值梨撞,如果沒(méi)有就開(kāi)始創(chuàng)建自己的圖層關(guān)聯(lián)上下文,從上下文創(chuàng)建CGImageRef香罐,最后賦值給contents屬性卧波,這與文檔關(guān)于contents屬性的描述一致。
文檔:

If you are using the layer to display a static image, you can set this property to the CGImageRef containing the image you want to display. (In macOS 10.6 and later, you can also set the property to an NSImage object.) Assigning a value to this property causes the layer to use your image rather than create a separate backing store.
If the layer object is tied to a view object, you should avoid setting the contents of this property directly. The interplay between views and layers usually results in the view replacing the contents of this property during a subsequent update.

那么這些和只在drawRect方法里才能獲取當(dāng)前圖層的上下文有什么關(guān)系呢庇茫,依然看源碼:

- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
    UIGraphicsPushContext(context);

    CGRect bounds;
    bounds = CGContextGetClipBoundingBox(context);
    [self drawRect:bounds];

    UIGraphicsPopContext();
}

drawRect方法在drawLayer:inContext:里被調(diào)用港粱,并且被調(diào)用前有個(gè)UIGraphicsPushContext(context);方法將視圖圖層對(duì)應(yīng)上下文壓入棧頂,然后drawRect執(zhí)行完后旦签,將視圖圖層對(duì)應(yīng)上下文執(zhí)行出棧操作查坪。
系統(tǒng)會(huì)維護(hù)一個(gè)CGContextRef的棧,而UIGraphicsGetCurrentContext()會(huì)取棧頂?shù)腃GContextRef宁炫,當(dāng)前視圖圖層的上下文的入棧和出棧操作恰好將drawRect的執(zhí)行包裹在其中偿曙,所以說(shuō)只在drawRect方法里才能獲取當(dāng)前圖層的上下文。

第一個(gè)問(wèn)題知道了答案羔巢,那么是時(shí)候總結(jié)下第二道個(gè)問(wèn)題的答案了:對(duì)于view的frame望忆,backgroundColor各種設(shè)置是通過(guò)view間接操作了layer,繼而存儲(chǔ)到backing store朵纷,view給暴露出drawRect接口只是一個(gè)詢問(wèn)補(bǔ)充的目的炭臭,layer自己會(huì)裝配它的后備存儲(chǔ),生成了上下文袍辞,已經(jīng)玩的紅紅火火了鞋仍,為了表示對(duì)你的尊重,再問(wèn)你一句:大爺還有要補(bǔ)充的嗎搅吁?你重寫(xiě)了drawRect說(shuō)有威创。大家都說(shuō)drawRect自定義view說(shuō)白了其實(shí)只是一個(gè)補(bǔ)充的作用。

把drawRect說(shuō)的這么不堪谎懦,其實(shí)不是沒(méi)有憑據(jù)的肚豺,因?yàn)樘O(píng)果說(shuō):

這聽(tīng)起來(lái)貌似有點(diǎn)低俗,但是最快的繪制就是你不要做任何繪制界拦。
大多數(shù)時(shí)間吸申,你可以不要合成你在其他視圖(圖層)上定制的視圖(圖層),這正是我們推薦的,因?yàn)?UIKit 的視圖類是非常優(yōu)化的 (就是讓我們不要閑著沒(méi)事做,自己去合并視圖或圖層) 截碴。

最后:圖層的后備存儲(chǔ)將會(huì)被不斷的渲染到屏幕上梳侨。直到下次再次調(diào)用視圖的 -setNeedsDisplay ,將會(huì)依次將圖層的后備存儲(chǔ)更新到視圖上日丹。

在調(diào)用中drawRect之前的都在cpu中執(zhí)行走哺,然后GPU將bitmap從RAM移動(dòng)到VRAM將按像素計(jì)算將一層層圖層合成成一張圖然后顯示:

屏幕快照 2016-11-28 下午7.46.21.png

// 以下待進(jìn)一步驗(yàn)證:
drawRect調(diào)是在Controller->loadView, Controller->viewDidLoad 兩方法之后掉用的.所以不用擔(dān)心在控制器中,這些View的drawRect就開(kāi)始畫(huà)了.這樣可以在控制器中設(shè)置一些值給View(如果這些View draw的時(shí)候需要用到某些變量值).
1.如果在UIView初始化時(shí)沒(méi)有設(shè)置rect大小,將直接導(dǎo)致drawRect不被自動(dòng)調(diào)用哲虾。
2.該方法在調(diào)用sizeThatFits后被調(diào)用丙躏,所以可以先調(diào)用sizeToFit計(jì)算出size。然后系統(tǒng)自動(dòng)調(diào)用drawRect:方法束凑。
3.通過(guò)設(shè)置contentMode屬性值為UIViewContentModeRedraw晒旅。那么將在每次設(shè)置或更改frame的時(shí)候自動(dòng)調(diào)用drawRect:。
4.直接調(diào)用setNeedsDisplay湘今,或者setNeedsDisplayInRect:觸發(fā)drawRect:敢朱,但是有個(gè)前提條件是rect不能為0.
以上1,2推薦;而3,4不提倡

參考:
ObjC中國(guó)
iOS開(kāi)發(fā)之圖形渲染分析摩瞎、離屏渲染、當(dāng)前屏幕渲染孝常、On-Screen Rendering旗们、Off-Screen Rendering
iOS開(kāi)發(fā)筆記--iOS 事件處理機(jī)制與圖像渲染過(guò)程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市构灸,隨后出現(xiàn)的幾起案子上渴,更是在濱河造成了極大的恐慌,老刑警劉巖喜颁,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件稠氮,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡半开,警方通過(guò)查閱死者的電腦和手機(jī)隔披,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)寂拆,“玉大人奢米,你說(shuō)我怎么就攤上這事【烙溃” “怎么了鬓长?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)尝江。 經(jīng)常有香客問(wèn)我涉波,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任啤覆,我火速辦了婚禮苍日,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘城侧。我一直安慰自己易遣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布嫌佑。 她就那樣靜靜地躺著豆茫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪屋摇。 梳的紋絲不亂的頭發(fā)上揩魂,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音炮温,去河邊找鬼火脉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛柒啤,可吹牛的內(nèi)容都是我干的倦挂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼担巩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼方援!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起涛癌,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤犯戏,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后拳话,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體先匪,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年弃衍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呀非。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡笨鸡,死狀恐怖姜钳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情形耗,我是刑警寧澤哥桥,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站激涤,受9級(jí)特大地震影響拟糕,放射性物質(zhì)發(fā)生泄漏判呕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一送滞、第九天 我趴在偏房一處隱蔽的房頂上張望侠草。 院中可真熱鬧,春花似錦犁嗅、人聲如沸边涕。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)功蜓。三九已至,卻和暖如春宠蚂,著一層夾襖步出監(jiān)牢的瞬間式撼,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工求厕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留著隆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓呀癣,卻偏偏與公主長(zhǎng)得像美浦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子项栏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

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