由于?一直沒(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);
}
不是只應(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)用棧:
首先: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ì)算將一層層圖層合成成一張圖然后顯示:
// 以下待進(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ò)程